diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..8886e959b1d43c04a1fb74753cf0ccd7438dadfd --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1 @@ +FROM mcr.microsoft.com/vscode/devcontainers/base:ubuntu-22.04 diff --git a/.devcontainer/boot.sh b/.devcontainer/boot.sh new file mode 100644 index 0000000000000000000000000000000000000000..8f30c9dfa98ba08b812f740844b6a5d6e51f7fe6 --- /dev/null +++ b/.devcontainer/boot.sh @@ -0,0 +1,44 @@ +#!/bin/bash + +CURRENT_DIR="$(pwd)" +BACKEND_PORT=3000 +WEB_PORT=3001 + +echo "Configuring backend environment variables..." +cd packages/backend +rm -rf .env +echo " +PORT=$BACKEND_PORT +WEB_APP_URL=http://localhost:$WEB_PORT +APP_ENV=development +POSTGRES_DATABASE=automatisch +POSTGRES_PORT=5432 +POSTGRES_HOST=postgres +POSTGRES_USERNAME=automatisch_user +POSTGRES_PASSWORD=automatisch_password +ENCRYPTION_KEY=sample_encryption_key +WEBHOOK_SECRET_KEY=sample_webhook_secret_key +APP_SECRET_KEY=sample_app_secret_key +REDIS_HOST=redis +SERVE_WEB_APP_SEPARATELY=true" >> .env +cd $CURRENT_DIR + +echo "Configuring web environment variables..." +cd packages/web +rm -rf .env +echo " +PORT=$WEB_PORT +REACT_APP_BACKEND_URL=http://localhost:$BACKEND_PORT +" >> .env +cd $CURRENT_DIR + +echo "Installing and linking dependencies..." +yarn +yarn lerna bootstrap + +echo "Migrating database..." +cd packages/backend +yarn db:migrate +yarn db:seed:user + +echo "Done!" \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000000000000000000000000000000..19273bbe5386edb9f83613350c958e9bf9d04317 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,53 @@ +{ + "name": "Automatisch", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + "features": { + "ghcr.io/devcontainers/features/git:1": { + "version": "latest" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": 18 + }, + "ghcr.io/devcontainers/features/common-utils:1": { + "username": "vscode", + "uid": 1000, + "gid": 1000, + "installZsh": true, + "installOhMyZsh": true, + "configureZshAsDefaultShell": true, + "upgradePackages": true + } + }, + + "hostRequirements": { + "cpus": 4, + "memory": "8gb", + "storage": "32gb" + }, + + "portsAttributes": { + "3000": { + "label": "Backend", + "onAutoForward": "silent", + "protocol": "http" + }, + "3001": { + "label": "Frontend", + "onAutoForward": "silent", + "protocol": "http" + } + }, + + "forwardPorts": [3000, 3001], + + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": ["bash", ".devcontainer/boot.sh"] + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..580c9f5506a4b0221ac2ddb709e65da9856c7b5c --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,63 @@ +version: '3.9' + +services: + app: + build: + context: .. + dockerfile: .devcontainer/Dockerfile + volumes: + - ..:/workspace:cached + command: sleep infinity + postgres: + image: 'postgres:14.5-alpine' + environment: + - POSTGRES_DB=automatisch + - POSTGRES_USER=automatisch_user + - POSTGRES_PASSWORD=automatisch_password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] + interval: 10s + timeout: 5s + retries: 5 + ports: + - '5432:5432' + expose: + - 5432 + redis: + image: 'redis:7.0.4-alpine' + volumes: + - redis_data:/data + ports: + - '6379:6379' + expose: + - 6379 + keycloak: + image: quay.io/keycloak/keycloak:21.1 + restart: always + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_DB=postgres + - KC_DB_URL_HOST=postgres + - KC_DB_URL_DATABASE=keycloak + - KC_DB_USERNAME=automatisch_user + - KC_DB_PASSWORD=automatisch_password + - KC_HEALTH_ENABLED=true + ports: + - "8080:8080" + command: start-dev + depends_on: + - postgres + healthcheck: + test: "curl -f http://localhost:8080/health/ready || exit 1" + volumes: + - keycloak:/opt/keycloak/data/ + expose: + - 8080 + +volumes: + postgres_data: + redis_data: + keycloak: diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..cc455d40826ddf96ff8699fa5bf0ae8bc52bb3ee --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +**/node_modules/ +**/dist/ +**/logs/ +**/.devcontainer +**/.github +**/.vscode +**/.env +**/.env.test +**/.env.production +**/yarn-error.log +packages/docs +packages/e2e-test diff --git a/.github/workflows/backend.yml b/.github/workflows/backend.yml new file mode 100644 index 0000000000000000000000000000000000000000..9874f2887602dea3eea545f76f555ae43b815dc2 --- /dev/null +++ b/.github/workflows/backend.yml @@ -0,0 +1,48 @@ +name: Automatisch Backend Tests +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + test: + timeout-minutes: 60 + runs-on: + - ubuntu-latest + services: + postgres: + image: postgres:14.5-alpine + env: + POSTGRES_DB: automatisch_test + POSTGRES_USER: automatisch_test_user + POSTGRES_PASSWORD: automatisch_test_user_password + options: >- + --health-cmd "pg_isready -U automatisch_test_user -d automatisch_test" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:7.0.4-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: cd packages/backend && yarn + - name: Copy .env-example.test file to .env.test + run: cd packages/backend && cp .env-example.test .env.test + - name: Run tests + run: cd packages/backend && yarn test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..f200e74e4442d3b4d436df3b7cac2d63a3c51ba7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,85 @@ +name: Automatisch CI +on: + push: + branches: + - main + pull_request: + workflow_dispatch: + +jobs: + linter: + runs-on: ubuntu-latest + steps: + - run: echo "πŸŽ‰ The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "πŸ”Ž The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'yarn' + cache-dependency-path: yarn.lock + - run: echo "πŸ’‘ The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "πŸ–₯️ The workflow is now ready to test your code on the runner." + - run: yarn --frozen-lockfile + - run: cd packages/backend && yarn lint + - run: echo "🍏 This job's status is ${{ job.status }}." + start-backend-server: + runs-on: ubuntu-latest + steps: + - run: echo "πŸŽ‰ The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "πŸ”Ž The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'yarn' + cache-dependency-path: yarn.lock + - run: echo "πŸ’‘ The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "πŸ–₯️ The workflow is now ready to test your code on the runner." + - run: yarn --frozen-lockfile && yarn lerna bootstrap + - run: cd packages/backend && yarn start + env: + ENCRYPTION_KEY: sample_encryption_key + WEBHOOK_SECRET_KEY: sample_webhook_secret_key + - run: echo "🍏 This job's status is ${{ job.status }}." + start-backend-worker: + runs-on: ubuntu-latest + steps: + - run: echo "πŸŽ‰ The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "πŸ”Ž The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'yarn' + cache-dependency-path: yarn.lock + - run: echo "πŸ’‘ The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "πŸ–₯️ The workflow is now ready to test your code on the runner." + - run: yarn --frozen-lockfile && yarn lerna bootstrap + - run: cd packages/backend && yarn start:worker + env: + ENCRYPTION_KEY: sample_encryption_key + WEBHOOK_SECRET_KEY: sample_webhook_secret_key + - run: echo "🍏 This job's status is ${{ job.status }}." + build-web: + runs-on: ubuntu-latest + steps: + - run: echo "πŸŽ‰ The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "πŸ”Ž The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '18' + cache: 'yarn' + cache-dependency-path: yarn.lock + - run: echo "πŸ’‘ The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "πŸ–₯️ The workflow is now ready to test your code on the runner." + - run: yarn --frozen-lockfile && yarn lerna bootstrap + - run: cd packages/web && yarn build + env: + CI: false + - run: echo "🍏 This job's status is ${{ job.status }}." diff --git a/.github/workflows/docs-change.yml b/.github/workflows/docs-change.yml new file mode 100644 index 0000000000000000000000000000000000000000..b95b80ff074536251664b054436a198c23a2f4f2 --- /dev/null +++ b/.github/workflows/docs-change.yml @@ -0,0 +1,32 @@ +name: Automatisch Docs Change +on: + pull_request: + paths: + - 'packages/docs/**' +jobs: + label: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Label PR + uses: actions/github-script@v6 + with: + script: | + const { pull_request } = context.payload; + + const label = 'documentation-change'; + const hasLabel = pull_request.labels.some(({ name }) => name === label); + + if (!hasLabel) { + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pull_request.number, + labels: [label], + }); + + console.log(`Label "${label}" added to PR #${pull_request.number}`); + } else { + console.log(`Label "${label}" already exists on PR #${pull_request.number}`); + } diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000000000000000000000000000000000..7b774780dfca615f50f1179d79d5dbfc34fefa20 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,122 @@ +name: Automatisch UI Tests +on: + push: + branches: + - main + pull_request: + paths: + - 'packages/backend/**' + - 'packages/e2e-tests/**' + - 'packages/web/**' + - '!packages/backend/src/apps/**' + workflow_dispatch: + +env: + ENCRYPTION_KEY: sample_encryption_key + WEBHOOK_SECRET_KEY: sample_webhook_secret_key + APP_SECRET_KEY: sample_app_secret_key + POSTGRES_HOST: localhost + POSTGRES_DATABASE: automatisch + POSTGRES_PORT: 5432 + POSTGRES_USERNAME: automatisch_user + POSTGRES_PASSWORD: automatisch_password + REDIS_HOST: localhost + APP_ENV: production + LICENSE_KEY: dummy_license_key + +jobs: + test: + timeout-minutes: 60 + runs-on: + - ubuntu-latest + services: + postgres: + image: postgres:14.5-alpine + env: + POSTGRES_DB: automatisch + POSTGRES_USER: automatisch_user + POSTGRES_PASSWORD: automatisch_password + options: >- + --health-cmd "pg_isready -U automatisch_user -d automatisch" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + redis: + image: redis:7.0.4-alpine + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 6379:6379 + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 18 + - name: Install dependencies + run: yarn && yarn lerna bootstrap + - name: Install Playwright Browsers + run: yarn playwright install --with-deps + - name: Build Automatisch web + working-directory: ./packages/web + run: yarn build + env: + # Keep this until we clean up warnings in build processes + CI: false + - name: Migrate database + working-directory: ./packages/backend + run: yarn db:migrate + - name: Seed user + working-directory: ./packages/backend + run: yarn db:seed:user & + - name: Install certutils + run: sudo apt install -y libnss3-tools + - name: Install mkcert + run: | + curl -JLO "https://dl.filippo.io/mkcert/latest?for=linux/amd64" \ + && chmod +x mkcert-v*-linux-amd64 \ + && sudo cp mkcert-v*-linux-amd64 /usr/local/bin/mkcert + - name: Install root certificate via mkcert + run: mkcert -install + - name: Create certificate + run: mkcert automatisch.io "*.automatisch.io" localhost 127.0.0.1 ::1 + working-directory: ./packages/e2e-tests + - name: Set CAROOT environment variable + run: echo "NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem" >> "$GITHUB_ENV" + - name: Override license server with local server + run: sudo echo "127.0.0.1 license.automatisch.io" | sudo tee -a /etc/hosts + - name: Run local license server + working-directory: ./packages/e2e-tests + run: sudo yarn start-mock-license-server & + - name: Run Automatisch + run: yarn start & + working-directory: ./packages/backend + - name: Run Automatisch worker + run: yarn start:worker & + working-directory: ./packages/backend + - name: Setup upterm session + if: false + uses: lhotari/action-upterm@v1 + with: + limit-access-to-actor: true + limit-access-to-users: barinali + - name: Run Playwright tests + working-directory: ./packages/e2e-tests + env: + LOGIN_EMAIL: user@automatisch.io + LOGIN_PASSWORD: sample + BASE_URL: http://localhost:3000 + GITHUB_CLIENT_ID: 1c0417daf898adfbd99a + GITHUB_CLIENT_SECRET: 3328fa814dd582ccd03dbe785cfd683fb8da92b3 + run: yarn test + - uses: actions/upload-artifact@v3 + if: always() + with: + name: playwright-report + path: packages/e2e-tests/test-results + retention-days: 30 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..3f9eca9148069dfc88b293fb4f00178b3e781416 --- /dev/null +++ b/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test +.env.production + +# cypress environment variables file +cypress.env.json + +# cypress screenshots +packages/e2e-tests/cypress/screenshots + +# cypress videos +packages/e2e-tests/cypress/videos/ + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# MacOS finder preferences +.DS_store diff --git a/.node-version b/.node-version new file mode 100644 index 0000000000000000000000000000000000000000..a9d087399d711261c7625e8304189e942c619347 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +18.19.0 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000000000000000000000000000000..a9d087399d711261c7625e8304189e942c619347 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.19.0 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000000000000000000000000000000000..e340799c76e6114844498eb4bd6a7b523de313aa --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +module.exports = { + singleQuote: true, +}; diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000000000000000000000000000000000000..0619573eccfe65807a4c770ea6c52c1ce5249672 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Launch server-side", + "type": "node-terminal", + "request": "launch", + "cwd": "${workspaceFolder}/packages/backend", + "command": "yarn dev" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..f62af9949ccdb9943719f1bf1384e3b74ad3630a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } +} diff --git a/.yarnrc b/.yarnrc new file mode 100644 index 0000000000000000000000000000000000000000..568d24553b5f58514b06d3e55371fd5c77186b73 --- /dev/null +++ b/.yarnrc @@ -0,0 +1 @@ +network-timeout 400000 diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000000000000000000000000000000000000..bdbcb85a0649c2dd1cc9afd9594a3326c36065e3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,76 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +Demonstrating empathy and kindness toward other people +Being respectful of differing opinions, viewpoints, and experiences +Giving and gracefully accepting constructive feedback +Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +Focusing on what is best not just for us as individuals, but for the overall community +Examples of unacceptable behavior include: + +The use of sexualized language or imagery, and sexual attention or advances of any kind +Trolling, insulting or derogatory comments, and personal or political attacks +Public or private harassment +Publishing others’ private information, such as a physical or email address, without their explicit permission +Other conduct which could reasonably be considered inappropriate in a professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. + +Community leaders 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, and will communicate reasons for moderation decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at ali@automatisch.io. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. + +Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +Community Impact: A violation through a single incident or series of actions. + +Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. + +### 3. Temporary Ban + +Community Impact: A serious violation of community standards, including sustained inappropriate behavior. + +Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. + +Consequence: A permanent ban from any sort of public interaction within the community. + +## Attribution + +This Code of Conduct is adapted from the Contributor Covenant, version 2.1, available at https://www.contributor-covenant.org/version/2/1/code_of_conduct.html. + +Community Impact Guidelines were inspired by Mozilla’s code of conduct enforcement ladder. + +For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTOR_LICENSE_AGREEMENT.md b/CONTRIBUTOR_LICENSE_AGREEMENT.md new file mode 100644 index 0000000000000000000000000000000000000000..423eb41ce238e3f32f92853af2ae9c4b4dcec92a --- /dev/null +++ b/CONTRIBUTOR_LICENSE_AGREEMENT.md @@ -0,0 +1,5 @@ +# Automatisch Contributor License Agreement + +I give Automatisch permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project. + +**_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._** diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..daa2e5d755342747825ef7525c9778a76730065d --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +LICENSE.agpl (AGPL-3.0) applies to all files in this +repository, except for files that contain ".ee." in their name +which are covered by LICENSE.enterprise. diff --git a/LICENSE.agpl b/LICENSE.agpl new file mode 100644 index 0000000000000000000000000000000000000000..162676cbef614cc119c0469097f6868178019f95 --- /dev/null +++ b/LICENSE.agpl @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + Preamble + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + +0. Definitions. + +"This License" refers to version 3 of the GNU Affero General Public License. + +"Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +"The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + +To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + +A "covered work" means either the unmodified Program or a work based +on the Program. + +To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +1. Source Code. + +The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + +A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +2. Basic Permissions. + +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +3. Protecting Users' Legal Rights From Anti-Circumvention Law. + +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +4. Conveying Verbatim Copies. + +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +5. Conveying Modified Source Versions. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +6. Conveying Non-Source Forms. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +"Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +7. Additional Terms. + +"Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + +All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +8. Termination. + +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +9. Acceptance Not Required for Having Copies. + +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +10. Automatic Licensing of Downstream Recipients. + +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +11. Patents. + +A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + +A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +12. No Surrender of Others' Freedom. + +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +13. Remote Network Interaction; Use with the GNU General Public License. + +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +14. Revised Versions of this License. + +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +15. Disclaimer of Warranty. + +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +16. Limitation of Liability. + +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +17. Interpretation of Sections 15 and 16. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSE.enterprise b/LICENSE.enterprise new file mode 100644 index 0000000000000000000000000000000000000000..a76c7a5acf9bebeffbbf8a66e3191b4803749bd4 --- /dev/null +++ b/LICENSE.enterprise @@ -0,0 +1,35 @@ +The Automatisch Enterprise license (the β€œEnterprise License”) +Copyright (c) 2023-present AB Software GmbH. + +With regard to the Automatisch Software: + +This software and associated documentation files (the "Software") may only be +used in production, if you (and any entity that you represent) have a valid +Automatisch Enterprise license for the correct number of user seats. Subject +to the foregoing sentence, you are free to modify this Software and publish +patches to the Software. You agree that Automatisch and/or its licensors +(as applicable) retain all right, title and interest in and to all such +modifications and/or patches, and all such modifications and/or patches may +only be used, copied, modified, displayed, distributed, or otherwise exploited +with a valid Automatisch Enterprise license for the correct number of user seats. +Notwithstanding the foregoing, you may copy and modify the Software for +development and testing purposes, without requiring a subscription. You agree +that Automatisch and/or its licensors (as applicable) retain all right, title +and interest in and to all such modifications. You are not granted any other +rights beyond what is expressly stated herein. Subject to the foregoing, it is +forbidden to copy, merge, publish, distribute, sublicense, and/or sell the Software. + +The full text of this Enterprise License shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +For all third party components incorporated into the Automatisch Software, those +components are licensed under the original license provided by the owner of the +applicable component. diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..66ee121838c4b10521b86d3b4cfa78c5713a2062 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Automatisch - Open Source Zapier Alternative + +![Automatisch - Screenshot](https://user-images.githubusercontent.com/2501931/191562539-e42f6c34-03c7-4dc4-bcf9-7f9473a9c64f.png) + +🧐 Automatisch is a business automation tool that lets you connect different services like Twitter, Slack, and more to automate your business processes. + +πŸ’Έ Automating your workflows doesn't have to be a difficult or expensive process. You also don't need any programming knowledge to use Automatisch. + +## Advantages + +There are other existing solutions in the market, like Zapier and Integromat, so you might be wondering why you should use Automatisch. + +βœ… One of the main benefits of using Automatisch is that it allows you to store your data on your own servers, which is essential for businesses that handle sensitive user information and cannot risk sharing it with external cloud services. This is especially relevant for industries such as healthcare and finance, as well as for European companies that must adhere to the General Data Protection Regulation (GDPR). + +πŸ€“ Your contributions are vital to the development of Automatisch. As an open-source software, anyone can have an impact on how it is being developed. + +πŸ’™ No vendor lock-in. If you ever decide that Automatisch is no longer helpful for your business, you can switch to any other provider, which will be easier than switching from the one cloud provider to another since you have all data and flexibility. + +## Documentation + +The official documentation can be found here: [https://automatisch.io/docs](https://automatisch.io/docs) + +## Installation + +```bash +# Clone the repository +git clone https://github.com/automatisch/automatisch.git + +# Go to the repository folder +cd automatisch + +# Start +docker compose up +``` + +You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. Please do not forget to change your email and password from the settings page. + +For other installation types, you can check the [installation](https://automatisch.io/docs/guide/installation) guide. + +## Community Links + +- [Discord](https://discord.gg/dJSah9CVrC) +- [Twitter](https://twitter.com/automatischio) + +## Support + +If you have any questions or problems, please visit our GitHub issues page, and we'll try to help you as soon as possible. + +[https://github.com/automatisch/automatisch/issues](https://github.com/automatisch/automatisch/issues) + +## License + +Automatisch Community Edition (Automatisch CE) is an open-source software with the [AGPL-3.0 license](LICENSE.agpl). + +Automatisch Enterprise Edition (Automatisch EE) is a commercial offering with the [Enterprise license](LICENSE.enterprise). + +The Automatisch repository contains both AGPL-licensed and Enterprise-licensed files. We maintain a single repository to make development easier. + +All files that contain ".ee." in their name fall under the [Enterprise license](LICENSE.enterprise). All other files fall under the [AGPL-3.0 license](LICENSE.agpl). + +See the [LICENSE](LICENSE) file for more information. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..c6fb050c2a5d68236f1647cd93aee309032dfc34 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,70 @@ +version: '3.9' +services: + main: + build: + context: ./docker + dockerfile: Dockerfile.compose + entrypoint: /compose-entrypoint.sh + ports: + - '3000:3000' + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_started + environment: + - HOST=localhost + - PROTOCOL=http + - PORT=3000 + - APP_ENV=production + - REDIS_HOST=redis + - POSTGRES_HOST=postgres + - POSTGRES_DATABASE=automatisch + - POSTGRES_USERNAME=automatisch_user + - POSTGRES_PASSWORD=automatisch_password + - ENCRYPTION_KEY + - WEBHOOK_SECRET_KEY + - APP_SECRET_KEY + volumes: + - automatisch_storage:/automatisch/storage + worker: + build: + context: ./docker + dockerfile: Dockerfile.compose + entrypoint: /compose-entrypoint.sh + depends_on: + - main + environment: + - APP_ENV=production + - REDIS_HOST=redis + - POSTGRES_HOST=postgres + - POSTGRES_DATABASE=automatisch + - POSTGRES_USERNAME=automatisch_user + - POSTGRES_PASSWORD=automatisch_password + - ENCRYPTION_KEY + - WEBHOOK_SECRET_KEY + - APP_SECRET_KEY + - WORKER=true + volumes: + - automatisch_storage:/automatisch/storage + postgres: + image: 'postgres:14.5' + environment: + - POSTGRES_DB=automatisch + - POSTGRES_USER=automatisch_user + - POSTGRES_PASSWORD=automatisch_password + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}'] + interval: 10s + timeout: 5s + retries: 5 + redis: + image: 'redis:7.0.4' + volumes: + - redis_data:/data +volumes: + automatisch_storage: + postgres_data: + redis_data: diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..f4aa6577490b8440f42dbe6c592a48b1f4662539 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,25 @@ +# syntax=docker/dockerfile:1 +FROM node:18-alpine + +ENV PORT 3000 + +RUN \ + apk --no-cache add --virtual build-dependencies python3 build-base git + +WORKDIR /automatisch + +# copy the app, note .dockerignore +COPY . /automatisch + +RUN yarn + +RUN cd packages/web && yarn build + +RUN \ + rm -rf /usr/local/share/.cache/ && \ + apk del build-dependencies + +COPY ./docker/entrypoint.sh /entrypoint.sh + +EXPOSE 3000 +ENTRYPOINT ["sh", "/entrypoint.sh"] diff --git a/docker/Dockerfile.compose b/docker/Dockerfile.compose new file mode 100644 index 0000000000000000000000000000000000000000..042596f9e7ab39d26b2d0ccba1f2d94f0b0a0b0f --- /dev/null +++ b/docker/Dockerfile.compose @@ -0,0 +1,11 @@ +# syntax=docker/dockerfile:1 +FROM automatischio/automatisch:latest +WORKDIR /automatisch + +RUN apk add --no-cache openssl dos2unix + +COPY ./compose-entrypoint.sh /compose-entrypoint.sh +RUN dos2unix /compose-entrypoint.sh + +EXPOSE 3000 +ENTRYPOINT ["sh", "/compose-entrypoint.sh"] diff --git a/docker/compose-entrypoint.sh b/docker/compose-entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..c02ae9888467bbdd2499193cb2d6d167dbbbf94e --- /dev/null +++ b/docker/compose-entrypoint.sh @@ -0,0 +1,26 @@ +#!/bin/sh + +set -e + +if [ ! -f /automatisch/storage/.env ]; then + >&2 echo "Saving environment variables" + ENCRYPTION_KEY="${ENCRYPTION_KEY:-$(openssl rand -base64 36)}" + WEBHOOK_SECRET_KEY="${WEBHOOK_SECRET_KEY:-$(openssl rand -base64 36)}" + APP_SECRET_KEY="${APP_SECRET_KEY:-$(openssl rand -base64 36)}" + echo "ENCRYPTION_KEY=$ENCRYPTION_KEY" >> /automatisch/storage/.env + echo "WEBHOOK_SECRET_KEY=$WEBHOOK_SECRET_KEY" >> /automatisch/storage/.env + echo "APP_SECRET_KEY=$APP_SECRET_KEY" >> /automatisch/storage/.env +fi + +# initiate env. vars. from /automatisch/storage/.env file +export $(grep -v '^#' /automatisch/storage/.env | xargs) + +# migration for webhook secret key, will be removed in the future. +if [[ -z "${WEBHOOK_SECRET_KEY}" ]]; then + WEBHOOK_SECRET_KEY="$(openssl rand -base64 36)" + echo "WEBHOOK_SECRET_KEY=$WEBHOOK_SECRET_KEY" >> /automatisch/storage/.env +fi + +echo "Environment variables have been set!" + +sh /entrypoint.sh diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000000000000000000000000000000000000..322a468da5dabc439e50fcc4e59f9422491e02b4 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh + +set -e + +cd packages/backend + +if [ -n "$WORKER" ]; then + yarn start:worker +else + yarn db:migrate + yarn db:seed:user + yarn start +fi diff --git a/lerna.json b/lerna.json new file mode 100644 index 0000000000000000000000000000000000000000..d4c10da7445de09d22dd81fe4f7ebdba763d125c --- /dev/null +++ b/lerna.json @@ -0,0 +1,13 @@ +{ + "packages": [ + "packages/*" + ], + "version": "0.10.0", + "npmClient": "yarn", + "useWorkspaces": true, + "command": { + "add": { + "exact": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e8a5eb9fe4f1977d7ea302ec4c401b2200423f6a --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "@automatisch/root", + "license": "See LICENSE file", + "private": true, + "scripts": { + "start": "lerna run --stream --parallel --scope=@*/{web,backend} dev", + "start:web": "lerna run --stream --scope=@*/web dev", + "start:backend": "lerna run --stream --scope=@*/backend dev", + "build:docs": "cd ./packages/docs && yarn install && yarn build" + }, + "workspaces": { + "packages": [ + "packages/*" + ], + "nohoist": [ + "**/babel-loader", + "**/webpack", + "**/@automatisch/web", + "**/ajv" + ] + }, + "devDependencies": { + "eslint": "^8.13.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "lerna": "^4.0.0", + "prettier": "^2.5.1" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/backend/.env-example b/packages/backend/.env-example new file mode 100644 index 0000000000000000000000000000000000000000..27ee434e6caf67ac2c6a7dd6570d4ce5170f1e0a --- /dev/null +++ b/packages/backend/.env-example @@ -0,0 +1,22 @@ +HOST=localhost +PROTOCOL=http +PORT=3000 +WEB_APP_URL=http://localhost:3001 +WEBHOOK_URL=http://localhost:3000 +APP_ENV=development +POSTGRES_DATABASE=automatisch_development +POSTGRES_PORT=5432 +POSTGRES_HOST=localhost +POSTGRES_USERNAME=automatish_development_user +POSTGRES_PASSWORD= +POSTGRES_ENABLE_SSL=false +ENCRYPTION_KEY=sample-encryption-key +WEBHOOK_SECRET_KEY=sample-webhook-key +APP_SECRET_KEY=sample-app-secret-key +REDIS_PORT=6379 +REDIS_HOST=127.0.0.1 +REDIS_USERNAME=redis_username +REDIS_PASSWORD=redis_password +REDIS_TLS=true +ENABLE_BULLMQ_DASHBOARD=false +SERVE_WEB_APP_SEPARATELY=true diff --git a/packages/backend/.env-example.test b/packages/backend/.env-example.test new file mode 100644 index 0000000000000000000000000000000000000000..9e435c04d373d83a8f0106d7db42e0d0f12385e7 --- /dev/null +++ b/packages/backend/.env-example.test @@ -0,0 +1,15 @@ +APP_ENV=test +HOST=localhost +PROTOCOL=http +PORT=3000 +LOG_LEVEL=debug +ENCRYPTION_KEY=sample_encryption_key +WEBHOOK_SECRET_KEY=sample_webhook_secret_key +APP_SECRET_KEY=sample_app_secret_key +POSTGRES_HOST=localhost +POSTGRES_DATABASE=automatisch_test +POSTGRES_PORT=5432 +POSTGRES_USERNAME=automatisch_test_user +POSTGRES_PASSWORD=automatisch_test_user_password +REDIS_HOST=localhost +AUTOMATISCH_CLOUD=true diff --git a/packages/backend/.eslintignore b/packages/backend/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..11c7540f7d2f2062805eaed17f3255ed4e3c3e0a --- /dev/null +++ b/packages/backend/.eslintignore @@ -0,0 +1,14 @@ +node_modules +dist +build +coverage +packages/docs/* +packages/e2e-tests + +.eslintrc.js +husky.config.js +jest.config.js +jest.config.base.js +lint-staged.config.js + +webpack.config.js diff --git a/packages/backend/.eslintrc.json b/packages/backend/.eslintrc.json new file mode 100644 index 0000000000000000000000000000000000000000..3731ac72aace213399d1b2d1fecd16af24ad9622 --- /dev/null +++ b/packages/backend/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "root": true, + "env": { + "node": true, + "es6": true + }, + "extends": ["eslint:recommended", "prettier"], + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + } +} diff --git a/packages/backend/README.md b/packages/backend/README.md new file mode 100644 index 0000000000000000000000000000000000000000..680345ff1bbad8808accf1d9b67225cabdddbaf3 --- /dev/null +++ b/packages/backend/README.md @@ -0,0 +1,4 @@ +# `backend` + +The open source Zapier alternative. Build workflow automation without spending +time and money. diff --git a/packages/backend/bin/database/client.js b/packages/backend/bin/database/client.js new file mode 100644 index 0000000000000000000000000000000000000000..08681b9e094d6ef7c4da50a3f0c517eb171adcf5 --- /dev/null +++ b/packages/backend/bin/database/client.js @@ -0,0 +1,9 @@ +import pg from 'pg'; + +const client = new pg.Client({ + host: 'localhost', + user: 'postgres', + port: 5432, +}); + +export default client; diff --git a/packages/backend/bin/database/convert-migrations.js b/packages/backend/bin/database/convert-migrations.js new file mode 100644 index 0000000000000000000000000000000000000000..8334edc5457e5ebfb9df77decd5cba00a05094ee --- /dev/null +++ b/packages/backend/bin/database/convert-migrations.js @@ -0,0 +1,31 @@ +import appConfig from '../../src/config/app.js'; +import logger from '../../src/helpers/logger.js'; +import '../../src/config/orm.js'; +import { client as knex } from '../../src/config/database.js'; + +export const renameMigrationsAsJsFiles = async () => { + if (!appConfig.isDev) { + return; + } + + try { + const tableExists = await knex.schema.hasTable('knex_migrations'); + + if (tableExists) { + await knex('knex_migrations') + .where('name', 'like', '%.ts') + .update({ + name: knex.raw("REPLACE(name, '.ts', '.js')"), + }); + logger.info( + `Migration file names with typescript renamed as JS file names!` + ); + } + } catch (err) { + logger.error(err.message); + } + + await knex.destroy(); +}; + +renameMigrationsAsJsFiles(); diff --git a/packages/backend/bin/database/create.js b/packages/backend/bin/database/create.js new file mode 100644 index 0000000000000000000000000000000000000000..572db5f50e5d76e999c170ef1506fad64a38db61 --- /dev/null +++ b/packages/backend/bin/database/create.js @@ -0,0 +1,3 @@ +import { createDatabaseAndUser } from './utils.js'; + +createDatabaseAndUser(); diff --git a/packages/backend/bin/database/drop.js b/packages/backend/bin/database/drop.js new file mode 100644 index 0000000000000000000000000000000000000000..15b97022cb8060150d2cd27fae1f67eae788c38c --- /dev/null +++ b/packages/backend/bin/database/drop.js @@ -0,0 +1,3 @@ +import { dropDatabase } from './utils.js'; + +dropDatabase(); diff --git a/packages/backend/bin/database/seed-user.js b/packages/backend/bin/database/seed-user.js new file mode 100644 index 0000000000000000000000000000000000000000..25f9bf7f78b853e88f27a2c55b24f3a55a1356e8 --- /dev/null +++ b/packages/backend/bin/database/seed-user.js @@ -0,0 +1,3 @@ +import { createUser } from './utils.js'; + +createUser(); diff --git a/packages/backend/bin/database/utils.js b/packages/backend/bin/database/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..0a3ae1294de2fd631d3ddcd687300cf7c4e3c602 --- /dev/null +++ b/packages/backend/bin/database/utils.js @@ -0,0 +1,145 @@ +import appConfig from '../../src/config/app.js'; +import logger from '../../src/helpers/logger.js'; +import client from './client.js'; +import User from '../../src/models/user.js'; +import Config from '../../src/models/config.js'; +import Role from '../../src/models/role.js'; +import '../../src/config/orm.js'; +import process from 'process'; + +async function fetchAdminRole() { + const role = await Role.query() + .where({ + key: 'admin', + }) + .limit(1) + .first(); + + return role; +} + +export async function createUser( + email = 'user@automatisch.io', + password = 'sample' +) { + if (appConfig.disableSeedUser) { + logger.info('Seed user is disabled.'); + + process.exit(0); + + return; + } + + const UNIQUE_VIOLATION_CODE = '23505'; + + const role = await fetchAdminRole(); + const userParams = { + email, + password, + fullName: 'Initial admin', + roleId: role.id, + }; + + try { + const userCount = await User.query().resultSize(); + + if (userCount === 0) { + const user = await User.query().insertAndFetch(userParams); + logger.info(`User has been saved: ${user.email}`); + + await Config.markInstallationCompleted(); + } else { + logger.info('No need to seed a user.'); + } + } catch (err) { + if (err.nativeError.code !== UNIQUE_VIOLATION_CODE) { + throw err; + } + + logger.info(`User already exists: ${email}`); + } + + process.exit(0); +} + +export const createDatabaseAndUser = async ( + database = appConfig.postgresDatabase, + user = appConfig.postgresUsername +) => { + await client.connect(); + await createDatabase(database); + await createDatabaseUser(user); + await grantPrivileges(database, user); + + await client.end(); + process.exit(0); +}; + +export const createDatabase = async (database = appConfig.postgresDatabase) => { + const DUPLICATE_DB_CODE = '42P04'; + + try { + await client.query(`CREATE DATABASE ${database}`); + logger.info(`Database: ${database} created!`); + } catch (err) { + if (err.code !== DUPLICATE_DB_CODE) { + throw err; + } + + logger.info(`Database: ${database} already exists!`); + } +}; + +export const createDatabaseUser = async (user = appConfig.postgresUsername) => { + const DUPLICATE_OBJECT_CODE = '42710'; + + try { + const result = await client.query(`CREATE USER ${user}`); + logger.info(`Database User: ${user} created!`); + + return result; + } catch (err) { + if (err.code !== DUPLICATE_OBJECT_CODE) { + throw err; + } + + logger.info(`Database User: ${user} already exists!`); + } +}; + +export const grantPrivileges = async ( + database = appConfig.postgresDatabase, + user = appConfig.postgresUsername +) => { + await client.query( + `GRANT ALL PRIVILEGES ON DATABASE ${database} TO ${user};` + ); + + logger.info(`${user} has granted all privileges on ${database}!`); +}; + +export const dropDatabase = async () => { + if (appConfig.appEnv != 'development' && appConfig.appEnv != 'test') { + const errorMessage = + 'Drop database command can be used only with development or test environments!'; + + logger.error(errorMessage); + return; + } + + await client.connect(); + await dropDatabaseAndUser(); + + await client.end(); +}; + +export const dropDatabaseAndUser = async ( + database = appConfig.postgresDatabase, + user = appConfig.postgresUsername +) => { + await client.query(`DROP DATABASE IF EXISTS ${database}`); + logger.info(`Database: ${database} removed!`); + + await client.query(`DROP USER IF EXISTS ${user}`); + logger.info(`Database User: ${user} removed!`); +}; diff --git a/packages/backend/knexfile.js b/packages/backend/knexfile.js new file mode 100644 index 0000000000000000000000000000000000000000..0748c74bdbd66fb2c080eec27af0dbfe1158620d --- /dev/null +++ b/packages/backend/knexfile.js @@ -0,0 +1,33 @@ +import { knexSnakeCaseMappers } from 'objection'; +import appConfig from './src/config/app.js'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const fileExtension = 'js'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const knexConfig = { + client: 'pg', + connection: { + host: appConfig.postgresHost, + port: appConfig.postgresPort, + user: appConfig.postgresUsername, + password: appConfig.postgresPassword, + database: appConfig.postgresDatabase, + ssl: appConfig.postgresEnableSsl, + }, + asyncStackTraces: appConfig.isDev, + searchPath: [appConfig.postgresSchema], + pool: { min: 0, max: 20 }, + migrations: { + directory: __dirname + '/src/db/migrations', + extension: fileExtension, + loadExtensions: [`.${fileExtension}`], + }, + seeds: { + directory: __dirname + '/src/db/seeds', + }, + ...(appConfig.isTest ? knexSnakeCaseMappers() : {}), +}; + +export default knexConfig; diff --git a/packages/backend/package.json b/packages/backend/package.json new file mode 100644 index 0000000000000000000000000000000000000000..50b8109f83542b18413de383f9770273d04a948b --- /dev/null +++ b/packages/backend/package.json @@ -0,0 +1,107 @@ +{ + "name": "@automatisch/backend", + "version": "0.10.0", + "license": "See LICENSE file", + "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", + "type": "module", + "scripts": { + "dev": "nodemon --watch 'src/**/*.js' --exec 'node' src/server.js", + "worker": "nodemon --watch 'src/**/*.js' --exec 'node' src/worker.js", + "start": "node src/server.js", + "start:worker": "node src/worker.js", + "pretest": "APP_ENV=test node ./test/setup/prepare-test-env.js", + "test": "APP_ENV=test vitest run", + "lint": "eslint .", + "db:create": "node ./bin/database/create.js", + "db:seed:user": "node ./bin/database/seed-user.js", + "db:drop": "node ./bin/database/drop.js", + "db:migration:create": "knex migrate:make", + "db:rollback": "knex migrate:rollback", + "db:migrate": "node ./bin/database/convert-migrations.js && knex migrate:latest" + }, + "dependencies": { + "@bull-board/express": "^3.10.1", + "@casl/ability": "^6.5.0", + "@graphql-tools/graphql-file-loader": "^7.3.4", + "@graphql-tools/load": "^7.5.2", + "@node-saml/passport-saml": "^4.0.4", + "@rudderstack/rudder-sdk-node": "^1.1.2", + "@sentry/node": "^7.42.0", + "@sentry/tracing": "^7.42.0", + "accounting": "^0.4.1", + "ajv-formats": "^2.1.1", + "axios": "1.6.0", + "bcrypt": "^5.1.0", + "bullmq": "^3.0.0", + "cors": "^2.8.5", + "crypto-js": "^4.1.1", + "debug": "~2.6.9", + "dotenv": "^10.0.0", + "express": "~4.18.2", + "express-async-handler": "^1.2.0", + "express-basic-auth": "^1.2.1", + "express-graphql": "^0.12.0", + "fast-xml-parser": "^4.0.11", + "graphql-middleware": "^6.1.15", + "graphql-shield": "^7.5.0", + "graphql-tools": "^8.2.0", + "handlebars": "^4.7.7", + "http-errors": "~1.6.3", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "jsonwebtoken": "^9.0.0", + "knex": "^2.4.0", + "libphonenumber-js": "^1.10.48", + "lodash.get": "^4.4.2", + "luxon": "2.5.2", + "memory-cache": "^0.2.0", + "morgan": "^1.10.0", + "multer": "1.4.5-lts.1", + "node-html-markdown": "^1.3.0", + "nodemailer": "6.7.0", + "oauth-1.0a": "^2.2.6", + "objection": "^3.0.0", + "passport": "^0.6.0", + "pg": "^8.7.1", + "php-serialize": "^4.0.2", + "pluralize": "^8.0.0", + "raw-body": "^2.5.2", + "showdown": "^2.1.0", + "uuid": "^9.0.1", + "winston": "^3.7.1", + "xmlrpc": "^1.3.2" + }, + "contributors": [ + { + "name": "automatisch contributors", + "url": "https://github.com/automatisch/automatisch/graphs/contributors" + } + ], + "homepage": "https://github.com/automatisch/automatisch#readme", + "main": "src/server", + "directories": { + "bin": "bin", + "src": "src", + "test": "__tests__" + }, + "files": [ + "bin", + "src" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/automatisch/automatisch.git" + }, + "bugs": { + "url": "https://github.com/automatisch/automatisch/issues" + }, + "devDependencies": { + "node-gyp": "^10.1.0", + "nodemon": "^2.0.13", + "supertest": "^6.3.3", + "vitest": "^1.1.3" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/backend/src/app.js b/packages/backend/src/app.js new file mode 100644 index 0000000000000000000000000000000000000000..170439bad8221cff379def88c17764f32fb27686 --- /dev/null +++ b/packages/backend/src/app.js @@ -0,0 +1,70 @@ +import createError from 'http-errors'; +import express from 'express'; +import cors from 'cors'; + +import appConfig from './config/app.js'; +import corsOptions from './config/cors-options.js'; +import morgan from './helpers/morgan.js'; +import * as Sentry from './helpers/sentry.ee.js'; +import appAssetsHandler from './helpers/app-assets-handler.js'; +import webUIHandler from './helpers/web-ui-handler.js'; +import errorHandler from './helpers/error-handler.js'; +import './config/orm.js'; +import { + createBullBoardHandler, + serverAdapter, +} from './helpers/create-bull-board-handler.js'; +import injectBullBoardHandler from './helpers/inject-bull-board-handler.js'; +import router from './routes/index.js'; +import configurePassport from './helpers/passport.js'; + +createBullBoardHandler(serverAdapter); + +const app = express(); + +Sentry.init(app); + +Sentry.attachRequestHandler(app); +Sentry.attachTracingHandler(app); + +injectBullBoardHandler(app, serverAdapter); + +appAssetsHandler(app); + +app.use(morgan); + +app.use( + express.json({ + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + req.rawBody = buf; + }, + }) +); +app.use( + express.urlencoded({ + extended: true, + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + req.rawBody = buf; + }, + }) +); +app.use(cors(corsOptions)); + +configurePassport(app); + +app.use('/', router); + +webUIHandler(app); + +// catch 404 and forward to error handler +app.use(function (req, res, next) { + next(createError(404)); +}); + +Sentry.attachErrorHandler(app); + +app.use(errorHandler); + +export default app; diff --git a/packages/backend/src/apps/airtable/actions/create-record/index.js b/packages/backend/src/apps/airtable/actions/create-record/index.js new file mode 100644 index 0000000000000000000000000000000000000000..554015bea981ee012a439ae18bb33e58187fcb07 --- /dev/null +++ b/packages/backend/src/apps/airtable/actions/create-record/index.js @@ -0,0 +1,92 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create record', + key: 'createRecord', + description: 'Creates a new record with fields that automatically populate.', + arguments: [ + { + label: 'Base', + key: 'baseId', + type: 'dropdown', + required: true, + description: 'Base in which to create the record.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBases', + }, + ], + }, + }, + { + label: 'Table', + key: 'tableId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.baseId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTables', + }, + { + name: 'parameters.baseId', + value: '{parameters.baseId}', + }, + ], + }, + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listFields', + }, + { + name: 'parameters.baseId', + value: '{parameters.baseId}', + }, + { + name: 'parameters.tableId', + value: '{parameters.tableId}', + }, + ], + }, + }, + ], + + async run($) { + const { baseId, tableId, ...rest } = $.step.parameters; + + const fields = Object.entries(rest).reduce((result, [key, value]) => { + if (Array.isArray(value)) { + result[key] = value.map((item) => item.value); + } else if (value !== '') { + result[key] = value; + } + return result; + }, {}); + + const body = { + typecast: true, + fields, + }; + + const { data } = await $.http.post(`/v0/${baseId}/${tableId}`, body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/airtable/actions/find-record/index.js b/packages/backend/src/apps/airtable/actions/find-record/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ad0f1ea74a2d470b7181864dc1ffd0c76ca1f02b --- /dev/null +++ b/packages/backend/src/apps/airtable/actions/find-record/index.js @@ -0,0 +1,174 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { URLSearchParams } from 'url'; + +export default defineAction({ + name: 'Find record', + key: 'findRecord', + description: + "Finds a record using simple field search or use Airtable's formula syntax to find a matching record.", + arguments: [ + { + label: 'Base', + key: 'baseId', + type: 'dropdown', + required: true, + description: 'Base in which to create the record.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBases', + }, + ], + }, + }, + { + label: 'Table', + key: 'tableId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.baseId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTables', + }, + { + name: 'parameters.baseId', + value: '{parameters.baseId}', + }, + ], + }, + }, + { + label: 'Search by field', + key: 'tableField', + type: 'dropdown', + required: false, + dependsOn: ['parameters.baseId', 'parameters.tableId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTableFields', + }, + { + name: 'parameters.baseId', + value: '{parameters.baseId}', + }, + { + name: 'parameters.tableId', + value: '{parameters.tableId}', + }, + ], + }, + }, + { + label: 'Search Value', + key: 'searchValue', + type: 'string', + required: false, + variables: true, + description: + 'The value of unique identifier for the record. For date values, please use the ISO format (e.g., "YYYY-MM-DD").', + }, + { + label: 'Search for exact match?', + key: 'exactMatch', + type: 'dropdown', + required: true, + description: '', + variables: true, + options: [ + { label: 'Yes', value: 'true' }, + { label: 'No', value: 'false' }, + ], + }, + { + label: 'Search Formula', + key: 'searchFormula', + type: 'string', + required: false, + variables: true, + description: + 'Instead, you have the option to use an Airtable search formula for locating records according to sophisticated criteria and across various fields.', + }, + { + label: 'Limit to View', + key: 'limitToView', + type: 'dropdown', + required: false, + dependsOn: ['parameters.baseId', 'parameters.tableId'], + description: + 'You have the choice to restrict the search to a particular view ID if desired.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTableViews', + }, + { + name: 'parameters.baseId', + value: '{parameters.baseId}', + }, + { + name: 'parameters.tableId', + value: '{parameters.tableId}', + }, + ], + }, + }, + ], + + async run($) { + const { + baseId, + tableId, + tableField, + searchValue, + exactMatch, + searchFormula, + limitToView, + } = $.step.parameters; + + let filterByFormula; + + if (tableField && searchValue) { + filterByFormula = + exactMatch === 'true' + ? `{${tableField}} = '${searchValue}'` + : `LOWER({${tableField}}) = LOWER('${searchValue}')`; + } else { + filterByFormula = searchFormula; + } + + const body = new URLSearchParams({ + filterByFormula, + view: limitToView, + }); + + const { data } = await $.http.post( + `/v0/${baseId}/${tableId}/listRecords`, + body + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/airtable/actions/index.js b/packages/backend/src/apps/airtable/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bb3c63ce9b3a959d0a03825b9e22dbf9b511abb7 --- /dev/null +++ b/packages/backend/src/apps/airtable/actions/index.js @@ -0,0 +1,4 @@ +import createRecord from './create-record/index.js'; +import findRecord from './find-record/index.js'; + +export default [createRecord, findRecord]; diff --git a/packages/backend/src/apps/airtable/assets/favicon.svg b/packages/backend/src/apps/airtable/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..867c3b5aef65f41fac7721df1d2f975454ff9fad --- /dev/null +++ b/packages/backend/src/apps/airtable/assets/favicon.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/backend/src/apps/airtable/auth/generate-auth-url.js b/packages/backend/src/apps/airtable/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..70d5d7e351de7c25c5d6a35bfd692cf883405209 --- /dev/null +++ b/packages/backend/src/apps/airtable/auth/generate-auth-url.js @@ -0,0 +1,38 @@ +import crypto from 'crypto'; +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const state = crypto.randomBytes(100).toString('base64url'); + const codeVerifier = crypto.randomBytes(96).toString('base64url'); + const codeChallenge = crypto + .createHash('sha256') + .update(codeVerifier) + .digest('base64') + .replace(/=/g, '') + .replace(/\+/g, '-') + .replace(/\//g, '_'); + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + response_type: 'code', + scope: authScope.join(' '), + state, + code_challenge: codeChallenge, + code_challenge_method: 'S256', + }); + + const url = `https://airtable.com/oauth2/v1/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + originalCodeChallenge: codeChallenge, + originalState: state, + codeVerifier, + }); +} diff --git a/packages/backend/src/apps/airtable/auth/index.js b/packages/backend/src/apps/airtable/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6422369f56ac3ad5d2bf06ee42aa8fd80a28e5bb --- /dev/null +++ b/packages/backend/src/apps/airtable/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/airtable/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Airtable, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/airtable/auth/is-still-verified.js b/packages/backend/src/apps/airtable/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..08962895460ec3224f6b3163ca953c3cecf84114 --- /dev/null +++ b/packages/backend/src/apps/airtable/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/airtable/auth/refresh-token.js b/packages/backend/src/apps/airtable/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..6f73550096106f7d52e8c168445c0bf9e07664ec --- /dev/null +++ b/packages/backend/src/apps/airtable/auth/refresh-token.js @@ -0,0 +1,40 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const basicAuthToken = Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64'); + + const { data } = await $.http.post( + 'https://airtable.com/oauth2/v1/token', + params.toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuthToken}`, + }, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + refreshExpiresIn: data.refresh_expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/airtable/auth/verify-credentials.js b/packages/backend/src/apps/airtable/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..f2ef8115d28835af63c520a209b1254c335818f9 --- /dev/null +++ b/packages/backend/src/apps/airtable/auth/verify-credentials.js @@ -0,0 +1,56 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + if ($.auth.data.originalState !== $.auth.data.state) { + throw new Error("The 'state' parameter does not match."); + } + if ($.auth.data.originalCodeChallenge !== $.auth.data.code_challenge) { + throw new Error("The 'code challenge' parameter does not match."); + } + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const basicAuthToken = Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64'); + + const { data } = await $.http.post( + 'https://airtable.com/oauth2/v1/token', + { + code: $.auth.data.code, + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + grant_type: 'authorization_code', + code_verifier: $.auth.data.codeVerifier, + }, + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${basicAuthToken}`, + }, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + expiresIn: data.expires_in, + refreshExpiresIn: data.refresh_expires_in, + refreshToken: data.refresh_token, + screenName: currentUser.email, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/airtable/common/add-auth-header.js b/packages/backend/src/apps/airtable/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..f957ebf964e872eccff445779486e674b45614fa --- /dev/null +++ b/packages/backend/src/apps/airtable/common/add-auth-header.js @@ -0,0 +1,12 @@ +const addAuthHeader = ($, requestConfig) => { + if ( + !requestConfig.additionalProperties?.skipAddingAuthHeader && + $.auth.data?.accessToken + ) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/airtable/common/auth-scope.js b/packages/backend/src/apps/airtable/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..8b4cbca8011980f2d485020d0153f5270446e685 --- /dev/null +++ b/packages/backend/src/apps/airtable/common/auth-scope.js @@ -0,0 +1,12 @@ +const authScope = [ + 'data.records:read', + 'data.records:write', + 'data.recordComments:read', + 'data.recordComments:write', + 'schema.bases:read', + 'schema.bases:write', + 'user.email:read', + 'webhook:manage', +]; + +export default authScope; diff --git a/packages/backend/src/apps/airtable/common/get-current-user.js b/packages/backend/src/apps/airtable/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..c04f16a95899e236695a559b3b14344a534d432d --- /dev/null +++ b/packages/backend/src/apps/airtable/common/get-current-user.js @@ -0,0 +1,6 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get('/v0/meta/whoami'); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/airtable/dynamic-data/index.js b/packages/backend/src/apps/airtable/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c12f341f99a1ad5cdbdfd508a49655cd8437b95d --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-data/index.js @@ -0,0 +1,6 @@ +import listBases from './list-bases/index.js'; +import listTableFields from './list-table-fields/index.js'; +import listTableViews from './list-table-views/index.js'; +import listTables from './list-tables/index.js'; + +export default [listBases, listTableFields, listTableViews, listTables]; diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2f075694adf1be7e59c3bc5aaaa68c05af5a6a68 --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js @@ -0,0 +1,28 @@ +export default { + name: 'List bases', + key: 'listBases', + + async run($) { + const bases = { + data: [], + }; + + const params = {}; + + do { + const { data } = await $.http.get('/v0/meta/bases', { params }); + params.offset = data.offset; + + if (data?.bases) { + for (const base of data.bases) { + bases.data.push({ + value: base.id, + name: base.name, + }); + } + } + } while (params.offset); + + return bases; + }, +}; diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3f96d127d303877c01f3cea19cf9eab336ab94fb --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js @@ -0,0 +1,39 @@ +export default { + name: 'List table fields', + key: 'listTableFields', + + async run($) { + const tableFields = { + data: [], + }; + const { baseId, tableId } = $.step.parameters; + + if (!baseId) { + return tableFields; + } + + const params = {}; + + do { + const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, { + params, + }); + params.offset = data.offset; + + if (data?.tables) { + for (const table of data.tables) { + if (table.id === tableId) { + table.fields.forEach((field) => { + tableFields.data.push({ + value: field.name, + name: field.name, + }); + }); + } + } + } + } while (params.offset); + + return tableFields; + }, +}; diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d2ec912757966754e2831fe032e2908260c6be7c --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js @@ -0,0 +1,39 @@ +export default { + name: 'List table views', + key: 'listTableViews', + + async run($) { + const tableViews = { + data: [], + }; + const { baseId, tableId } = $.step.parameters; + + if (!baseId) { + return tableViews; + } + + const params = {}; + + do { + const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, { + params, + }); + params.offset = data.offset; + + if (data?.tables) { + for (const table of data.tables) { + if (table.id === tableId) { + table.views.forEach((view) => { + tableViews.data.push({ + value: view.id, + name: view.name, + }); + }); + } + } + } + } while (params.offset); + + return tableViews; + }, +}; diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js new file mode 100644 index 0000000000000000000000000000000000000000..90d6b4c0fa210cc031ca958caffa7106b3d04c25 --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js @@ -0,0 +1,35 @@ +export default { + name: 'List tables', + key: 'listTables', + + async run($) { + const tables = { + data: [], + }; + const baseId = $.step.parameters.baseId; + + if (!baseId) { + return tables; + } + + const params = {}; + + do { + const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, { + params, + }); + params.offset = data.offset; + + if (data?.tables) { + for (const table of data.tables) { + tables.data.push({ + value: table.id, + name: table.name, + }); + } + } + } while (params.offset); + + return tables; + }, +}; diff --git a/packages/backend/src/apps/airtable/dynamic-fields/index.js b/packages/backend/src/apps/airtable/dynamic-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5d97313ea0790869e9a8a1b8f6f57f021e13ba42 --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-fields/index.js @@ -0,0 +1,3 @@ +import listFields from './list-fields/index.js'; + +export default [listFields]; diff --git a/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js b/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..704d194054344d748719828049ee03ca8f9e038e --- /dev/null +++ b/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js @@ -0,0 +1,86 @@ +const hasValue = (value) => value !== null && value !== undefined; + +export default { + name: 'List fields', + key: 'listFields', + + async run($) { + const options = []; + const { baseId, tableId } = $.step.parameters; + + if (!hasValue(baseId) || !hasValue(tableId)) { + return; + } + + const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`); + + const selectedTable = data.tables.find((table) => table.id === tableId); + + if (!selectedTable) return; + + selectedTable.fields.forEach((field) => { + if (field.type === 'singleSelect') { + options.push({ + label: field.name, + key: field.name, + type: 'dropdown', + required: false, + variables: true, + options: field.options.choices.map((choice) => ({ + label: choice.name, + value: choice.id, + })), + }); + } else if (field.type === 'multipleSelects') { + options.push({ + label: field.name, + key: field.name, + type: 'dynamic', + required: false, + variables: true, + fields: [ + { + label: 'Value', + key: 'value', + type: 'dropdown', + required: false, + variables: true, + options: field.options.choices.map((choice) => ({ + label: choice.name, + value: choice.id, + })), + }, + ], + }); + } else if (field.type === 'checkbox') { + options.push({ + label: field.name, + key: field.name, + type: 'dropdown', + required: false, + variables: true, + options: [ + { + label: 'Yes', + value: 'true', + }, + { + label: 'No', + value: 'false', + }, + ], + }); + } else { + options.push({ + label: field.name, + key: field.name, + type: 'string', + required: false, + variables: true, + }); + } + }); + + return options; + }, +}; diff --git a/packages/backend/src/apps/airtable/index.js b/packages/backend/src/apps/airtable/index.js new file mode 100644 index 0000000000000000000000000000000000000000..81ecaf789099944ea8980c1ce260571f665ea49f --- /dev/null +++ b/packages/backend/src/apps/airtable/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; +import dynamicFields from './dynamic-fields/index.js'; + +export default defineApp({ + name: 'Airtable', + key: 'airtable', + baseUrl: 'https://airtable.com', + apiBaseUrl: 'https://api.airtable.com', + iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/airtable/connection', + primaryColor: 'FFBF00', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, + dynamicFields, +}); diff --git a/packages/backend/src/apps/appwrite/assets/favicon.svg b/packages/backend/src/apps/appwrite/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..63bf0f237c8c6017e7a1a58ca6b96f9ac7597411 --- /dev/null +++ b/packages/backend/src/apps/appwrite/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/appwrite/auth/index.js b/packages/backend/src/apps/appwrite/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dfdd374bdc90b87814f1562ee261f228341d165c --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/index.js @@ -0,0 +1,65 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'projectId', + label: 'Project ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Project ID of your Appwrite project.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API key of your Appwrite project.', + clickToCopy: false, + }, + { + key: 'instanceUrl', + label: 'Appwrite instance URL', + type: 'string', + required: false, + readOnly: false, + placeholder: '', + description: '', + clickToCopy: true, + }, + { + key: 'host', + label: 'Host Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Host name of your Appwrite project.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/appwrite/auth/is-still-verified.js b/packages/backend/src/apps/appwrite/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/appwrite/auth/verify-credentials.js b/packages/backend/src/apps/appwrite/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..3cd61698980be49eb6f4c70cb1227aa48d4218c9 --- /dev/null +++ b/packages/backend/src/apps/appwrite/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v1/users'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/appwrite/common/add-auth-header.js b/packages/backend/src/apps/appwrite/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..1bec6104c4af838ec133a7ee04a61e3fc71f13d1 --- /dev/null +++ b/packages/backend/src/apps/appwrite/common/add-auth-header.js @@ -0,0 +1,16 @@ +const addAuthHeader = ($, requestConfig) => { + requestConfig.headers['Content-Type'] = 'application/json'; + + if ($.auth.data?.apiKey && $.auth.data?.projectId) { + requestConfig.headers['X-Appwrite-Project'] = $.auth.data.projectId; + requestConfig.headers['X-Appwrite-Key'] = $.auth.data.apiKey; + } + + if ($.auth.data?.host) { + requestConfig.headers['Host'] = $.auth.data.host; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/appwrite/common/set-base-url.js b/packages/backend/src/apps/appwrite/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..35a7a957f954077cfa60a3b2d19a3b5f9f9250c6 --- /dev/null +++ b/packages/backend/src/apps/appwrite/common/set-base-url.js @@ -0,0 +1,13 @@ +const setBaseUrl = ($, requestConfig) => { + const instanceUrl = $.auth.data.instanceUrl; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } else if ($.app.apiBaseUrl) { + requestConfig.baseURL = $.app.apiBaseUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/index.js b/packages/backend/src/apps/appwrite/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45eecdb0b46fb284e746eba36dec240022e9a304 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listCollections from './list-collections/index.js'; +import listDatabases from './list-databases/index.js'; + +export default [listCollections, listDatabases]; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00a839f6077dd0bc47ae00d13273d222f0b3a546 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-collections/index.js @@ -0,0 +1,44 @@ +export default { + name: 'List collections', + key: 'listCollections', + + async run($) { + const collections = { + data: [], + }; + const databaseId = $.step.parameters.databaseId; + + if (!databaseId) { + return collections; + } + + const params = { + queries: [ + JSON.stringify({ + method: 'orderAsc', + attribute: 'name', + }), + JSON.stringify({ + method: 'limit', + values: [100], + }), + ], + }; + + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections`, + { params } + ); + + if (data?.collections) { + for (const collection of data.collections) { + collections.data.push({ + value: collection.$id, + name: collection.name, + }); + } + } + + return collections; + }, +}; diff --git a/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js new file mode 100644 index 0000000000000000000000000000000000000000..225e4dd02f4ec9f557ce6ebdb87613fdd3e3f755 --- /dev/null +++ b/packages/backend/src/apps/appwrite/dynamic-data/list-databases/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List databases', + key: 'listDatabases', + + async run($) { + const databases = { + data: [], + }; + + const params = { + queries: [ + JSON.stringify({ + method: 'orderAsc', + attribute: 'name', + }), + JSON.stringify({ + method: 'limit', + values: [100], + }), + ], + }; + + const { data } = await $.http.get('/v1/databases', { params }); + + if (data?.databases) { + for (const database of data.databases) { + databases.data.push({ + value: database.$id, + name: database.name, + }); + } + } + + return databases; + }, +}; diff --git a/packages/backend/src/apps/appwrite/index.js b/packages/backend/src/apps/appwrite/index.js new file mode 100644 index 0000000000000000000000000000000000000000..735c2491bd929f052485ec0445cdf6eb341cac6a --- /dev/null +++ b/packages/backend/src/apps/appwrite/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Appwrite', + key: 'appwrite', + baseUrl: 'https://appwrite.io', + apiBaseUrl: 'https://cloud.appwrite.io', + iconUrl: '{BASE_URL}/apps/appwrite/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/appwrite/connection', + primaryColor: 'FD366E', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/appwrite/triggers/index.js b/packages/backend/src/apps/appwrite/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..30d4b6cc8699a4a71f5984179f4eaf938cc01738 --- /dev/null +++ b/packages/backend/src/apps/appwrite/triggers/index.js @@ -0,0 +1,3 @@ +import newDocuments from './new-documents/index.js'; + +export default [newDocuments]; diff --git a/packages/backend/src/apps/appwrite/triggers/new-documents/index.js b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b006862fb5c0e14d9e1e9956fd7cd536ce6cd531 --- /dev/null +++ b/packages/backend/src/apps/appwrite/triggers/new-documents/index.js @@ -0,0 +1,104 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New documents', + key: 'newDocuments', + pollInterval: 15, + description: 'Triggers when a new document is created.', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + { + label: 'Collection', + key: 'collectionId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.databaseId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCollections', + }, + { + name: 'parameters.databaseId', + value: '{parameters.databaseId}', + }, + ], + }, + }, + ], + + async run($) { + const { databaseId, collectionId } = $.step.parameters; + + const limit = 1; + let lastDocumentId = undefined; + let offset = 0; + let documentCount = 0; + + do { + const params = { + queries: [ + JSON.stringify({ + method: 'orderDesc', + attribute: '$createdAt', + }), + JSON.stringify({ + method: 'limit', + values: [limit], + }), + // An invalid cursor shouldn't be sent. + lastDocumentId && + JSON.stringify({ + method: 'cursorAfter', + values: [lastDocumentId], + }), + ].filter(Boolean), + }; + + const { data } = await $.http.get( + `/v1/databases/${databaseId}/collections/${collectionId}/documents`, + { params } + ); + + const documents = data?.documents; + documentCount = documents?.length; + offset = offset + limit; + lastDocumentId = documents[documentCount - 1]?.$id; + + if (!documentCount) { + return; + } + + for (const document of documents) { + $.pushTriggerItem({ + raw: document, + meta: { + internalId: document.$id, + }, + }); + } + } while (documentCount === limit); + }, +}); diff --git a/packages/backend/src/apps/azure-openai/actions/index.js b/packages/backend/src/apps/azure-openai/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..44f6cbc09c2955ed6872ec50bf586a766776be03 --- /dev/null +++ b/packages/backend/src/apps/azure-openai/actions/index.js @@ -0,0 +1,3 @@ +import sendPrompt from './send-prompt/index.js'; + +export default [sendPrompt]; diff --git a/packages/backend/src/apps/azure-openai/actions/send-prompt/index.js b/packages/backend/src/apps/azure-openai/actions/send-prompt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..91ae307004674cae37ce44b51ea158d7fdd3366d --- /dev/null +++ b/packages/backend/src/apps/azure-openai/actions/send-prompt/index.js @@ -0,0 +1,97 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Send prompt', + key: 'sendPrompt', + description: 'Creates a completion for the provided prompt and parameters.', + arguments: [ + { + label: 'Prompt', + key: 'prompt', + type: 'string', + required: true, + variables: true, + description: 'The text to analyze.', + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use, between 0 and 2. Higher values means the model will take more risks. Try 0.9 for more creative applications, and 0 (argmax sampling) for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: + 'The maximum number of tokens to generate in the completion.', + }, + { + label: 'Stop Sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + description: + 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, + }, + ], + + async run($) { + const payload = { + model: $.step.parameters.model, + prompt: $.step.parameters.prompt, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + stop: $.step.parameters.stopSequence || null, + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + }; + + const { data } = await $.http.post( + `/deployments/${$.auth.data.deploymentId}/completions`, + payload + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/azure-openai/assets/favicon.svg b/packages/backend/src/apps/azure-openai/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..b62b84eb144e7679e9ad93882da71d38730c2ade --- /dev/null +++ b/packages/backend/src/apps/azure-openai/assets/favicon.svg @@ -0,0 +1,6 @@ + + OpenAI + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/azure-openai/auth/index.js b/packages/backend/src/apps/azure-openai/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3de895d580ed2363b6c0df2c83d598ecd3a6b8be --- /dev/null +++ b/packages/backend/src/apps/azure-openai/auth/index.js @@ -0,0 +1,58 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'yourResourceName', + label: 'Your Resource Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The name of your Azure OpenAI Resource.', + docUrl: 'https://automatisch.io/docs/azure-openai#your-resource-name', + clickToCopy: false, + }, + { + key: 'deploymentId', + label: 'Deployment ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The deployment name you chose when you deployed the model.', + docUrl: 'https://automatisch.io/docs/azure-openai#deployment-id', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Azure OpenAI API key of your account.', + docUrl: 'https://automatisch.io/docs/azure-openai#api-key', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/azure-openai/auth/is-still-verified.js b/packages/backend/src/apps/azure-openai/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..a88adf3bdc56a9241e381552141ae3080540788b --- /dev/null +++ b/packages/backend/src/apps/azure-openai/auth/is-still-verified.js @@ -0,0 +1,6 @@ +const isStillVerified = async ($) => { + await $.http.get('/fine_tuning/jobs'); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/azure-openai/auth/verify-credentials.js b/packages/backend/src/apps/azure-openai/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..3f0e9dd1d721d6d2ef8d3ac57870db2a7a98729c --- /dev/null +++ b/packages/backend/src/apps/azure-openai/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/fine_tuning/jobs'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/azure-openai/common/add-auth-header.js b/packages/backend/src/apps/azure-openai/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..9e3670630c09b38f9f33547bf190dbc33e1e04f5 --- /dev/null +++ b/packages/backend/src/apps/azure-openai/common/add-auth-header.js @@ -0,0 +1,13 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers['api-key'] = $.auth.data.apiKey; + } + + requestConfig.params = { + 'api-version': '2023-10-01-preview', + }; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/azure-openai/common/set-base-url.js b/packages/backend/src/apps/azure-openai/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..222ccf796f18d953e7f8bf37c842dd39026c21ae --- /dev/null +++ b/packages/backend/src/apps/azure-openai/common/set-base-url.js @@ -0,0 +1,11 @@ +const setBaseUrl = ($, requestConfig) => { + const yourResourceName = $.auth.data.yourResourceName; + + if (yourResourceName) { + requestConfig.baseURL = `https://${yourResourceName}.openai.azure.com/openai`; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/azure-openai/index.js b/packages/backend/src/apps/azure-openai/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e42652f6fea491f16d430d18d886c4735540a20a --- /dev/null +++ b/packages/backend/src/apps/azure-openai/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import setBaseUrl from './common/set-base-url.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Azure OpenAI', + key: 'azure-openai', + baseUrl: + 'https://azure.microsoft.com/en-us/products/ai-services/openai-service', + apiBaseUrl: '', + iconUrl: '{BASE_URL}/apps/azure-openai/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/azure-openai/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/carbone/actions/add-template/index.js b/packages/backend/src/apps/carbone/actions/add-template/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a247fffad5da1eb7c77e9c7b22e6fcb90607806b --- /dev/null +++ b/packages/backend/src/apps/carbone/actions/add-template/index.js @@ -0,0 +1,35 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Add Template', + key: 'addTemplate', + description: + 'Creates an attachment of a specified object by given parent ID.', + arguments: [ + { + label: 'Templete Data', + key: 'templateData', + type: 'string', + required: true, + variables: true, + description: 'The content of your new Template in XML/HTML format.', + }, + ], + + async run($) { + const templateData = $.step.parameters.templateData; + + const base64Data = Buffer.from(templateData).toString('base64'); + const dataURI = `data:application/xml;base64,${base64Data}`; + + const body = JSON.stringify({ template: dataURI }); + + const response = await $.http.post('/template', body, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/carbone/actions/index.js b/packages/backend/src/apps/carbone/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4c513e7af32c72130af65bce9d3bcd5a185d338d --- /dev/null +++ b/packages/backend/src/apps/carbone/actions/index.js @@ -0,0 +1,3 @@ +import addTemplate from './add-template/index.js'; + +export default [addTemplate]; diff --git a/packages/backend/src/apps/carbone/assets/favicon.svg b/packages/backend/src/apps/carbone/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..cadf2c90003436e83d8967bacf2ac499935db5cf --- /dev/null +++ b/packages/backend/src/apps/carbone/assets/favicon.svg @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/carbone/auth/index.js b/packages/backend/src/apps/carbone/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..736516eefebbf886a9a9d054045b3cfac6f3d710 --- /dev/null +++ b/packages/backend/src/apps/carbone/auth/index.js @@ -0,0 +1,33 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Carbone API key of your account.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/carbone/auth/is-still-verified.js b/packages/backend/src/apps/carbone/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/carbone/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/carbone/auth/verify-credentials.js b/packages/backend/src/apps/carbone/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..7bf000bcf3eebb04a59cc7f5b77c1bc1581b3ff3 --- /dev/null +++ b/packages/backend/src/apps/carbone/auth/verify-credentials.js @@ -0,0 +1,10 @@ +const verifyCredentials = async ($) => { + await $.http.get('/templates'); + + await $.auth.set({ + screenName: $.auth.data.screenName, + apiKey: $.auth.data.apiKey, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/carbone/common/add-auth-header.js b/packages/backend/src/apps/carbone/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..ced8898deee3e1adf313e38d2b82007f6b5cb056 --- /dev/null +++ b/packages/backend/src/apps/carbone/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiKey}`; + requestConfig.headers['carbone-version'] = '4'; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/carbone/index.js b/packages/backend/src/apps/carbone/index.js new file mode 100644 index 0000000000000000000000000000000000000000..10d743f38edff56120567352b4eba63e63042cfc --- /dev/null +++ b/packages/backend/src/apps/carbone/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Carbone', + key: 'carbone', + iconUrl: '{BASE_URL}/apps/carbone/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/carbone/connection', + supportsConnections: true, + baseUrl: 'https://carbone.io', + apiBaseUrl: 'https://api.carbone.io', + primaryColor: '6f42c1', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/datastore/actions/get-value/index.js b/packages/backend/src/apps/datastore/actions/get-value/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e574b57023d274675d987ae25d2fda401949c862 --- /dev/null +++ b/packages/backend/src/apps/datastore/actions/get-value/index.js @@ -0,0 +1,27 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Get value', + key: 'getValue', + description: 'Get value from the persistent datastore.', + arguments: [ + { + label: 'Key', + key: 'key', + type: 'string', + required: true, + description: 'The key of your value to get.', + variables: true, + }, + ], + + async run($) { + const keyValuePair = await $.datastore.get({ + key: $.step.parameters.key, + }); + + $.setActionItem({ + raw: keyValuePair, + }); + }, +}); diff --git a/packages/backend/src/apps/datastore/actions/index.js b/packages/backend/src/apps/datastore/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0d2b2212230dfdc076f526bef16f6fd12650d5bb --- /dev/null +++ b/packages/backend/src/apps/datastore/actions/index.js @@ -0,0 +1,4 @@ +import getValue from './get-value/index.js'; +import setValue from './set-value/index.js'; + +export default [getValue, setValue]; diff --git a/packages/backend/src/apps/datastore/actions/set-value/index.js b/packages/backend/src/apps/datastore/actions/set-value/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0ec9a7e15d9e31343455359ed8d6c2bf0203b0ea --- /dev/null +++ b/packages/backend/src/apps/datastore/actions/set-value/index.js @@ -0,0 +1,36 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Set value', + key: 'setValue', + description: 'Set value to the persistent datastore.', + arguments: [ + { + label: 'Key', + key: 'key', + type: 'string', + required: true, + description: 'The key of your value to set.', + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + description: 'The value to set.', + variables: true, + }, + ], + + async run($) { + const keyValuePair = await $.datastore.set({ + key: $.step.parameters.key, + value: $.step.parameters.value, + }); + + $.setActionItem({ + raw: keyValuePair, + }); + }, +}); diff --git a/packages/backend/src/apps/datastore/assets/favicon.svg b/packages/backend/src/apps/datastore/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..c45032f5b6f876429e62ede590b46196abdefffa --- /dev/null +++ b/packages/backend/src/apps/datastore/assets/favicon.svg @@ -0,0 +1,13 @@ + + + + + + datastore + + + + + + + diff --git a/packages/backend/src/apps/datastore/index.js b/packages/backend/src/apps/datastore/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4a90b389b32a77303dd79a50134532332dca35be --- /dev/null +++ b/packages/backend/src/apps/datastore/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Datastore', + key: 'datastore', + iconUrl: '{BASE_URL}/apps/datastore/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/datastore/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, +}); diff --git a/packages/backend/src/apps/deepl/actions/index.js b/packages/backend/src/apps/deepl/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f22e6e6d1cf774fe925f38ca9dd4d6619599ac10 --- /dev/null +++ b/packages/backend/src/apps/deepl/actions/index.js @@ -0,0 +1,3 @@ +import translateText from './translate-text/index.js'; + +export default [translateText]; diff --git a/packages/backend/src/apps/deepl/actions/translate-text/index.js b/packages/backend/src/apps/deepl/actions/translate-text/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7202d024bb7365aac940c06f6484dd8f3b2e0eb2 --- /dev/null +++ b/packages/backend/src/apps/deepl/actions/translate-text/index.js @@ -0,0 +1,77 @@ +import qs from 'qs'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Translate text', + key: 'translateText', + description: 'Translates text from one language to another.', + arguments: [ + { + label: 'Text', + key: 'text', + type: 'string', + required: true, + description: 'Text to be translated.', + variables: true, + }, + { + label: 'Target Language', + key: 'targetLanguage', + type: 'dropdown', + required: true, + description: 'Language to translate the text to.', + variables: true, + value: '', + options: [ + { label: 'Bulgarian', value: 'BG' }, + { label: 'Chinese (simplified)', value: 'ZH' }, + { label: 'Czech', value: 'CS' }, + { label: 'Danish', value: 'DA' }, + { label: 'Dutch', value: 'NL' }, + { label: 'English', value: 'EN' }, + { label: 'English (American)', value: 'EN-US' }, + { label: 'English (British)', value: 'EN-GB' }, + { label: 'Estonian', value: 'ET' }, + { label: 'Finnish', value: 'FI' }, + { label: 'French', value: 'FR' }, + { label: 'German', value: 'DE' }, + { label: 'Greek', value: 'EL' }, + { label: 'Hungarian', value: 'HU' }, + { label: 'Indonesian', value: 'ID' }, + { label: 'Italian', value: 'IT' }, + { label: 'Japanese', value: 'JA' }, + { label: 'Latvian', value: 'LV' }, + { label: 'Lithuanian', value: 'LT' }, + { label: 'Polish', value: 'PL' }, + { label: 'Portuguese', value: 'PT' }, + { label: 'Portuguese (Brazilian)', value: 'PT-BR' }, + { + label: + 'Portuguese (all Portuguese varieties excluding Brazilian Portuguese)', + value: 'PT-PT', + }, + { label: 'Romanian', value: 'RO' }, + { label: 'Russian', value: 'RU' }, + { label: 'Slovak', value: 'SK' }, + { label: 'Slovenian', value: 'SL' }, + { label: 'Spanish', value: 'ES' }, + { label: 'Swedish', value: 'SV' }, + { label: 'Turkish', value: 'TR' }, + { label: 'Ukrainian', value: 'UK' }, + ], + }, + ], + + async run($) { + const stringifiedBody = qs.stringify({ + text: $.step.parameters.text, + target_lang: $.step.parameters.targetLanguage, + }); + + const response = await $.http.post('/v2/translate', stringifiedBody); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/deepl/assets/favicon.svg b/packages/backend/src/apps/deepl/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..7b96b43ee8d0617a100f3b1145fcc1941583c24d --- /dev/null +++ b/packages/backend/src/apps/deepl/assets/favicon.svg @@ -0,0 +1,39 @@ + + image/svg+xml + + + + + + + background + + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/backend/src/apps/deepl/auth/index.js b/packages/backend/src/apps/deepl/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0de2ecc2e59357410e5693a33e4a98922ea6b29a --- /dev/null +++ b/packages/backend/src/apps/deepl/auth/index.js @@ -0,0 +1,33 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'authenticationKey', + label: 'Authentication Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'DeepL authentication key of your account.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/deepl/auth/is-still-verified.js b/packages/backend/src/apps/deepl/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/deepl/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/deepl/auth/verify-credentials.js b/packages/backend/src/apps/deepl/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..1310f2c2c16d3114e6266717e984cd5e58cbaad8 --- /dev/null +++ b/packages/backend/src/apps/deepl/auth/verify-credentials.js @@ -0,0 +1,9 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v2/usage'); + + await $.auth.set({ + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/deepl/common/add-auth-header.js b/packages/backend/src/apps/deepl/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..c1938e82212f118d88bbe919b0d8b60a696aac3a --- /dev/null +++ b/packages/backend/src/apps/deepl/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.authenticationKey) { + const authorizationHeader = `DeepL-Auth-Key ${$.auth.data.authenticationKey}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/deepl/index.js b/packages/backend/src/apps/deepl/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0e776e663aa1e6858a9b14b16c79c252b594be14 --- /dev/null +++ b/packages/backend/src/apps/deepl/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'DeepL', + key: 'deepl', + iconUrl: '{BASE_URL}/apps/deepl/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/deepl/connection', + supportsConnections: true, + baseUrl: 'https://deepl.com', + apiBaseUrl: 'https://api.deepl.com', + primaryColor: '0d2d45', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/delay/actions/delay-for/index.js b/packages/backend/src/apps/delay/actions/delay-for/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e50455ef4d065ba9c7a535fd2a9d69b5b786ef39 --- /dev/null +++ b/packages/backend/src/apps/delay/actions/delay-for/index.js @@ -0,0 +1,56 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Delay for', + key: 'delayFor', + description: + 'Delays the execution of the next action by a specified amount of time.', + arguments: [ + { + label: 'Delay for unit', + key: 'delayForUnit', + type: 'dropdown', + required: true, + value: null, + description: 'Delay for unit, e.g. minutes, hours, days, weeks.', + variables: true, + options: [ + { + label: 'Minutes', + value: 'minutes', + }, + { + label: 'Hours', + value: 'hours', + }, + { + label: 'Days', + value: 'days', + }, + { + label: 'Weeks', + value: 'weeks', + }, + ], + }, + { + label: 'Delay for value', + key: 'delayForValue', + type: 'string', + required: true, + description: 'Delay for value, use a number, e.g. 1, 2, 3.', + variables: true, + }, + ], + + async run($) { + const { delayForUnit, delayForValue } = $.step.parameters; + + const dataItem = { + delayForUnit, + delayForValue, + }; + + $.setActionItem({ raw: dataItem }); + }, +}); diff --git a/packages/backend/src/apps/delay/actions/delay-until/index.js b/packages/backend/src/apps/delay/actions/delay-until/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4d82b2350d96bfb000ab93bd74fad4fa0d10f4a6 --- /dev/null +++ b/packages/backend/src/apps/delay/actions/delay-until/index.js @@ -0,0 +1,28 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Delay until', + key: 'delayUntil', + description: + 'Delays the execution of the next action until a specified date.', + arguments: [ + { + label: 'Delay until (Date)', + key: 'delayUntil', + type: 'string', + required: true, + description: 'Delay until the date. E.g. 2022-12-18', + variables: true, + }, + ], + + async run($) { + const { delayUntil } = $.step.parameters; + + const dataItem = { + delayUntil, + }; + + $.setActionItem({ raw: dataItem }); + }, +}); diff --git a/packages/backend/src/apps/delay/actions/index.js b/packages/backend/src/apps/delay/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8782fa78722f508d6302197a0ee55075b39790df --- /dev/null +++ b/packages/backend/src/apps/delay/actions/index.js @@ -0,0 +1,4 @@ +import delayFor from './delay-for/index.js'; +import delayUntil from './delay-until/index.js'; + +export default [delayFor, delayUntil]; diff --git a/packages/backend/src/apps/delay/assets/favicon.svg b/packages/backend/src/apps/delay/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..af5da4d3ac1944569407c76734276dfbae3b85bc --- /dev/null +++ b/packages/backend/src/apps/delay/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/backend/src/apps/delay/index.js b/packages/backend/src/apps/delay/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c0ac205988088ec1cd819b6f1d5a23e7c436d7f7 --- /dev/null +++ b/packages/backend/src/apps/delay/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Delay', + key: 'delay', + iconUrl: '{BASE_URL}/apps/delay/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/delay/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, +}); diff --git a/packages/backend/src/apps/discord/actions/create-scheduled-event/index.js b/packages/backend/src/apps/discord/actions/create-scheduled-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..40a0611bdd455e1ca3246fd2a748c34adf264732 --- /dev/null +++ b/packages/backend/src/apps/discord/actions/create-scheduled-event/index.js @@ -0,0 +1,88 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create a scheduled event', + key: 'createScheduledEvent', + description: 'Creates a scheduled event', + arguments: [ + { + label: 'Type', + key: 'entityType', + type: 'dropdown', + required: true, + variables: true, + options: [ + { label: 'Stage channel', value: 1 }, + { label: 'Voice channel', value: 2 }, + { label: 'External', value: 3 }, + ], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listExternalScheduledEventFields', + }, + { + name: 'parameters.entityType', + value: '{parameters.entityType}', + }, + ], + }, + }, + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + variables: true, + }, + { + label: 'Image', + key: 'image', + type: 'string', + required: false, + description: + 'Image as DataURI scheme [data:image/;base64,BASE64_ENCODED__IMAGE_DATA]', + variables: true, + }, + ], + + async run($) { + const data = { + channel_id: $.step.parameters.channel_id, + name: $.step.parameters.name, + privacy_level: 2, + scheduled_start_time: $.step.parameters.scheduledStartTime, + scheduled_end_time: $.step.parameters.scheduledEndTime, + description: $.step.parameters.description, + entity_type: $.step.parameters.entityType, + image: $.step.parameters.image, + }; + + const isExternal = $.step.parameters.entityType === 3; + + if (isExternal) { + data.entity_metadata = { + location: $.step.parameters.location, + }; + + data.channel_id = null; + } + + const response = await $.http?.post( + `/guilds/${$.auth.data.guildId}/scheduled-events`, + data + ); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/discord/actions/index.js b/packages/backend/src/apps/discord/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..598e7b29371f78371868dd81fb4c04f9d8de638e --- /dev/null +++ b/packages/backend/src/apps/discord/actions/index.js @@ -0,0 +1,4 @@ +import sendMessageToChannel from './send-message-to-channel/index.js'; +import createScheduledEvent from './create-scheduled-event/index.js'; + +export default [sendMessageToChannel, createScheduledEvent]; diff --git a/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js b/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..332ed9b808dd7ab4d04a5cc90e2338d75cb5f5da --- /dev/null +++ b/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js @@ -0,0 +1,48 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send a message to channel', + key: 'sendMessageToChannel', + description: 'Sends a message to a specific channel you specify.', + arguments: [ + { + label: 'Channel', + key: 'channel', + type: 'dropdown', + required: true, + description: 'Pick a channel to send the message to.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listChannels', + }, + ], + }, + }, + { + label: 'Message text', + key: 'message', + type: 'string', + required: true, + description: 'The content of your new message.', + variables: true, + }, + ], + + async run($) { + const data = { + content: $.step.parameters.message, + }; + + const response = await $.http?.post( + `/channels/${$.step.parameters.channel}/messages`, + data + ); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/discord/assets/favicon.svg b/packages/backend/src/apps/discord/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0483a9d3134dfca966a675be08e7e572281c9f31 --- /dev/null +++ b/packages/backend/src/apps/discord/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/discord/auth/generate-auth-url.js b/packages/backend/src/apps/discord/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..60ac0b8433f19f9e5e2d9328e5a6d39a9946a3e3 --- /dev/null +++ b/packages/backend/src/apps/discord/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.consumerKey, + redirect_uri: callbackUrl, + response_type: 'code', + permissions: '2146958591', + scope: scopes.join(' '), + }); + + const url = `${$.app.apiBaseUrl}/oauth2/authorize?${searchParams.toString()}`; + + await $.auth.set({ url }); +} diff --git a/packages/backend/src/apps/discord/auth/index.js b/packages/backend/src/apps/discord/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..62579e4215c21fab52ab1b33c3a7c7005496ff17 --- /dev/null +++ b/packages/backend/src/apps/discord/auth/index.js @@ -0,0 +1,61 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/discord/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Discord OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/discord#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'Consumer Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/discord#consumer-key', + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'Consumer Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/discord#consumer-secret', + clickToCopy: false, + }, + { + key: 'botToken', + label: 'Bot token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/discord#bot-token', + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/discord/auth/is-still-verified.js b/packages/backend/src/apps/discord/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..62748909bad3818cc89320b43945ed0d26cc0f20 --- /dev/null +++ b/packages/backend/src/apps/discord/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + await getCurrentUser($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/discord/auth/verify-credentials.js b/packages/backend/src/apps/discord/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..57555c8397343f6d136aa53c77eb94313e40ee59 --- /dev/null +++ b/packages/backend/src/apps/discord/auth/verify-credentials.js @@ -0,0 +1,55 @@ +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes.js'; +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + + const params = new URLSearchParams({ + client_id: $.auth.data.consumerKey, + redirect_uri: callbackUrl, + response_type: 'code', + scope: scopes.join(' '), + client_secret: $.auth.data.consumerSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + }); + + const { data: verifiedCredentials } = await $.http.post( + '/oauth2/token', + params.toString() + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + scope: scope, + token_type: tokenType, + guild: { id: guildId, name: guildName }, + } = verifiedCredentials; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + scope, + tokenType, + }); + + const user = await getCurrentUser($); + + await $.auth.set({ + userId: user.id, + screenName: user.username, + email: user.email, + guildId, + guildName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/discord/common/add-auth-header.js b/packages/backend/src/apps/discord/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..d9f5b10a882f27cee0cbeb29f05249dd567757ab --- /dev/null +++ b/packages/backend/src/apps/discord/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + const { tokenType, botToken } = $.auth.data; + if (tokenType && botToken) { + requestConfig.headers.Authorization = `Bot ${botToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/discord/common/get-current-user.js b/packages/backend/src/apps/discord/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..57ab474f4e8b602bd7472da3cb1cfab62643c917 --- /dev/null +++ b/packages/backend/src/apps/discord/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/users/@me'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/discord/common/scopes.js b/packages/backend/src/apps/discord/common/scopes.js new file mode 100644 index 0000000000000000000000000000000000000000..c924ca835f39c773bc536bf2b9f52a5e38a26b96 --- /dev/null +++ b/packages/backend/src/apps/discord/common/scopes.js @@ -0,0 +1,3 @@ +const scopes = ['bot', 'identify']; + +export default scopes; diff --git a/packages/backend/src/apps/discord/dynamic-data/index.js b/packages/backend/src/apps/discord/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d6a7cec128958fb3086bc569cc6ddab3501740c7 --- /dev/null +++ b/packages/backend/src/apps/discord/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listChannels from './list-channels/index.js'; +import listVoiceChannels from './list-voice-channels/index.js'; + +export default [listChannels, listVoiceChannels]; diff --git a/packages/backend/src/apps/discord/dynamic-data/list-channels/index.js b/packages/backend/src/apps/discord/dynamic-data/list-channels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..52fc719ffcac091eba5ab56e3400771913f733ad --- /dev/null +++ b/packages/backend/src/apps/discord/dynamic-data/list-channels/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List channels', + key: 'listChannels', + + async run($) { + const channels = { + data: [], + error: null, + }; + + const response = await $.http.get( + `/guilds/${$.auth.data.guildId}/channels` + ); + + channels.data = response.data + .filter((channel) => { + // filter in text channels and announcement channels only + return channel.type === 0 || channel.type === 5; + }) + .map((channel) => { + return { + value: channel.id, + name: channel.name, + }; + }); + + return channels; + }, +}; diff --git a/packages/backend/src/apps/discord/dynamic-data/list-voice-channels/index.js b/packages/backend/src/apps/discord/dynamic-data/list-voice-channels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..975e5fa64102592f9db7c45dc889344607682a21 --- /dev/null +++ b/packages/backend/src/apps/discord/dynamic-data/list-voice-channels/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List voice channels', + key: 'listVoiceChannels', + + async run($) { + const channels = { + data: [], + error: null, + }; + + const response = await $.http.get( + `/guilds/${$.auth.data.guildId}/channels` + ); + + channels.data = response.data + .filter((channel) => { + // filter in voice and stage channels only + return channel.type === 2 || channel.type === 13; + }) + .map((channel) => { + return { + value: channel.id, + name: channel.name, + }; + }); + + return channels; + }, +}; diff --git a/packages/backend/src/apps/discord/dynamic-fields/index.js b/packages/backend/src/apps/discord/dynamic-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..889acb3edd608d52faa6a08873829001dccd379c --- /dev/null +++ b/packages/backend/src/apps/discord/dynamic-fields/index.js @@ -0,0 +1,3 @@ +import listExternalScheduledEventFields from './list-external-scheduled-event-fields/index.js'; + +export default [listExternalScheduledEventFields]; diff --git a/packages/backend/src/apps/discord/dynamic-fields/list-external-scheduled-event-fields/index.js b/packages/backend/src/apps/discord/dynamic-fields/list-external-scheduled-event-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dbe66bf7492ce8f1bedfdcc377ffe2058ab82564 --- /dev/null +++ b/packages/backend/src/apps/discord/dynamic-fields/list-external-scheduled-event-fields/index.js @@ -0,0 +1,87 @@ +export default { + name: 'List external scheduled event fields', + key: 'listExternalScheduledEventFields', + + async run($) { + const isExternal = $.step.parameters.entityType === 3; + + if (isExternal) { + return [ + { + label: 'Location', + key: 'location', + type: 'string', + required: true, + description: + 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL', + variables: true, + }, + { + label: 'Start-Time', + key: 'scheduledStartTime', + type: 'string', + required: true, + description: 'The time the event will start [ISO8601]', + variables: true, + }, + { + label: 'End-Time', + key: 'scheduledEndTime', + type: 'string', + required: true, + description: + 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL', + variables: true, + }, + ]; + } + + return [ + { + label: 'Channel', + key: 'channel_id', + type: 'dropdown', + required: true, + description: + 'Pick a voice or stage channel to link the event to. This will be omitted if type is EXTERNAL', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listVoiceChannels', + }, + ], + }, + }, + { + label: 'Location', + key: 'location', + type: 'string', + required: false, + description: + 'The location of the event (1-100 characters). This will be omitted if type is NOT EXTERNAL', + variables: true, + }, + { + label: 'Start-Time', + key: 'scheduledStartTime', + type: 'string', + required: true, + description: 'The time the event will start [ISO8601]', + variables: true, + }, + { + label: 'End-Time', + key: 'scheduledEndTime', + type: 'string', + required: false, + description: + 'The time the event will end [ISO8601]. This will be omitted if type is NOT EXTERNAL', + variables: true, + }, + ]; + }, +}; diff --git a/packages/backend/src/apps/discord/index.js b/packages/backend/src/apps/discord/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bc8caa1f554c2e81051591d08e452424b22aafdd --- /dev/null +++ b/packages/backend/src/apps/discord/index.js @@ -0,0 +1,24 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import dynamicData from './dynamic-data/index.js'; +import actions from './actions/index.js'; +import triggers from './triggers/index.js'; +import dynamicFields from './dynamic-fields/index.js'; + +export default defineApp({ + name: 'Discord', + key: 'discord', + iconUrl: '{BASE_URL}/apps/discord/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/discord/connection', + supportsConnections: true, + baseUrl: 'https://discord.com', + apiBaseUrl: 'https://discord.com/api', + primaryColor: '5865f2', + beforeRequest: [addAuthHeader], + auth, + dynamicData, + dynamicFields, + triggers, + actions, +}); diff --git a/packages/backend/src/apps/discord/triggers/index.js b/packages/backend/src/apps/discord/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d6d1738de67ec12cd1cae1bbef0525681e0f82c7 --- /dev/null +++ b/packages/backend/src/apps/discord/triggers/index.js @@ -0,0 +1 @@ +export default []; diff --git a/packages/backend/src/apps/disqus/assets/favicon.svg b/packages/backend/src/apps/disqus/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..66ffeb34fe4ba22b20370c51b8fe356db018d683 --- /dev/null +++ b/packages/backend/src/apps/disqus/assets/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/packages/backend/src/apps/disqus/auth/generate-auth-url.js b/packages/backend/src/apps/disqus/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..f94a5257fc626c1c18a0b4c2ee8ac5c26608ff9a --- /dev/null +++ b/packages/backend/src/apps/disqus/auth/generate-auth-url.js @@ -0,0 +1,21 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.apiKey, + scope: authScope.join(','), + response_type: 'code', + redirect_uri: redirectUri, + }); + + const url = `https://disqus.com/api/oauth/2.0/authorize/?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/disqus/auth/index.js b/packages/backend/src/apps/disqus/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fb84d805e3787ac822d2b04d5c00985db3f06953 --- /dev/null +++ b/packages/backend/src/apps/disqus/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/disqus/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Disqus, enter the URL above.', + clickToCopy: true, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'apiSecret', + label: 'API Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/disqus/auth/is-still-verified.js b/packages/backend/src/apps/disqus/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..c42b4a96e27015353dedfb2d4fa5de5321849de8 --- /dev/null +++ b/packages/backend/src/apps/disqus/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.response.username; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/disqus/auth/refresh-token.js b/packages/backend/src/apps/disqus/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..c813898ac618a00a18c63b1daeba0f9209a543f5 --- /dev/null +++ b/packages/backend/src/apps/disqus/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.apiKey, + client_secret: $.auth.data.apiSecret, + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + `https://disqus.com/api/oauth/2.0/access_token/`, + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + scope: authScope.join(','), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/disqus/auth/verify-credentials.js b/packages/backend/src/apps/disqus/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a3c3eb3de2f61a0a251cbbe37a28713be2eaab3f --- /dev/null +++ b/packages/backend/src/apps/disqus/auth/verify-credentials.js @@ -0,0 +1,34 @@ +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: $.auth.data.apiKey, + client_secret: $.auth.data.apiSecret, + redirect_uri: redirectUri, + code: $.auth.data.code, + }); + + const { data } = await $.http.post( + `https://disqus.com/api/oauth/2.0/access_token/`, + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + apiKey: $.auth.data.apiKey, + apiSecret: $.auth.data.apiSecret, + scope: $.auth.data.scope, + userId: data.user_id, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + screenName: data.username, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/disqus/common/add-auth-header.js b/packages/backend/src/apps/disqus/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..45e5c6837b92cf888bf4dfa94fcb08b23607550c --- /dev/null +++ b/packages/backend/src/apps/disqus/common/add-auth-header.js @@ -0,0 +1,15 @@ +import { URLSearchParams } from 'url'; + +const addAuthHeader = ($, requestConfig) => { + const params = new URLSearchParams({ + access_token: $.auth.data.accessToken, + api_key: $.auth.data.apiKey, + api_secret: $.auth.data.apiSecret, + }); + + requestConfig.params = params; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/disqus/common/auth-scope.js b/packages/backend/src/apps/disqus/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..97f3eb8d8bcba351c862ee20038f769e088d04cd --- /dev/null +++ b/packages/backend/src/apps/disqus/common/auth-scope.js @@ -0,0 +1,3 @@ +const authScope = ['read', 'write', 'admin', 'email']; + +export default authScope; diff --git a/packages/backend/src/apps/disqus/common/get-current-user.js b/packages/backend/src/apps/disqus/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..63b0c78a140fdc18b10a4c8d63df059372a64876 --- /dev/null +++ b/packages/backend/src/apps/disqus/common/get-current-user.js @@ -0,0 +1,10 @@ +const getCurrentUser = async ($) => { + try { + const { data: currentUser } = await $.http.get('/3.0/users/details.json'); + return currentUser; + } catch (error) { + throw new Error('You are not authenticated.'); + } +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/disqus/dynamic-data/index.js b/packages/backend/src/apps/disqus/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3198aeed5fb703545ae6b2859f876b039126e91e --- /dev/null +++ b/packages/backend/src/apps/disqus/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listForums from './list-forums/index.js'; + +export default [listForums]; diff --git a/packages/backend/src/apps/disqus/dynamic-data/list-forums/index.js b/packages/backend/src/apps/disqus/dynamic-data/list-forums/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bb4298e6b4bbfe15f2bcb6378da9554c7adc9d12 --- /dev/null +++ b/packages/backend/src/apps/disqus/dynamic-data/list-forums/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List forums', + key: 'listForums', + + async run($) { + const forums = { + data: [], + }; + + const params = { + limit: 100, + order: 'desc', + cursor: undefined, + }; + + let more; + do { + const { data } = await $.http.get('/3.0/users/listForums.json', { + params, + }); + params.cursor = data.cursor.next; + more = data.cursor.hasNext; + + if (data.response?.length) { + for (const forum of data.response) { + forums.data.push({ + value: forum.id, + name: forum.id, + }); + } + } + } while (more); + + return forums; + }, +}; diff --git a/packages/backend/src/apps/disqus/index.js b/packages/backend/src/apps/disqus/index.js new file mode 100644 index 0000000000000000000000000000000000000000..43670ac4d891cfaf61caf91526c06388b10f272b --- /dev/null +++ b/packages/backend/src/apps/disqus/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import dynamicData from './dynamic-data/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Disqus', + key: 'disqus', + baseUrl: 'https://disqus.com', + apiBaseUrl: 'https://disqus.com/api', + iconUrl: '{BASE_URL}/apps/disqus/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/disqus/connection', + primaryColor: '2E9FFF', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + dynamicData, + triggers, +}); diff --git a/packages/backend/src/apps/disqus/triggers/index.js b/packages/backend/src/apps/disqus/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f530825797dec3b53f2ceba16259de76e99ff119 --- /dev/null +++ b/packages/backend/src/apps/disqus/triggers/index.js @@ -0,0 +1,4 @@ +import newComments from './new-comments/index.js'; +import newFlaggedComments from './new-flagged-comments/index.js'; + +export default [newComments, newFlaggedComments]; diff --git a/packages/backend/src/apps/disqus/triggers/new-comments/index.js b/packages/backend/src/apps/disqus/triggers/new-comments/index.js new file mode 100644 index 0000000000000000000000000000000000000000..119c8cf9e87044c931dc67071bfb8df33e2ff10b --- /dev/null +++ b/packages/backend/src/apps/disqus/triggers/new-comments/index.js @@ -0,0 +1,92 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { URLSearchParams } from 'url'; + +export default defineTrigger({ + name: 'New comments', + key: 'newComments', + pollInterval: 15, + description: 'Triggers when a new comment is posted in a forum using Disqus.', + arguments: [ + { + label: 'Post Types', + key: 'postTypes', + type: 'dynamic', + required: false, + description: + 'Which posts should be considered for inclusion in the trigger?', + fields: [ + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'Unapproved Posts', value: 'unapproved' }, + { label: 'Approved Posts', value: 'approved' }, + { label: 'Spam Posts', value: 'spam' }, + { label: 'Deleted Posts', value: 'deleted' }, + { label: 'Flagged Posts', value: 'flagged' }, + { label: 'Highlighted Posts', value: 'highlighted' }, + ], + }, + ], + }, + { + label: 'Forum', + key: 'forumId', + type: 'dropdown', + required: true, + description: 'Select the forum where you want comments to be triggered.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listForums', + }, + ], + }, + }, + ], + + async run($) { + const forumId = $.step.parameters.forumId; + const postTypes = $.step.parameters.postTypes; + const formattedCommentTypes = postTypes + .filter((type) => type.type !== '') + .map((type) => type.type); + + const params = new URLSearchParams({ + limit: '100', + forum: forumId, + }); + + if (formattedCommentTypes.length) { + formattedCommentTypes.forEach((type) => params.append('include', type)); + } + + let more; + do { + const { data } = await $.http.get( + `/3.0/posts/list.json?${params.toString()}` + ); + params.set('cursor', data.cursor.next); + more = data.cursor.hasNext; + + if (data.response?.length) { + for (const comment of data.response) { + $.pushTriggerItem({ + raw: comment, + meta: { + internalId: comment.id, + }, + }); + } + } + } while (more); + }, +}); diff --git a/packages/backend/src/apps/disqus/triggers/new-flagged-comments/index.js b/packages/backend/src/apps/disqus/triggers/new-flagged-comments/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cd87ddeb97b24c2ce6b598a7e310cb99c57e0f4 --- /dev/null +++ b/packages/backend/src/apps/disqus/triggers/new-flagged-comments/index.js @@ -0,0 +1,60 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { URLSearchParams } from 'url'; + +export default defineTrigger({ + name: 'New flagged comments', + key: 'newFlaggedComments', + pollInterval: 15, + description: 'Triggers when a Disqus comment is marked with a flag', + arguments: [ + { + label: 'Forum', + key: 'forumId', + type: 'dropdown', + required: true, + description: 'Select the forum where you want comments to be triggered.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listForums', + }, + ], + }, + }, + ], + + async run($) { + const forumId = $.step.parameters.forumId; + const isFlaggedFilter = 5; + + const params = new URLSearchParams({ + limit: 100, + forum: forumId, + filters: [isFlaggedFilter], + }); + + let more; + do { + const { data } = await $.http.get( + `/3.0/posts/list.json?${params.toString()}` + ); + params.set('cursor', data.cursor.next); + more = data.cursor.hasNext; + + if (data.response?.length) { + for (const comment of data.response) { + $.pushTriggerItem({ + raw: comment, + meta: { + internalId: comment.id, + }, + }); + } + } + } while (more); + }, +}); diff --git a/packages/backend/src/apps/dropbox/actions/create-folder/index.js b/packages/backend/src/apps/dropbox/actions/create-folder/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8309f94db31c72b21b111ac11d4176ae700d4872 --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/create-folder/index.js @@ -0,0 +1,40 @@ +import path from 'node:path'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create folder', + key: 'createFolder', + description: + 'Create a new folder with the given parent folder and folder name', + arguments: [ + { + label: 'Folder', + key: 'parentFolder', + type: 'string', + required: true, + description: + 'Enter the parent folder path, like /TextFiles/ or /Documents/Taxes/', + variables: true, + }, + { + label: 'Folder Name', + key: 'folderName', + type: 'string', + required: true, + description: 'Enter the name for the new folder', + variables: true, + }, + ], + + async run($) { + const parentFolder = $.step.parameters.parentFolder; + const folderName = $.step.parameters.folderName; + const folderPath = path.join(parentFolder, folderName); + + const response = await $.http.post('/2/files/create_folder_v2', { + path: folderPath, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/dropbox/actions/index.js b/packages/backend/src/apps/dropbox/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c0b1917b6ead63b2e3ba197eed1e83f34d3e5814 --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/index.js @@ -0,0 +1,4 @@ +import createFolder from './create-folder/index.js'; +import renameFile from './rename-file/index.js'; + +export default [createFolder, renameFile]; diff --git a/packages/backend/src/apps/dropbox/actions/rename-file/index.js b/packages/backend/src/apps/dropbox/actions/rename-file/index.js new file mode 100644 index 0000000000000000000000000000000000000000..789b3f3a08b744529f298827b168c66f37ce5688 --- /dev/null +++ b/packages/backend/src/apps/dropbox/actions/rename-file/index.js @@ -0,0 +1,45 @@ +import path from 'node:path'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Rename file', + key: 'renameFile', + description: 'Rename a file with the given file path and new name', + arguments: [ + { + label: 'File Path', + key: 'filePath', + type: 'string', + required: true, + description: 'Write the full path to the file such as /Folder1/File.pdf', + variables: true, + }, + { + label: 'New Name', + key: 'newName', + type: 'string', + required: true, + description: + "Enter the new name for the file (without the extension, e.g., '.pdf')", + variables: true, + }, + ], + + async run($) { + const filePath = $.step.parameters.filePath; + const newName = $.step.parameters.newName; + const fileObject = path.parse(filePath); + const newPath = path.format({ + dir: fileObject.dir, + ext: fileObject.ext, + name: newName, + }); + + const response = await $.http.post('/2/files/move_v2', { + from_path: filePath, + to_path: newPath, + }); + + $.setActionItem({ raw: response.data.metadata }); + }, +}); diff --git a/packages/backend/src/apps/dropbox/assets/favicon.svg b/packages/backend/src/apps/dropbox/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..59f3862655f672491be1f279241fa273eda0f6b3 --- /dev/null +++ b/packages/backend/src/apps/dropbox/assets/favicon.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/backend/src/apps/dropbox/auth/generate-auth-url.js b/packages/backend/src/apps/dropbox/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..1aa78f6b9c4fcc57775998356f97fcacb00cbb47 --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: callbackUrl, + response_type: 'code', + scope: scopes.join(' '), + token_access_type: 'offline', + }); + + const url = `${$.app.baseUrl}/oauth2/authorize?${searchParams.toString()}`; + + await $.auth.set({ url }); +} diff --git a/packages/backend/src/apps/dropbox/auth/index.js b/packages/backend/src/apps/dropbox/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a037f9ec802925c1840aa40706120e6502aab8ca --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/dropbox/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Dropbox OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'App Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'App Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/dropbox/auth/is-still-verified.js b/packages/backend/src/apps/dropbox/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..b65bc239bb897f52869c83302eda92aa5afaff0d --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentAccount from '../common/get-current-account.js'; + +const isStillVerified = async ($) => { + const account = await getCurrentAccount($); + return !!account; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/dropbox/auth/refresh-token.js b/packages/backend/src/apps/dropbox/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..1f34cdee96364b56174d64af4a6e0f8609d91d74 --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/refresh-token.js @@ -0,0 +1,36 @@ +import { Buffer } from 'node:buffer'; + +const refreshToken = async ($) => { + const params = { + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }; + + const basicAuthToken = Buffer.from( + `${$.auth.data.clientId}:${$.auth.data.clientSecret}` + ).toString('base64'); + + const { data } = await $.http.post('oauth2/token', null, { + params, + headers: { + Authorization: `Basic ${basicAuthToken}`, + }, + additionalProperties: { + skipAddingAuthHeader: true, + }, + }); + + const { + access_token: accessToken, + expires_in: expiresIn, + token_type: tokenType, + } = data; + + await $.auth.set({ + accessToken, + expiresIn, + tokenType, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/dropbox/auth/verify-credentials.js b/packages/backend/src/apps/dropbox/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..22097a1c2cbc5492073748f1e11733690dec5b7d --- /dev/null +++ b/packages/backend/src/apps/dropbox/auth/verify-credentials.js @@ -0,0 +1,78 @@ +import getCurrentAccount from '../common/get-current-account.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const redirectUrl = oauthRedirectUrlField.value; + + const params = { + client_id: $.auth.data.clientId, + redirect_uri: redirectUrl, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + }; + + const { data: verifiedCredentials } = await $.http.post( + '/oauth2/token', + null, + { params } + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + scope: scope, + token_type: tokenType, + account_id: accountId, + team_id: teamId, + id_token: idToken, + uid, + } = verifiedCredentials; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + scope, + tokenType, + accountId, + teamId, + idToken, + uid, + }); + + const account = await getCurrentAccount($); + + await $.auth.set({ + accountId: account.account_id, + name: { + givenName: account.name.given_name, + surname: account.name.surname, + familiarName: account.name.familiar_name, + displayName: account.name.display_name, + abbreviatedName: account.name.abbreviated_name, + }, + email: account.email, + emailVerified: account.email_verified, + disabled: account.disabled, + country: account.country, + locale: account.locale, + referralLink: account.referral_link, + isPaired: account.is_paired, + accountType: { + '.tag': account.account_type['.tag'], + }, + rootInfo: { + '.tag': account.root_info['.tag'], + rootNamespaceId: account.root_info.root_namespace_id, + homeNamespaceId: account.root_info.home_namespace_id, + }, + screenName: `${account.name.display_name} - ${account.email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/dropbox/common/add-auth-header.js b/packages/backend/src/apps/dropbox/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..1030633bbc845ed5799b031dd68f4447cbf0bfdb --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/add-auth-header.js @@ -0,0 +1,14 @@ +const addAuthHeader = ($, requestConfig) => { + requestConfig.headers['Content-Type'] = 'application/json'; + + if ( + !requestConfig.additionalProperties?.skipAddingAuthHeader && + $.auth.data?.accessToken + ) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/dropbox/common/get-current-account.js b/packages/backend/src/apps/dropbox/common/get-current-account.js new file mode 100644 index 0000000000000000000000000000000000000000..26786dde3a8660f1a5f5e226f8f5c037a59460df --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/get-current-account.js @@ -0,0 +1,6 @@ +const getCurrentAccount = async ($) => { + const response = await $.http.post('/2/users/get_current_account', null); + return response.data; +}; + +export default getCurrentAccount; diff --git a/packages/backend/src/apps/dropbox/common/scopes.js b/packages/backend/src/apps/dropbox/common/scopes.js new file mode 100644 index 0000000000000000000000000000000000000000..b257d7cb1e6ecdd9fa413bb1eefd2602dff3bdc5 --- /dev/null +++ b/packages/backend/src/apps/dropbox/common/scopes.js @@ -0,0 +1,8 @@ +const scopes = [ + 'account_info.read', + 'files.metadata.read', + 'files.content.write', + 'files.content.read', +]; + +export default scopes; diff --git a/packages/backend/src/apps/dropbox/index.js b/packages/backend/src/apps/dropbox/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8ab7e94fa2ceea719d8b20a4520123f8eb45bdc4 --- /dev/null +++ b/packages/backend/src/apps/dropbox/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Dropbox', + key: 'dropbox', + iconUrl: '{BASE_URL}/apps/dropbox/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/dropbox/connection', + supportsConnections: true, + baseUrl: 'https://dropbox.com', + apiBaseUrl: 'https://api.dropboxapi.com', + primaryColor: '0061ff', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/filter/actions/continue/index.js b/packages/backend/src/apps/filter/actions/continue/index.js new file mode 100644 index 0000000000000000000000000000000000000000..57240287d3e4524f5e5c65a9555212917bddf9a2 --- /dev/null +++ b/packages/backend/src/apps/filter/actions/continue/index.js @@ -0,0 +1,88 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const isEqual = (a, b) => a === b; +const isNotEqual = (a, b) => !isEqual(a, b); +const isGreaterThan = (a, b) => Number(a) > Number(b); +const isLessThan = (a, b) => Number(a) < Number(b); +const isGreaterThanOrEqual = (a, b) => Number(a) >= Number(b); +const isLessThanOrEqual = (a, b) => Number(a) <= Number(b); +const contains = (a, b) => a.includes(b); +const doesNotContain = (a, b) => !contains(a, b); + +const shouldContinue = (orGroups) => { + let atLeastOneGroupMatches = false; + + for (const group of orGroups) { + let groupMatches = true; + + for (const condition of group.and) { + const conditionMatches = operate( + condition.operator, + condition.key, + condition.value + ); + + if (!conditionMatches) { + groupMatches = false; + + break; + } + } + + if (groupMatches) { + atLeastOneGroupMatches = true; + + break; + } + } + + return atLeastOneGroupMatches; +}; + +const operators = { + equal: isEqual, + not_equal: isNotEqual, + greater_than: isGreaterThan, + less_than: isLessThan, + greater_than_or_equal: isGreaterThanOrEqual, + less_than_or_equal: isLessThanOrEqual, + contains: contains, + not_contains: doesNotContain, +}; + +const operate = (operation, a, b) => { + return operators[operation](a, b); +}; + +export default defineAction({ + name: 'Continue if conditions match', + key: 'continueIfMatches', + description: 'Let the execution continue if the conditions match', + arguments: [], + + async run($) { + const orGroups = $.step.parameters.or; + + const matchingGroups = orGroups.reduce((groups, group) => { + const matchingConditions = group.and.filter((condition) => + operate(condition.operator, condition.key, condition.value) + ); + + if (matchingConditions.length) { + return groups.concat([{ and: matchingConditions }]); + } + + return groups; + }, []); + + if (!shouldContinue(orGroups)) { + $.execution.exit(); + } + + $.setActionItem({ + raw: { + or: matchingGroups, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/filter/actions/index.js b/packages/backend/src/apps/filter/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0390f47d18c97d0855e3e06a462b852ccfce2ffb --- /dev/null +++ b/packages/backend/src/apps/filter/actions/index.js @@ -0,0 +1,3 @@ +import continueIfMatches from './continue/index.js'; + +export default [continueIfMatches]; diff --git a/packages/backend/src/apps/filter/assets/favicon.svg b/packages/backend/src/apps/filter/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..77d3ebe22357e24a67b9ba8407a0e9bb5846b603 --- /dev/null +++ b/packages/backend/src/apps/filter/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/filter/index.js b/packages/backend/src/apps/filter/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1fa756c4c77caf78cec56eb3429ceb82565821e2 --- /dev/null +++ b/packages/backend/src/apps/filter/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Filter', + key: 'filter', + iconUrl: '{BASE_URL}/apps/filter/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/filter/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, +}); diff --git a/packages/backend/src/apps/flickr/assets/favicon.svg b/packages/backend/src/apps/flickr/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..f8499a7a99386655ecd47ac866d660411170e154 --- /dev/null +++ b/packages/backend/src/apps/flickr/assets/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/backend/src/apps/flickr/auth/generate-auth-url.js b/packages/backend/src/apps/flickr/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..4de36c075c3e1e53ab1697ff8c9c26a179343e0e --- /dev/null +++ b/packages/backend/src/apps/flickr/auth/generate-auth-url.js @@ -0,0 +1,20 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + const requestPath = '/oauth/request_token'; + const data = { oauth_callback: callbackUrl }; + + const response = await $.http.post(requestPath, data); + const responseData = Object.fromEntries(new URLSearchParams(response.data)); + + await $.auth.set({ + url: `${$.app.apiBaseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}&perms=delete`, + accessToken: responseData.oauth_token, + accessSecret: responseData.oauth_token_secret, + }); +} diff --git a/packages/backend/src/apps/flickr/auth/index.js b/packages/backend/src/apps/flickr/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f06e6db0801add545c508254c9e9c4bff0044c9d --- /dev/null +++ b/packages/backend/src/apps/flickr/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/flickr/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Flickr OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/flickr#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'Consumer Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/flickr#consumer-key', + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'Consumer Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/flickr#consumer-secret', + clickToCopy: false, + }, + ], + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/flickr/auth/is-still-verified.js b/packages/backend/src/apps/flickr/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..3c695828adb6f8f325fffb69c7a1c3d3791aaf4a --- /dev/null +++ b/packages/backend/src/apps/flickr/auth/is-still-verified.js @@ -0,0 +1,11 @@ +const isStillVerified = async ($) => { + const params = { + method: 'flickr.test.login', + format: 'json', + nojsoncallback: 1, + }; + const response = await $.http.get('/rest', { params }); + return !!response.data.user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/flickr/auth/verify-credentials.js b/packages/backend/src/apps/flickr/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..1d3905dcdf80252c5f38e76ddb1f80a6019e98d9 --- /dev/null +++ b/packages/backend/src/apps/flickr/auth/verify-credentials.js @@ -0,0 +1,21 @@ +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const response = await $.http.post( + `/oauth/access_token?oauth_verifier=${$.auth.data.oauth_verifier}&oauth_token=${$.auth.data.accessToken}`, + null + ); + + const responseData = Object.fromEntries(new URLSearchParams(response.data)); + + await $.auth.set({ + consumerKey: $.auth.data.consumerKey, + consumerSecret: $.auth.data.consumerSecret, + accessToken: responseData.oauth_token, + accessSecret: responseData.oauth_token_secret, + userId: responseData.user_nsid, + screenName: responseData.fullname, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/flickr/common/add-auth-header.js b/packages/backend/src/apps/flickr/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..01c26019d2ba0b4c06165fc079b64085a90a0aee --- /dev/null +++ b/packages/backend/src/apps/flickr/common/add-auth-header.js @@ -0,0 +1,33 @@ +import oauthClient from './oauth-client.js'; + +const addAuthHeader = ($, requestConfig) => { + const { url, method, data, params } = requestConfig; + + const token = { + key: $.auth.data?.accessToken, + secret: $.auth.data?.accessSecret, + }; + + const requestData = { + url: `${requestConfig.baseURL}${url}`, + method, + }; + + if (url === '/oauth/request_token') { + requestData.data = data; + } + + if (method === 'get') { + requestData.data = params; + } + + const authHeader = oauthClient($).toHeader( + oauthClient($).authorize(requestData, token) + ); + + requestConfig.headers.Authorization = authHeader.Authorization; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/flickr/common/oauth-client.js b/packages/backend/src/apps/flickr/common/oauth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..d89c4889d0a1f199b3ce9f88e35228b904b349f6 --- /dev/null +++ b/packages/backend/src/apps/flickr/common/oauth-client.js @@ -0,0 +1,22 @@ +import crypto from 'crypto'; +import OAuth from 'oauth-1.0a'; + +const oauthClient = ($) => { + const consumerData = { + key: $.auth.data.consumerKey, + secret: $.auth.data.consumerSecret, + }; + + return new OAuth({ + consumer: consumerData, + signature_method: 'HMAC-SHA1', + hash_function(base_string, key) { + return crypto + .createHmac('sha1', key) + .update(base_string) + .digest('base64'); + }, + }); +}; + +export default oauthClient; diff --git a/packages/backend/src/apps/flickr/dynamic-data/index.js b/packages/backend/src/apps/flickr/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cd0d4c38afd54d670750931f7feeded611e8e819 --- /dev/null +++ b/packages/backend/src/apps/flickr/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listAlbums from './list-albums/index.js'; + +export default [listAlbums]; diff --git a/packages/backend/src/apps/flickr/dynamic-data/list-albums/index.js b/packages/backend/src/apps/flickr/dynamic-data/list-albums/index.js new file mode 100644 index 0000000000000000000000000000000000000000..55f0b9cf8c4ceb199847a3ba8e69075a1d8cb1c4 --- /dev/null +++ b/packages/backend/src/apps/flickr/dynamic-data/list-albums/index.js @@ -0,0 +1,41 @@ +export default { + name: 'List albums', + key: 'listAlbums', + + async run($) { + const params = { + page: 1, + per_page: 500, + user_id: $.auth.data.userId, + method: 'flickr.photosets.getList', + format: 'json', + nojsoncallback: 1, + }; + + let response = await $.http.get('/rest', { params }); + + const aggregatedResponse = { + data: [...response.data.photosets.photoset], + }; + + while (response.data.photosets.page < response.data.photosets.pages) { + response = await $.http.get('/rest', { + params: { + ...params, + page: response.data.photosets.page, + }, + }); + + aggregatedResponse.data.push(...response.data.photosets.photoset); + } + + aggregatedResponse.data = aggregatedResponse.data.map((photoset) => { + return { + value: photoset.id, + name: photoset.title._content, + }; + }); + + return aggregatedResponse; + }, +}; diff --git a/packages/backend/src/apps/flickr/index.js b/packages/backend/src/apps/flickr/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7a079d38b8b8b7b3126c0eee3a41520ac4deeadd --- /dev/null +++ b/packages/backend/src/apps/flickr/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Flickr', + key: 'flickr', + iconUrl: '{BASE_URL}/apps/flickr/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/flickr/connection', + docUrl: 'https://automatisch.io/docs/flickr', + primaryColor: '000000', + supportsConnections: true, + baseUrl: 'https://www.flickr.com/', + apiBaseUrl: 'https://www.flickr.com/services', + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/flickr/triggers/index.js b/packages/backend/src/apps/flickr/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2c978926a1f85f5596c6f309c19e018780bbae61 --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/index.js @@ -0,0 +1,6 @@ +import newAlbums from './new-albums/index.js'; +import newFavoritePhotos from './new-favorite-photos/index.js'; +import newPhotos from './new-photos/index.js'; +import newPhotosInAlbums from './new-photos-in-album/index.js'; + +export default [newAlbums, newFavoritePhotos, newPhotos, newPhotosInAlbums]; diff --git a/packages/backend/src/apps/flickr/triggers/new-albums/index.js b/packages/backend/src/apps/flickr/triggers/new-albums/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ff7bca1200de598d751bbcf3a5311119988e0006 --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-albums/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newAlbums from './new-albums.js'; + +export default defineTrigger({ + name: 'New albums', + pollInterval: 15, + key: 'newAlbums', + description: 'Triggers when you create a new album.', + + async run($) { + await newAlbums($); + }, +}); diff --git a/packages/backend/src/apps/flickr/triggers/new-albums/new-albums.js b/packages/backend/src/apps/flickr/triggers/new-albums/new-albums.js new file mode 100644 index 0000000000000000000000000000000000000000..b4490942a0b02fecd67b873efb9a5098d8e511c3 --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-albums/new-albums.js @@ -0,0 +1,53 @@ +const extraFields = [ + 'license', + 'date_upload', + 'date_taken', + 'owner_name', + 'icon_server', + 'original_format', + 'last_update', + 'geo', + 'tags', + 'machine_tags', + 'o_dims', + 'views', + 'media', + 'path_alias', + 'url_sq', + 'url_t', + 'url_s', + 'url_m', + 'url_o', +].join(','); + +const newAlbums = async ($) => { + let page = 1; + let pages = 1; + + do { + const params = { + page, + per_page: 500, + user_id: $.auth.data.userId, + extras: extraFields, + method: 'flickr.photosets.getList', + format: 'json', + nojsoncallback: 1, + }; + const response = await $.http.get('/rest', { params }); + const photosets = response.data.photosets; + page = photosets.page + 1; + pages = photosets.pages; + + for (const photoset of photosets.photoset) { + $.pushTriggerItem({ + raw: photoset, + meta: { + internalId: photoset.id, + }, + }); + } + } while (page <= pages); +}; + +export default newAlbums; diff --git a/packages/backend/src/apps/flickr/triggers/new-favorite-photos/index.js b/packages/backend/src/apps/flickr/triggers/new-favorite-photos/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6e80d2b43477c6a7f485a2f4fab9c3209ad02c70 --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-favorite-photos/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newFavoritePhotos from './new-favorite-photos.js'; + +export default defineTrigger({ + name: 'New favorite photos', + pollInterval: 15, + key: 'newFavoritePhotos', + description: 'Triggers when you favorite a photo.', + + async run($) { + await newFavoritePhotos($); + }, +}); diff --git a/packages/backend/src/apps/flickr/triggers/new-favorite-photos/new-favorite-photos.js b/packages/backend/src/apps/flickr/triggers/new-favorite-photos/new-favorite-photos.js new file mode 100644 index 0000000000000000000000000000000000000000..5a06649adf4a3a0b077126cd98a07fac9a60f871 --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-favorite-photos/new-favorite-photos.js @@ -0,0 +1,59 @@ +const extraFields = [ + 'description', + 'license', + 'date_upload', + 'date_taken', + 'owner_name', + 'icon_server', + 'original_format', + 'last_update', + 'geo', + 'tags', + 'machine_tags', + 'o_dims', + 'views', + 'media', + 'path_alias', + 'url_sq', + 'url_t', + 'url_s', + 'url_q', + 'url_m', + 'url_n', + 'url_z', + 'url_c', + 'url_l', + 'url_o', +].join(','); + +const newPhotos = async ($) => { + let page = 1; + let pages = 1; + + do { + const params = { + page, + per_page: 500, + user_id: $.auth.data.userId, + extras: extraFields, + method: 'flickr.favorites.getList', + format: 'json', + nojsoncallback: 1, + }; + const response = await $.http.get('/rest', { params }); + const photos = response.data.photos; + page = photos.page + 1; + pages = photos.pages; + + for (const photo of photos.photo) { + $.pushTriggerItem({ + raw: photo, + meta: { + internalId: photo.date_faved, + }, + }); + } + } while (page <= pages); +}; + +export default newPhotos; diff --git a/packages/backend/src/apps/flickr/triggers/new-photos-in-album/index.js b/packages/backend/src/apps/flickr/triggers/new-photos-in-album/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d61261b7ae64a4862dd0c51b92c48a9cb13e9cbd --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-photos-in-album/index.js @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newPhotosInAlbum from './new-photos-in-album.js'; + +export default defineTrigger({ + name: 'New photos in album', + pollInterval: 15, + key: 'newPhotosInAlbum', + description: 'Triggers when you add a new photo in an album.', + arguments: [ + { + label: 'Album', + key: 'album', + type: 'dropdown', + required: true, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listAlbums', + }, + ], + }, + }, + ], + + async run($) { + await newPhotosInAlbum($); + }, +}); diff --git a/packages/backend/src/apps/flickr/triggers/new-photos-in-album/new-photos-in-album.js b/packages/backend/src/apps/flickr/triggers/new-photos-in-album/new-photos-in-album.js new file mode 100644 index 0000000000000000000000000000000000000000..aa681dd63e31982ff9fe99260416db6efbb98c5c --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-photos-in-album/new-photos-in-album.js @@ -0,0 +1,54 @@ +const extraFields = [ + 'license', + 'date_upload', + 'date_taken', + 'owner_name', + 'icon_server', + 'original_format', + 'last_update', + 'geo', + 'tags', + 'machine_tags', + 'o_dims', + 'views', + 'media', + 'path_alias', + 'url_sq', + 'url_t', + 'url_s', + 'url_m', + 'url_o', +].join(','); + +const newPhotosInAlbum = async ($) => { + let page = 1; + let pages = 1; + + do { + const params = { + page, + per_page: 11, + user_id: $.auth.data.userId, + extras: extraFields, + photoset_id: $.step.parameters.album, + method: 'flickr.photosets.getPhotos', + format: 'json', + nojsoncallback: 1, + }; + const response = await $.http.get('/rest', { params }); + const photoset = response.data.photoset; + page = photoset.page + 1; + pages = photoset.pages; + + for (const photo of photoset.photo) { + $.pushTriggerItem({ + raw: photo, + meta: { + internalId: photo.id, + }, + }); + } + } while (page <= pages); +}; + +export default newPhotosInAlbum; diff --git a/packages/backend/src/apps/flickr/triggers/new-photos/index.js b/packages/backend/src/apps/flickr/triggers/new-photos/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f88a7b97bd7b900f16e2580270145d045346f65e --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-photos/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newPhotos from './new-photos.js'; + +export default defineTrigger({ + name: 'New photos', + pollInterval: 15, + key: 'newPhotos', + description: 'Triggers when you add a new photo.', + + async run($) { + await newPhotos($); + }, +}); diff --git a/packages/backend/src/apps/flickr/triggers/new-photos/new-photos.js b/packages/backend/src/apps/flickr/triggers/new-photos/new-photos.js new file mode 100644 index 0000000000000000000000000000000000000000..feef50eeec3c5f4a2db855293b20988da23a621b --- /dev/null +++ b/packages/backend/src/apps/flickr/triggers/new-photos/new-photos.js @@ -0,0 +1,59 @@ +const extraFields = [ + 'description', + 'license', + 'date_upload', + 'date_taken', + 'owner_name', + 'icon_server', + 'original_format', + 'last_update', + 'geo', + 'tags', + 'machine_tags', + 'o_dims', + 'views', + 'media', + 'path_alias', + 'url_sq', + 'url_t', + 'url_s', + 'url_q', + 'url_m', + 'url_n', + 'url_z', + 'url_c', + 'url_l', + 'url_o', +].join(','); + +const newPhotos = async ($) => { + let page = 1; + let pages = 1; + + do { + const params = { + page, + per_page: 500, + user_id: $.auth.data.userId, + extras: extraFields, + method: 'flickr.photos.search', + format: 'json', + nojsoncallback: 1, + }; + const response = await $.http.get('/rest', { params }); + const photos = response.data.photos; + page = photos.page + 1; + pages = photos.pages; + + for (const photo of photos.photo) { + $.pushTriggerItem({ + raw: photo, + meta: { + internalId: photo.id, + }, + }); + } + } while (page <= pages); +}; + +export default newPhotos; diff --git a/packages/backend/src/apps/flowers-software/assets/favicon.svg b/packages/backend/src/apps/flowers-software/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..55b8ed608cd38e4c658bfd3b5ee2847f7a7aee3b --- /dev/null +++ b/packages/backend/src/apps/flowers-software/assets/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/flowers-software/auth/index.js b/packages/backend/src/apps/flowers-software/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..04724510d739533c9eb58c8abb2ed728abd05bfa --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/index.js @@ -0,0 +1,43 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'username', + label: 'Username', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'password', + label: 'Password', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/flowers-software/auth/is-still-verified.js b/packages/backend/src/apps/flowers-software/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..270d415ba799b443cb98782cb6af6ffd13b0f23c --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/flowers-software/auth/verify-credentials.js b/packages/backend/src/apps/flowers-software/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..33c709bae9926231c2d9186e7920b4f8f195f638 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/auth/verify-credentials.js @@ -0,0 +1,19 @@ +import getWebhooks from '../common/get-webhooks.js'; + +const verifyCredentials = async ($) => { + const response = await getWebhooks($); + const successful = Array.isArray(response.data); + + if (!successful) { + throw new Error('Failed while authorizing!'); + } + + await $.auth.set({ + screenName: $.auth.data.username, + username: $.auth.data.username, + password: $.auth.data.password, + apiKey: $.auth.data.apiKey, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/flowers-software/common/add-auth-header.js b/packages/backend/src/apps/flowers-software/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..23e64600265b1ab60f4e95cd0b0cdb890982c956 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/add-auth-header.js @@ -0,0 +1,16 @@ +const addAuthHeader = ($, requestConfig) => { + const { data } = $.auth; + + if (data?.username && data.password && data.apiKey) { + requestConfig.headers['x-api-key'] = data.apiKey; + + requestConfig.auth = { + username: data.username, + password: data.password, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/flowers-software/common/get-webhooks.js b/packages/backend/src/apps/flowers-software/common/get-webhooks.js new file mode 100644 index 0000000000000000000000000000000000000000..70c06fe36ad3f45a8053f032c6d3b02476f1a5bb --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/get-webhooks.js @@ -0,0 +1,3 @@ +export default async function getWebhooks($) { + return await $.http.get('/v2/public/api/webhooks'); +} diff --git a/packages/backend/src/apps/flowers-software/common/webhook-filters.js b/packages/backend/src/apps/flowers-software/common/webhook-filters.js new file mode 100644 index 0000000000000000000000000000000000000000..d8a22d13541708ca8534efe696389d22b4cec8d2 --- /dev/null +++ b/packages/backend/src/apps/flowers-software/common/webhook-filters.js @@ -0,0 +1,488 @@ +const webhookFilters = [ + { + label: "Contact Company Created", + value: "CONTACT_COMPANY_CREATED" + }, + { + label: "Contact Company Deleted", + value: "CONTACT_COMPANY_DELETED" + }, + { + label: "Contact Company Updated", + value: "CONTACT_COMPANY_UPDATED" + }, + { + label: "Contact Created", + value: "CONTACT_CREATED" + }, + { + label: "Contact Deleted", + value: "CONTACT_DELETED" + }, + { + label: "Contact Updated", + value: "CONTACT_UPDATED" + }, + { + label: "Customer Created", + value: "CUSTOMER_CREATED" + }, + { + label: "Customer Updated", + value: "CUSTOMER_UPDATED" + }, + { + label: "Document Deleted", + value: "DOCUMENT_DELETED" + }, + { + label: "Document Downloaded", + value: "DOCUMENT_DOWNLOADED" + }, + { + label: "Document Saved", + value: "DOCUMENT_SAVED" + }, + { + label: "Document Updated", + value: "DOCUMENT_UPDATED" + }, + { + label: "Flow Archived", + value: "FLOW_ARCHIVED" + }, + { + label: "Flow Created", + value: "FLOW_CREATED" + }, + { + label: "Flow Object Automation Action Created", + value: "FLOW_OBJECT_AUTOMATION_ACTION_CREATED" + }, + { + label: "Flow Object Automation Action Deleted", + value: "FLOW_OBJECT_AUTOMATION_ACTION_DELETED" + }, + { + label: "Flow Object Automation Created", + value: "FLOW_OBJECT_AUTOMATION_CREATED" + }, + { + label: "Flow Object Automation Deleted", + value: "FLOW_OBJECT_AUTOMATION_DELETED" + }, + { + label: "Flow Object Automation Updated", + value: "FLOW_OBJECT_AUTOMATION_UPDATED" + }, + { + label: "Flow Object Automation Webdav Created", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_CREATED" + }, + { + label: "Flow Object Automation Webdav Deleted", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_DELETED" + }, + { + label: "Flow Object Automation Webdav Updated", + value: "FLOW_OBJECT_AUTOMATION_WEBDAV_UPDATED" + }, + { + label: "Flow Object Created", + value: "FLOW_OBJECT_CREATED" + }, + { + label: "Flow Object Deleted", + value: "FLOW_OBJECT_DELETED" + }, + { + label: "Flow Object Document Added", + value: "FLOW_OBJECT_DOCUMENT_ADDED" + }, + { + label: "Flow Object Document Removed", + value: "FLOW_OBJECT_DOCUMENT_REMOVED" + }, + { + label: "Flow Object Resource Created", + value: "FLOW_OBJECT_RESOURCE_CREATED" + }, + { + label: "Flow Object Resource Deleted", + value: "FLOW_OBJECT_RESOURCE_DELETED" + }, + { + label: "Flow Object Resource Updated", + value: "FLOW_OBJECT_RESOURCE_UPDATED" + }, + { + label: "Flow Object Task Condition Created", + value: "FLOW_OBJECT_TASK_CONDITION_CREATED" + }, + { + label: "Flow Object Task Condition Deleted", + value: "FLOW_OBJECT_TASK_CONDITION_DELETED" + }, + { + label: "Flow Object Task Condition Updated", + value: "FLOW_OBJECT_TASK_CONDITION_UPDATED" + }, + { + label: "Flow Object Task Created", + value: "FLOW_OBJECT_TASK_CREATED" + }, + { + label: "Flow Object Task Deleted", + value: "FLOW_OBJECT_TASK_DELETED" + }, + { + label: "Flow Object Task Updated", + value: "FLOW_OBJECT_TASK_UPDATED" + }, + { + label: "Flow Object Updated", + value: "FLOW_OBJECT_UPDATED" + }, + { + label: "Flow Objects Connection Created", + value: "FLOW_OBJECTS_CONNECTION_CREATED" + }, + { + label: "Flow Objects Connection Deleted", + value: "FLOW_OBJECTS_CONNECTION_DELETED" + }, + { + label: "Flow Objects Connection Updated", + value: "FLOW_OBJECTS_CONNECTION_UPDATED" + }, + { + label: "Flow Objects External Connection Created", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_CREATED" + }, + { + label: "Flow Objects External Connection Deleted", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_DELETED" + }, + { + label: "Flow Objects External Connection Updated", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTION_UPDATED" + }, + { + label: "Flow Objects External Connections Group Created", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_CREATED" + }, + { + label: "Flow Objects External Connections Group Deleted", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_DELETED" + }, + { + label: "Flow Objects External Connections Group Updated", + value: "FLOW_OBJECTS_EXTERNAL_CONNECTIONS_GROUP_UPDATED" + }, + { + label: "Flow Unarchived", + value: "FLOW_UNARCHIVED" + }, + { + label: "Flow Updated", + value: "FLOW_UPDATED" + }, + { + label: "Note Created", + value: "NOTE_CREATED" + }, + { + label: "Note Deleted", + value: "NOTE_DELETED" + }, + { + label: "Note Updated", + value: "NOTE_UPDATED" + }, + { + label: "Team Created", + value: "TEAM_CREATED" + }, + { + label: "Team Deleted", + value: "TEAM_DELETED" + }, + { + label: "Team Updated", + value: "TEAM_UPDATED" + }, + { + label: "User Added To Team", + value: "USER_ADDED_TO_TEAM" + }, + { + label: "User Added To Teamleads", + value: "USER_ADDED_TO_TEAMLEADS" + }, + { + label: "User Archived", + value: "USER_ARCHIVED" + }, + { + label: "User Changed Password", + value: "USER_CHANGED_PASSWORD" + }, + { + label: "User Created", + value: "USER_CREATED" + }, + { + label: "User Forgot Password", + value: "USER_FORGOT_PASSWORD" + }, + { + label: "User Invited", + value: "USER_INVITED" + }, + { + label: "User Logged In", + value: "USER_LOGGED_IN" + }, + { + label: "User Notification Settings Changed", + value: "USER_NOTIFICATION_SETTINGS_CHANGED" + }, + { + label: "User Profile Updated", + value: "USER_PROFILE_UPDATED" + }, + { + label: "User Removed From Team", + value: "USER_REMOVED_FROM_TEAM" + }, + { + label: "User Removed From Teamleads", + value: "USER_REMOVED_FROM_TEAMLEADS" + }, + { + label: "User Unarchived", + value: "USER_UNARCHIVED" + }, + { + label: "Workflow Archived", + value: "WORKFLOW_ARCHIVED" + }, + { + label: "Workflow Completed", + value: "WORKFLOW_COMPLETED" + }, + { + label: "Workflow Created", + value: "WORKFLOW_CREATED" + }, + { + label: "Workflow Creation Failed", + value: "WORKFLOW_CREATION_FAILED" + }, + { + label: "Workflow Object Automation Api Get Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_GET_COMPLETED" + }, + { + label: "Workflow Object Automation Api Get Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_GET_FAILED" + }, + { + label: "Workflow Object Automation Api Post Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_POST_COMPLETED" + }, + { + label: "Workflow Object Automation Api Post Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_API_POST_FAILED" + }, + { + label: "Workflow Object Automation Datev Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_DATEV_COMPLETED" + }, + { + label: "Workflow Object Automation Datev Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_DATEV_FAILED" + }, + { + label: "Workflow Object Automation Email Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_EMAIL_COMPLETED" + }, + { + label: "Workflow Object Automation Email Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_EMAIL_FAILED" + }, + { + label: "Workflow Object Automation Lexoffice Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_LEXOFFICE_COMPLETED" + }, + { + label: "Workflow Object Automation Lexoffice Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_LEXOFFICE_FAILED" + }, + { + label: "Workflow Object Automation Rejected", + value: "WORKFLOW_OBJECT_AUTOMATION_REJECTED" + }, + { + label: "Workflow Object Automation Retried", + value: "WORKFLOW_OBJECT_AUTOMATION_RETRIED" + }, + { + label: "Workflow Object Automation Sevdesk Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_SEVDESK_COMPLETED" + }, + { + label: "Workflow Object Automation Sevdesk Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_SEVDESK_FAILED" + }, + { + label: "Workflow Object Automation Stamp Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_STAMP_COMPLETED" + }, + { + label: "Workflow Object Automation Stamp Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_STAMP_FAILED" + }, + { + label: "Workflow Object Automation Task Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_TASK_COMPLETED" + }, + { + label: "Workflow Object Automation Task Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_TASK_FAILED" + }, + { + label: "Workflow Object Automation Template Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_TEMPLATE_COMPLETED" + }, + { + label: "Workflow Object Automation Template Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_TEMPLATE_FAILED" + }, + { + label: "Workflow Object Automation Webdav Document Uploaded", + value: "WORKFLOW_OBJECT_AUTOMATION_WEBDAV_DOCUMENT_UPLOADED" + }, + { + label: "Workflow Object Automation Zapier Completed", + value: "WORKFLOW_OBJECT_AUTOMATION_ZAPIER_COMPLETED" + }, + { + label: "Workflow Object Automation Zapier Failed", + value: "WORKFLOW_OBJECT_AUTOMATION_ZAPIER_FAILED" + }, + { + label: "Workflow Object Combination Task Group Created", + value: "WORKFLOW_OBJECT_COMBINATION_TASK_GROUP_CREATED" + }, + { + label: "Workflow Object Combination Task Group Deleted", + value: "WORKFLOW_OBJECT_COMBINATION_TASK_GROUP_DELETED" + }, + { + label: "Workflow Object Completed Automations Finished", + value: "WORKFLOW_OBJECT_COMPLETED_AUTOMATIONS_FINISHED" + }, + { + label: "Workflow Object Completed", + value: "WORKFLOW_OBJECT_COMPLETED" + }, + { + label: "Workflow Object Created", + value: "WORKFLOW_OBJECT_CREATED" + }, + { + label: "Workflow Object Document Added", + value: "WORKFLOW_OBJECT_DOCUMENT_ADDED" + }, + { + label: "Workflow Object Document Lock Added", + value: "WORKFLOW_OBJECT_DOCUMENT_LOCK_ADDED" + }, + { + label: "Workflow Object Document Lock Deleted", + value: "WORKFLOW_OBJECT_DOCUMENT_LOCK_DELETED" + }, + { + label: "Workflow Object Document Removed", + value: "WORKFLOW_OBJECT_DOCUMENT_REMOVED" + }, + { + label: "Workflow Object Email Added", + value: "WORKFLOW_OBJECT_EMAIL_ADDED" + }, + { + label: "Workflow Object Email Removed", + value: "WORKFLOW_OBJECT_EMAIL_REMOVED" + }, + { + label: "Workflow Object External User Created", + value: "WORKFLOW_OBJECT_EXTERNAL_USER_CREATED" + }, + { + label: "Workflow Object External User Deleted", + value: "WORKFLOW_OBJECT_EXTERNAL_USER_DELETED" + }, + { + label: "Workflow Object Note Added", + value: "WORKFLOW_OBJECT_NOTE_ADDED" + }, + { + label: "Workflow Object Note Removed", + value: "WORKFLOW_OBJECT_NOTE_REMOVED" + }, + { + label: "Workflow Object Resource Created", + value: "WORKFLOW_OBJECT_RESOURCE_CREATED" + }, + { + label: "Workflow Object Snapshot Created", + value: "WORKFLOW_OBJECT_SNAPSHOT_CREATED" + }, + { + label: "Workflow Object Task Condition Created", + value: "WORKFLOW_OBJECT_TASK_CONDITION_CREATED" + }, + { + label: "Workflow Object Task Created", + value: "WORKFLOW_OBJECT_TASK_CREATED" + }, + { + label: "Workflow Object Task Deleted", + value: "WORKFLOW_OBJECT_TASK_DELETED" + }, + { + label: "Workflow Object Task Snapshot Created", + value: "WORKFLOW_OBJECT_TASK_SNAPSHOT_CREATED" + }, + { + label: "Workflow Object Task Updated", + value: "WORKFLOW_OBJECT_TASK_UPDATED" + }, + { + label: "Workflow Object Updated", + value: "WORKFLOW_OBJECT_UPDATED" + }, + { + label: "Workflow Objects Connection Created", + value: "WORKFLOW_OBJECTS_CONNECTION_CREATED" + }, + { + label: "Workflow Objects External Connection Created", + value: "WORKFLOW_OBJECTS_EXTERNAL_CONNECTION_CREATED" + }, + { + label: "Workflow Objects External Connection Group Created", + value: "WORKFLOW_OBJECTS_EXTERNAL_CONNECTION_GROUP_CREATED" + }, + { + label: "Workflow Unarchived", + value: "WORKFLOW_UNARCHIVED" + }, + { + label: "Workflow Updated", + value: "WORKFLOW_UPDATED" + } +]; + +export default webhookFilters; \ No newline at end of file diff --git a/packages/backend/src/apps/flowers-software/index.js b/packages/backend/src/apps/flowers-software/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b2665bcc9e9a5af46944282af5cebfaa7c88611e --- /dev/null +++ b/packages/backend/src/apps/flowers-software/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Flowers Software', + key: 'flowers-software', + iconUrl: '{BASE_URL}/apps/flowers-software/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/flowers-software/connection', + supportsConnections: true, + baseUrl: 'https://flowers-software.com', + apiBaseUrl: 'https://webapp.flowers-software.com/api', + primaryColor: '02AFC7', + beforeRequest: [addAuthHeader], + auth, + triggers, +}); diff --git a/packages/backend/src/apps/flowers-software/triggers/index.js b/packages/backend/src/apps/flowers-software/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0c60d46e1d9b123f3ab3ce8f78076a73f173d33b --- /dev/null +++ b/packages/backend/src/apps/flowers-software/triggers/index.js @@ -0,0 +1,3 @@ +import newActivity from './new-activity/index.js'; + +export default [newActivity]; diff --git a/packages/backend/src/apps/flowers-software/triggers/new-activity/index.js b/packages/backend/src/apps/flowers-software/triggers/new-activity/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a0c975e2f40646cbefad897c0ea9487b3bccacac --- /dev/null +++ b/packages/backend/src/apps/flowers-software/triggers/new-activity/index.js @@ -0,0 +1,63 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; +import webhookFilters from '../../common/webhook-filters.js'; + +export default defineTrigger({ + name: 'New activity', + key: 'newActivity', + type: 'webhook', + description: 'Triggers when a new activity occurs.', + arguments: [ + { + label: 'Activity type', + key: 'filters', + type: 'dropdown', + required: true, + description: 'Pick an activity type to receive events for.', + variables: false, + options: webhookFilters, + }, + ], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const payload = { + name: $.flow.id, + type: 'POST', + url: $.webhookUrl, + filters: [$.step.parameters.filters], + }; + + const { data } = await $.http.post(`/v2/public/api/webhooks`, payload); + + await $.flow.setRemoteWebhookId(data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v2/public/api/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/formatter/actions/date-time/index.js b/packages/backend/src/apps/formatter/actions/date-time/index.js new file mode 100644 index 0000000000000000000000000000000000000000..830421d7536d1ce6e3873f832dd175ef9b3ce525 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/date-time/index.js @@ -0,0 +1,47 @@ +import defineAction from '../../../../helpers/define-action.js'; +import formatDateTime from './transformers/format-date-time.js'; + +const transformers = { + formatDateTime, +}; + +export default defineAction({ + name: 'Date / Time', + key: 'date-time', + description: 'Perform date and time related transformations on your data.', + arguments: [ + { + label: 'Transform', + key: 'transform', + type: 'dropdown', + required: true, + variables: true, + options: [{ label: 'Format Date / Time', value: 'formatDateTime' }], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listTransformOptions', + }, + { + name: 'parameters.transform', + value: '{parameters.transform}', + }, + ], + }, + }, + ], + + async run($) { + const transformerName = $.step.parameters.transform; + const output = transformers[transformerName]($); + + $.setActionItem({ + raw: { + output, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/formatter/actions/date-time/transformers/format-date-time.js b/packages/backend/src/apps/formatter/actions/date-time/transformers/format-date-time.js new file mode 100644 index 0000000000000000000000000000000000000000..cc93ae762af6218d5f84cd9afd97db480b6fb2e1 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/date-time/transformers/format-date-time.js @@ -0,0 +1,35 @@ +import { DateTime } from 'luxon'; + +const formatDateTime = ($) => { + const input = $.step.parameters.input; + + const fromFormat = $.step.parameters.fromFormat; + const fromTimezone = $.step.parameters.fromTimezone; + let inputDateTime; + + if (fromFormat === 'X') { + inputDateTime = DateTime.fromSeconds(Number(input), fromFormat, { + zone: fromTimezone, + setZone: true, + }); + } else if (fromFormat === 'x') { + inputDateTime = DateTime.fromMillis(Number(input), fromFormat, { + zone: fromTimezone, + setZone: true, + }); + } else { + inputDateTime = DateTime.fromFormat(input, fromFormat, { + zone: fromTimezone, + setZone: true, + }); + } + + const toFormat = $.step.parameters.toFormat; + const toTimezone = $.step.parameters.toTimezone; + + const outputDateTime = inputDateTime.setZone(toTimezone).toFormat(toFormat); + + return outputDateTime; +}; + +export default formatDateTime; diff --git a/packages/backend/src/apps/formatter/actions/index.js b/packages/backend/src/apps/formatter/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f7f07e502b5e7b7933c62a03283a529e70ccd205 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/index.js @@ -0,0 +1,5 @@ +import text from './text/index.js'; +import numbers from './numbers/index.js'; +import dateTime from './date-time/index.js'; + +export default [text, numbers, dateTime]; diff --git a/packages/backend/src/apps/formatter/actions/numbers/index.js b/packages/backend/src/apps/formatter/actions/numbers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..94aca22eae9ddd2bbf56ad4048fa3d7356b26f13 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/numbers/index.js @@ -0,0 +1,60 @@ +import defineAction from '../../../../helpers/define-action.js'; + +import performMathOperation from './transformers/perform-math-operation.js'; +import randomNumber from './transformers/random-number.js'; +import formatNumber from './transformers/format-number.js'; +import formatPhoneNumber from './transformers/format-phone-number.js'; + +const transformers = { + performMathOperation, + randomNumber, + formatNumber, + formatPhoneNumber, +}; + +export default defineAction({ + name: 'Numbers', + key: 'numbers', + description: + 'Transform numbers to perform math operations, generate random numbers, format numbers, and much more.', + arguments: [ + { + label: 'Transform', + key: 'transform', + type: 'dropdown', + required: true, + variables: true, + options: [ + { label: 'Perform Math Operation', value: 'performMathOperation' }, + { label: 'Random Number', value: 'randomNumber' }, + { label: 'Format Number', value: 'formatNumber' }, + { label: 'Format Phone Number', value: 'formatPhoneNumber' }, + ], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listTransformOptions', + }, + { + name: 'parameters.transform', + value: '{parameters.transform}', + }, + ], + }, + }, + ], + + async run($) { + const transformerName = $.step.parameters.transform; + const output = transformers[transformerName]($); + + $.setActionItem({ + raw: { + output, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/formatter/actions/numbers/transformers/format-number.js b/packages/backend/src/apps/formatter/actions/numbers/transformers/format-number.js new file mode 100644 index 0000000000000000000000000000000000000000..783ad012bc14b0bbc599a38ded2a9d6c2a8df2dc --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/numbers/transformers/format-number.js @@ -0,0 +1,27 @@ +import accounting from 'accounting'; + +const formatNumber = ($) => { + const input = $.step.parameters.input; + const inputDecimalMark = $.step.parameters.inputDecimalMark; + const toFormat = $.step.parameters.toFormat; + + const normalizedNumber = accounting.unformat(input, inputDecimalMark); + const decimalPart = normalizedNumber.toString().split('.')[1]; + const precision = decimalPart ? decimalPart.length : 0; + + if (toFormat === '0') { + // Comma for grouping & period for decimal + return accounting.formatNumber(normalizedNumber, precision, ',', '.'); + } else if (toFormat === '1') { + // Period for grouping & comma for decimal + return accounting.formatNumber(normalizedNumber, precision, '.', ','); + } else if (toFormat === '2') { + // Space for grouping & period for decimal + return accounting.formatNumber(normalizedNumber, precision, ' ', '.'); + } else if (toFormat === '3') { + // Space for grouping & comma for decimal + return accounting.formatNumber(normalizedNumber, precision, ' ', ','); + } +}; + +export default formatNumber; diff --git a/packages/backend/src/apps/formatter/actions/numbers/transformers/format-phone-number.js b/packages/backend/src/apps/formatter/actions/numbers/transformers/format-phone-number.js new file mode 100644 index 0000000000000000000000000000000000000000..f9aca9a28c78de825d0a040b22810a7dfa874a11 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/numbers/transformers/format-phone-number.js @@ -0,0 +1,23 @@ +import parsePhoneNumber from 'libphonenumber-js'; + +const formatPhoneNumber = ($) => { + const phoneNumber = $.step.parameters.phoneNumber; + const toFormat = $.step.parameters.toFormat; + const phoneNumberCountryCode = + $.step.parameters.phoneNumberCountryCode || 'US'; + + const parsedPhoneNumber = parsePhoneNumber( + phoneNumber, + phoneNumberCountryCode + ); + + if (toFormat === 'e164') { + return parsedPhoneNumber.format('E.164'); + } else if (toFormat === 'international') { + return parsedPhoneNumber.formatInternational(); + } else if (toFormat === 'national') { + return parsedPhoneNumber.formatNational(); + } +}; + +export default formatPhoneNumber; diff --git a/packages/backend/src/apps/formatter/actions/numbers/transformers/perform-math-operation.js b/packages/backend/src/apps/formatter/actions/numbers/transformers/perform-math-operation.js new file mode 100644 index 0000000000000000000000000000000000000000..e6127b6c725e4614ce956a0e66f22ba2767de63b --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/numbers/transformers/perform-math-operation.js @@ -0,0 +1,23 @@ +import add from 'lodash/add.js'; +import divide from 'lodash/divide.js'; +import multiply from 'lodash/multiply.js'; +import subtract from 'lodash/subtract.js'; + +const mathOperation = ($) => { + const mathOperation = $.step.parameters.mathOperation; + const values = $.step.parameters.values.map((value) => Number(value.input)); + + if (mathOperation === 'add') { + return values.reduce((acc, curr) => add(acc, curr), 0); + } else if (mathOperation === 'divide') { + return values.reduce((acc, curr) => divide(acc, curr)); + } else if (mathOperation === 'makeNegative') { + return values.map((value) => -value); + } else if (mathOperation === 'multiply') { + return values.reduce((acc, curr) => multiply(acc, curr), 1); + } else if (mathOperation === 'subtract') { + return values.reduce((acc, curr) => subtract(acc, curr)); + } +}; + +export default mathOperation; diff --git a/packages/backend/src/apps/formatter/actions/numbers/transformers/random-number.js b/packages/backend/src/apps/formatter/actions/numbers/transformers/random-number.js new file mode 100644 index 0000000000000000000000000000000000000000..38848246a41d2cce4369200057a54d16b6162645 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/numbers/transformers/random-number.js @@ -0,0 +1,13 @@ +const randomNumber = ($) => { + const lowerRange = Number($.step.parameters.lowerRange); + const upperRange = Number($.step.parameters.upperRange); + const decimalPoints = Number($.step.parameters.decimalPoints) || 0; + + return Number( + (Math.random() * (upperRange - lowerRange) + lowerRange).toFixed( + decimalPoints + ) + ); +}; + +export default randomNumber; diff --git a/packages/backend/src/apps/formatter/actions/text/index.js b/packages/backend/src/apps/formatter/actions/text/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2c05928908be28d4057fa6db5869cbeef9f7659a --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/index.js @@ -0,0 +1,93 @@ +import defineAction from '../../../../helpers/define-action.js'; + +import base64ToString from './transformers/base64-to-string.js'; +import capitalize from './transformers/capitalize.js'; +import encodeUriComponent from './transformers/encode-uri-component.js'; +import extractEmailAddress from './transformers/extract-email-address.js'; +import extractNumber from './transformers/extract-number.js'; +import htmlToMarkdown from './transformers/html-to-markdown.js'; +import lowercase from './transformers/lowercase.js'; +import markdownToHtml from './transformers/markdown-to-html.js'; +import pluralize from './transformers/pluralize.js'; +import replace from './transformers/replace.js'; +import stringToBase64 from './transformers/string-to-base64.js'; +import encodeUri from './transformers/encode-uri.js'; +import trimWhitespace from './transformers/trim-whitespace.js'; +import useDefaultValue from './transformers/use-default-value.js'; + +const transformers = { + base64ToString, + capitalize, + encodeUriComponent, + extractEmailAddress, + extractNumber, + htmlToMarkdown, + lowercase, + markdownToHtml, + pluralize, + replace, + stringToBase64, + encodeUri, + trimWhitespace, + useDefaultValue, +}; + +export default defineAction({ + name: 'Text', + key: 'text', + description: + 'Transform text data to capitalize, extract emails, apply default value, and much more.', + arguments: [ + { + label: 'Transform', + key: 'transform', + type: 'dropdown', + required: true, + variables: true, + options: [ + { label: 'Base64 to String', value: 'base64ToString' }, + { label: 'Capitalize', value: 'capitalize' }, + { + label: 'Encode URI Component', + value: 'encodeUriComponent', + }, + { label: 'Convert HTML to Markdown', value: 'htmlToMarkdown' }, + { label: 'Convert Markdown to HTML', value: 'markdownToHtml' }, + { label: 'Extract Email Address', value: 'extractEmailAddress' }, + { label: 'Extract Number', value: 'extractNumber' }, + { label: 'Lowercase', value: 'lowercase' }, + { label: 'Pluralize', value: 'pluralize' }, + { label: 'Replace', value: 'replace' }, + { label: 'String to Base64', value: 'stringToBase64' }, + { label: 'Encode URI', value: 'encodeUri' }, + { label: 'Trim Whitespace', value: 'trimWhitespace' }, + { label: 'Use Default Value', value: 'useDefaultValue' }, + ], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listTransformOptions', + }, + { + name: 'parameters.transform', + value: '{parameters.transform}', + }, + ], + }, + }, + ], + + async run($) { + const transformerName = $.step.parameters.transform; + const output = transformers[transformerName]($); + + $.setActionItem({ + raw: { + output, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/base64-to-string.js b/packages/backend/src/apps/formatter/actions/text/transformers/base64-to-string.js new file mode 100644 index 0000000000000000000000000000000000000000..7e4e397fdaed346c8408ebb827070ae532c51b77 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/base64-to-string.js @@ -0,0 +1,8 @@ +const base64ToString = ($) => { + const input = $.step.parameters.input; + const decodedString = Buffer.from(input, 'base64').toString('utf8'); + + return decodedString; +}; + +export default base64ToString; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/capitalize.js b/packages/backend/src/apps/formatter/actions/text/transformers/capitalize.js new file mode 100644 index 0000000000000000000000000000000000000000..b886806d6b73ae4a23d515aac1cef9adbd6f7659 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/capitalize.js @@ -0,0 +1,10 @@ +import lodashCapitalize from 'lodash/capitalize.js'; + +const capitalize = ($) => { + const input = $.step.parameters.input; + const capitalizedInput = input.replace(/\w+/g, lodashCapitalize); + + return capitalizedInput; +}; + +export default capitalize; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri-component.js b/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri-component.js new file mode 100644 index 0000000000000000000000000000000000000000..8d211fc58a93c47c03df65d283c02abc236ad11a --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri-component.js @@ -0,0 +1,8 @@ +const encodeUriComponent = ($) => { + const input = $.step.parameters.input; + const encodedString = encodeURIComponent(input); + + return encodedString; +}; + +export default encodeUriComponent; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri.js b/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri.js new file mode 100644 index 0000000000000000000000000000000000000000..06333001c3f1a0ee60a06a0d626f77dce05ad1dc --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/encode-uri.js @@ -0,0 +1,8 @@ +const encodeUri = ($) => { + const input = $.step.parameters.input; + const encodedString = encodeURI(input); + + return encodedString; +}; + +export default encodeUri; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/extract-email-address.js b/packages/backend/src/apps/formatter/actions/text/transformers/extract-email-address.js new file mode 100644 index 0000000000000000000000000000000000000000..127c554d8a608712ba6b77532f99f89af2742712 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/extract-email-address.js @@ -0,0 +1,10 @@ +const extractEmailAddress = ($) => { + const input = $.step.parameters.input; + const emailRegexp = + /[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?/; + + const email = input.match(emailRegexp); + return email ? email[0] : ''; +}; + +export default extractEmailAddress; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/extract-number.js b/packages/backend/src/apps/formatter/actions/text/transformers/extract-number.js new file mode 100644 index 0000000000000000000000000000000000000000..70abd58fa7b1ba6a7ede1c7b1c21c190212b503c --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/extract-number.js @@ -0,0 +1,24 @@ +const extractNumber = ($) => { + const input = $.step.parameters.input; + + // Example numbers that's supported: + // 123 + // -123 + // 123456 + // -123456 + // 121,234 + // -121,234 + // 121.234 + // -121.234 + // 1,234,567.89 + // -1,234,567.89 + // 1.234.567,89 + // -1.234.567,89 + + const numberRegexp = /-?((\d{1,3})+\.?,?)+/g; + + const numbers = input.match(numberRegexp); + return numbers ? numbers[0] : ''; +}; + +export default extractNumber; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/html-to-markdown.js b/packages/backend/src/apps/formatter/actions/text/transformers/html-to-markdown.js new file mode 100644 index 0000000000000000000000000000000000000000..adda99f429a9c624b21c04b091d86b8c094b0b6b --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/html-to-markdown.js @@ -0,0 +1,10 @@ +import { NodeHtmlMarkdown } from 'node-html-markdown'; + +const htmlToMarkdown = ($) => { + const input = $.step.parameters.input; + + const markdown = NodeHtmlMarkdown.translate(input); + return markdown; +}; + +export default htmlToMarkdown; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/lowercase.js b/packages/backend/src/apps/formatter/actions/text/transformers/lowercase.js new file mode 100644 index 0000000000000000000000000000000000000000..7d712a593534b04fcc6e454b59a748a11383a452 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/lowercase.js @@ -0,0 +1,6 @@ +const lowercase = ($) => { + const input = $.step.parameters.input; + return input.toLowerCase(); +}; + +export default lowercase; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/markdown-to-html.js b/packages/backend/src/apps/formatter/actions/text/transformers/markdown-to-html.js new file mode 100644 index 0000000000000000000000000000000000000000..47bdaf367eb747ef547f967773a2d16f2d9c7d63 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/markdown-to-html.js @@ -0,0 +1,12 @@ +import showdown from 'showdown'; + +const converter = new showdown.Converter(); + +const markdownToHtml = ($) => { + const input = $.step.parameters.input; + + const html = converter.makeHtml(input); + return html; +}; + +export default markdownToHtml; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/pluralize.js b/packages/backend/src/apps/formatter/actions/text/transformers/pluralize.js new file mode 100644 index 0000000000000000000000000000000000000000..8ba219e0920952ae48c9456d18ff96f85556728b --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/pluralize.js @@ -0,0 +1,8 @@ +import pluralizeLibrary from 'pluralize'; + +const pluralize = ($) => { + const input = $.step.parameters.input; + return pluralizeLibrary(input); +}; + +export default pluralize; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/replace.js b/packages/backend/src/apps/formatter/actions/text/transformers/replace.js new file mode 100644 index 0000000000000000000000000000000000000000..4aa08b7f8e7cb1c25b8e2285a8e2df76ee3f5a2d --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/replace.js @@ -0,0 +1,10 @@ +const replace = ($) => { + const input = $.step.parameters.input; + + const find = $.step.parameters.find; + const replace = $.step.parameters.replace; + + return input.replaceAll(find, replace); +}; + +export default replace; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/string-to-base64.js b/packages/backend/src/apps/formatter/actions/text/transformers/string-to-base64.js new file mode 100644 index 0000000000000000000000000000000000000000..0b7052fee0e37efd946c1e806d4a279cd0342786 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/string-to-base64.js @@ -0,0 +1,8 @@ +const stringtoBase64 = ($) => { + const input = $.step.parameters.input; + const base64String = Buffer.from(input).toString('base64'); + + return base64String; +}; + +export default stringtoBase64; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/trim-whitespace.js b/packages/backend/src/apps/formatter/actions/text/transformers/trim-whitespace.js new file mode 100644 index 0000000000000000000000000000000000000000..1ee810231a27a5aa1002ef6e021a4567bd7dd6a7 --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/trim-whitespace.js @@ -0,0 +1,6 @@ +const trimWhitespace = ($) => { + const input = $.step.parameters.input; + return input.trim(); +}; + +export default trimWhitespace; diff --git a/packages/backend/src/apps/formatter/actions/text/transformers/use-default-value.js b/packages/backend/src/apps/formatter/actions/text/transformers/use-default-value.js new file mode 100644 index 0000000000000000000000000000000000000000..54bc61ab005f3b49664ec233a4638be3e90b3d7d --- /dev/null +++ b/packages/backend/src/apps/formatter/actions/text/transformers/use-default-value.js @@ -0,0 +1,11 @@ +const useDefaultValue = ($) => { + const input = $.step.parameters.input; + + if (input && input.trim().length > 0) { + return input; + } + + return $.step.parameters.defaultValue; +}; + +export default useDefaultValue; diff --git a/packages/backend/src/apps/formatter/assets/favicon.svg b/packages/backend/src/apps/formatter/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..858aed39134e68e7a4381675d7205df875bc2093 --- /dev/null +++ b/packages/backend/src/apps/formatter/assets/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/backend/src/apps/formatter/common/phone-number-country-codes.js b/packages/backend/src/apps/formatter/common/phone-number-country-codes.js new file mode 100644 index 0000000000000000000000000000000000000000..f4512f8f38b517b0c7af89e531d391e9f7025137 --- /dev/null +++ b/packages/backend/src/apps/formatter/common/phone-number-country-codes.js @@ -0,0 +1,249 @@ +const phoneNumberCountryCodes = [ + { label: 'Ascension Island', value: 'AC' }, + { label: 'Andorra', value: 'AD' }, + { label: 'United Arab Emirates', value: 'AE' }, + { label: 'Afghanistan', value: 'AF' }, + { label: 'Antigua & Barbuda', value: 'AG' }, + { label: 'Anguilla', value: 'AI' }, + { label: 'Albania', value: 'AL' }, + { label: 'Armenia', value: 'AM' }, + { label: 'Angola', value: 'AO' }, + { label: 'Argentina', value: 'AR' }, + { label: 'American Samoa', value: 'AS' }, + { label: 'Austria', value: 'AT' }, + { label: 'Australia', value: 'AU' }, + { label: 'Aruba', value: 'AW' }, + { label: 'Γ…land Islands', value: 'AX' }, + { label: 'Azerbaijan', value: 'AZ' }, + { label: 'Bosnia & Herzegovina', value: 'BA' }, + { label: 'Barbados', value: 'BB' }, + { label: 'Bangladesh', value: 'BD' }, + { label: 'Belgium', value: 'BE' }, + { label: 'Burkina Faso', value: 'BF' }, + { label: 'Bulgaria', value: 'BG' }, + { label: 'Bahrain', value: 'BH' }, + { label: 'Burundi', value: 'BI' }, + { label: 'Benin', value: 'BJ' }, + { label: 'St. BarthΓ©lemy', value: 'BL' }, + { label: 'Bermuda', value: 'BM' }, + { label: 'Brunei', value: 'BN' }, + { label: 'Bolivia', value: 'BO' }, + { label: 'Caribbean Netherlands', value: 'BQ' }, + { label: 'Brazil', value: 'BR' }, + { label: 'Bahamas', value: 'BS' }, + { label: 'Bhutan', value: 'BT' }, + { label: 'Botswana', value: 'BW' }, + { label: 'Belarus', value: 'BY' }, + { label: 'Belize', value: 'BZ' }, + { label: 'Canada', value: 'CA' }, + { label: 'Cocos (Keeling) Islands', value: 'CC' }, + { label: 'Congo - Kinshasa', value: 'CD' }, + { label: 'Central African Republic', value: 'CF' }, + { label: 'Congo - Brazzaville', value: 'CG' }, + { label: 'Switzerland', value: 'CH' }, + { label: 'CΓ΄te d’Ivoire', value: 'CI' }, + { label: 'Cook Islands', value: 'CK' }, + { label: 'Chile', value: 'CL' }, + { label: 'Cameroon', value: 'CM' }, + { label: 'China', value: 'CN' }, + { label: 'Colombia', value: 'CO' }, + { label: 'Costa Rica', value: 'CR' }, + { label: 'Cuba', value: 'CU' }, + { label: 'Cape Verde', value: 'CV' }, + { label: 'CuraΓ§ao', value: 'CW' }, + { label: 'Christmas Island', value: 'CX' }, + { label: 'Cyprus', value: 'CY' }, + { label: 'Czechia', value: 'CZ' }, + { label: 'Germany', value: 'DE' }, + { label: 'Djibouti', value: 'DJ' }, + { label: 'Denmark', value: 'DK' }, + { label: 'Dominica', value: 'DM' }, + { label: 'Dominican Republic', value: 'DO' }, + { label: 'Algeria', value: 'DZ' }, + { label: 'Ecuador', value: 'EC' }, + { label: 'Estonia', value: 'EE' }, + { label: 'Egypt', value: 'EG' }, + { label: 'Western Sahara', value: 'EH' }, + { label: 'Eritrea', value: 'ER' }, + { label: 'Spain', value: 'ES' }, + { label: 'Ethiopia', value: 'ET' }, + { label: 'Finland', value: 'FI' }, + { label: 'Fiji', value: 'FJ' }, + { label: 'Falkland Islands (Islas Malvinas)', value: 'FK' }, + { label: 'Micronesia', value: 'FM' }, + { label: 'Faroe Islands', value: 'FO' }, + { label: 'France', value: 'FR' }, + { label: 'Gabon', value: 'GA' }, + { label: 'United Kingdom', value: 'GB' }, + { label: 'Grenada', value: 'GD' }, + { label: 'Georgia', value: 'GE' }, + { label: 'French Guiana', value: 'GF' }, + { label: 'Guernsey', value: 'GG' }, + { label: 'Ghana', value: 'GH' }, + { label: 'Gibraltar', value: 'GI' }, + { label: 'Greenland', value: 'GL' }, + { label: 'Gambia', value: 'GM' }, + { label: 'Guinea', value: 'GN' }, + { label: 'Guadeloupe', value: 'GP' }, + { label: 'Equatorial Guinea', value: 'GQ' }, + { label: 'Greece', value: 'GR' }, + { label: 'Guatemala', value: 'GT' }, + { label: 'Guam', value: 'GU' }, + { label: 'Guinea-Bissau', value: 'GW' }, + { label: 'Guyana', value: 'GY' }, + { label: 'Hong Kong', value: 'HK' }, + { label: 'Honduras', value: 'HN' }, + { label: 'Croatia', value: 'HR' }, + { label: 'Haiti', value: 'HT' }, + { label: 'Hungary', value: 'HU' }, + { label: 'Indonesia', value: 'ID' }, + { label: 'Ireland', value: 'IE' }, + { label: 'Israel', value: 'IL' }, + { label: 'Isle of Man', value: 'IM' }, + { label: 'India', value: 'IN' }, + { label: 'British Indian Ocean Territory', value: 'IO' }, + { label: 'Iraq', value: 'IQ' }, + { label: 'Iran', value: 'IR' }, + { label: 'Iceland', value: 'IS' }, + { label: 'Italy', value: 'IT' }, + { label: 'Jersey', value: 'JE' }, + { label: 'Jamaica', value: 'JM' }, + { label: 'Jordan', value: 'JO' }, + { label: 'Japan', value: 'JP' }, + { label: 'Kenya', value: 'KE' }, + { label: 'Kyrgyzstan', value: 'KG' }, + { label: 'Cambodia', value: 'KH' }, + { label: 'Kiribati', value: 'KI' }, + { label: 'Comoros', value: 'KM' }, + { label: 'St. Kitts & Nevis', value: 'KN' }, + { label: 'North Korea', value: 'KP' }, + { label: 'South Korea', value: 'KR' }, + { label: 'Kuwait', value: 'KW' }, + { label: 'Cayman Islands', value: 'KY' }, + { label: 'Kazakhstan', value: 'KZ' }, + { label: 'Laos', value: 'LA' }, + { label: 'Lebanon', value: 'LB' }, + { label: 'St. Lucia', value: 'LC' }, + { label: 'Liechtenstein', value: 'LI' }, + { label: 'Sri Lanka', value: 'LK' }, + { label: 'Liberia', value: 'LR' }, + { label: 'Lesotho', value: 'LS' }, + { label: 'Lithuania', value: 'LT' }, + { label: 'Luxembourg', value: 'LU' }, + { label: 'Latvia', value: 'LV' }, + { label: 'Libya', value: 'LY' }, + { label: 'Morocco', value: 'MA' }, + { label: 'Monaco', value: 'MC' }, + { label: 'Moldova', value: 'MD' }, + { label: 'Montenegro', value: 'ME' }, + { label: 'St. Martin', value: 'MF' }, + { label: 'Madagascar', value: 'MG' }, + { label: 'Marshall Islands', value: 'MH' }, + { label: 'North Macedonia', value: 'MK' }, + { label: 'Mali', value: 'ML' }, + { label: 'Myanmar (Burma)', value: 'MM' }, + { label: 'Mongolia', value: 'MN' }, + { label: 'Macao', value: 'MO' }, + { label: 'Northern Mariana Islands', value: 'MP' }, + { label: 'Martinique', value: 'MQ' }, + { label: 'Mauritania', value: 'MR' }, + { label: 'Montserrat', value: 'MS' }, + { label: 'Malta', value: 'MT' }, + { label: 'Mauritius', value: 'MU' }, + { label: 'Maldives', value: 'MV' }, + { label: 'Malawi', value: 'MW' }, + { label: 'Mexico', value: 'MX' }, + { label: 'Malaysia', value: 'MY' }, + { label: 'Mozambique', value: 'MZ' }, + { label: 'Namibia', value: 'NA' }, + { label: 'New Caledonia', value: 'NC' }, + { label: 'Niger', value: 'NE' }, + { label: 'Norfolk Island', value: 'NF' }, + { label: 'Nigeria', value: 'NG' }, + { label: 'Nicaragua', value: 'NI' }, + { label: 'Netherlands', value: 'NL' }, + { label: 'Norway', value: 'NO' }, + { label: 'Nepal', value: 'NP' }, + { label: 'Nauru', value: 'NR' }, + { label: 'Niue', value: 'NU' }, + { label: 'New Zealand', value: 'NZ' }, + { label: 'Oman', value: 'OM' }, + { label: 'Panama', value: 'PA' }, + { label: 'Peru', value: 'PE' }, + { label: 'French Polynesia', value: 'PF' }, + { label: 'Papua New Guinea', value: 'PG' }, + { label: 'Philippines', value: 'PH' }, + { label: 'Pakistan', value: 'PK' }, + { label: 'Poland', value: 'PL' }, + { label: 'St. Pierre & Miquelon', value: 'PM' }, + { label: 'Puerto Rico', value: 'PR' }, + { label: 'Palestine', value: 'PS' }, + { label: 'Portugal', value: 'PT' }, + { label: 'Palau', value: 'PW' }, + { label: 'Paraguay', value: 'PY' }, + { label: 'Qatar', value: 'QA' }, + { label: 'RΓ©union', value: 'RE' }, + { label: 'Romania', value: 'RO' }, + { label: 'Serbia', value: 'RS' }, + { label: 'Russia', value: 'RU' }, + { label: 'Rwanda', value: 'RW' }, + { label: 'Saudi Arabia', value: 'SA' }, + { label: 'Solomon Islands', value: 'SB' }, + { label: 'Seychelles', value: 'SC' }, + { label: 'Sudan', value: 'SD' }, + { label: 'Sweden', value: 'SE' }, + { label: 'Singapore', value: 'SG' }, + { label: 'St. Helena', value: 'SH' }, + { label: 'Slovenia', value: 'SI' }, + { label: 'Svalbard & Jan Mayen', value: 'SJ' }, + { label: 'Slovakia', value: 'SK' }, + { label: 'Sierra Leone', value: 'SL' }, + { label: 'San Marino', value: 'SM' }, + { label: 'Senegal', value: 'SN' }, + { label: 'Somalia', value: 'SO' }, + { label: 'Suriname', value: 'SR' }, + { label: 'South Sudan', value: 'SS' }, + { label: 'SΓ£o TomΓ© & PrΓ­ncipe', value: 'ST' }, + { label: 'El Salvador', value: 'SV' }, + { label: 'Sint Maarten', value: 'SX' }, + { label: 'Syria', value: 'SY' }, + { label: 'Eswatini', value: 'SZ' }, + { label: 'Tristan da Cunha', value: 'TA' }, + { label: 'Turks & Caicos Islands', value: 'TC' }, + { label: 'Chad', value: 'TD' }, + { label: 'Togo', value: 'TG' }, + { label: 'Thailand', value: 'TH' }, + { label: 'Tajikistan', value: 'TJ' }, + { label: 'Tokelau', value: 'TK' }, + { label: 'Timor-Leste', value: 'TL' }, + { label: 'Turkmenistan', value: 'TM' }, + { label: 'Tunisia', value: 'TN' }, + { label: 'Tonga', value: 'TO' }, + { label: 'TΓΌrkiye', value: 'TR' }, + { label: 'Trinidad & Tobago', value: 'TT' }, + { label: 'Tuvalu', value: 'TV' }, + { label: 'Taiwan', value: 'TW' }, + { label: 'Tanzania', value: 'TZ' }, + { label: 'Ukraine', value: 'UA' }, + { label: 'Uganda', value: 'UG' }, + { label: 'United States', value: 'US' }, + { label: 'Uruguay', value: 'UY' }, + { label: 'Uzbekistan', value: 'UZ' }, + { label: 'Vatican City', value: 'VA' }, + { label: 'St. Vincent & Grenadines', value: 'VC' }, + { label: 'Venezuela', value: 'VE' }, + { label: 'British Virgin Islands', value: 'VG' }, + { label: 'U.S. Virgin Islands', value: 'VI' }, + { label: 'Vietnam', value: 'VN' }, + { label: 'Vanuatu', value: 'VU' }, + { label: 'Wallis & Futuna', value: 'WF' }, + { label: 'Samoa', value: 'WS' }, + { label: 'Kosovo', value: 'XK' }, + { label: 'Yemen', value: 'YE' }, + { label: 'Mayotte', value: 'YT' }, + { label: 'South Africa', value: 'ZA' }, + { label: 'Zambia', value: 'ZM' }, + { label: 'Zimbabwe', value: 'ZW' }, +]; + +export default phoneNumberCountryCodes; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/index.js b/packages/backend/src/apps/formatter/dynamic-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..41ca2e37f7df6e26d390bde7f4209b5b6965ae2a --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/index.js @@ -0,0 +1,3 @@ +import listTransformOptions from './list-transform-options/index.js'; + +export default [listTransformOptions]; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/format-date-time.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/format-date-time.js new file mode 100644 index 0000000000000000000000000000000000000000..7dc47c07ca7ba76ddc113731126648785d9046de --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/format-date-time.js @@ -0,0 +1,51 @@ +import formatOptions from './options/format.js'; +import timezoneOptions from './options/timezone.js'; + +const formatDateTime = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'The datetime you want to format.', + variables: true, + }, + { + label: 'From Format', + key: 'fromFormat', + type: 'dropdown', + required: true, + description: 'The format of the input.', + variables: true, + options: formatOptions, + }, + { + label: 'From Timezone', + key: 'fromTimezone', + type: 'dropdown', + required: true, + description: 'The timezone of the input.', + variables: true, + options: timezoneOptions, + }, + { + label: 'To Format', + key: 'toFormat', + type: 'dropdown', + required: true, + description: 'The format of the output.', + variables: true, + options: formatOptions, + }, + { + label: 'To Timezone', + key: 'toTimezone', + type: 'dropdown', + required: true, + description: 'The timezone of the output.', + variables: true, + options: timezoneOptions, + }, +]; + +export default formatDateTime; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/format.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/format.js new file mode 100644 index 0000000000000000000000000000000000000000..24bf8dae60206673288c74b65abec5af6e7b1cbd --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/format.js @@ -0,0 +1,64 @@ +const formatOptions = [ + { + label: 'ccc MMM dd HH:mm:ssZZZ yyyy (Wed Aug 23 12:25:36-0000 2023)', + value: 'ccc MMM dd HH:mm:ssZZZ yyyy', + }, + { + label: 'MMMM dd yyyy HH:mm:ss (August 23 2023 12:25:36)', + value: 'MMMM dd yyyy HH:mm:ss', + }, + { + label: 'MMMM dd yyyy (August 23 2023)', + value: 'MMMM dd yyyy', + }, + { + label: 'MMM dd yyyy (Aug 23 2023)', + value: 'MMM dd yyyy', + }, + { + label: 'yyyy-MM-dd HH:mm:ss ZZZ (2023-08-23 12:25:36 -0000)', + value: 'yyyy-MM-dd HH:mm:ss ZZZ', + }, + { + label: 'yyyy-MM-dd (2023-08-23)', + value: 'yyyy-MM-dd', + }, + { + label: 'MM-dd-yyyy (08-23-2023)', + value: 'MM-dd-yyyy', + }, + { + label: 'MM/dd/yyyy (08/23/2023)', + value: 'MM/dd/yyyy', + }, + { + label: 'MM/dd/yy (08/23/23)', + value: 'MM/dd/yy', + }, + { + label: 'dd-MM-yyyy (23-08-2023)', + value: 'dd-MM-yyyy', + }, + { + label: 'dd/MM/yyyy (23/08/2023)', + value: 'dd/MM/yyyy', + }, + { + label: 'dd/MM/yy (23/08/23)', + value: 'dd/MM/yy', + }, + { + label: 'MM-yyyy (08-2023)', + value: 'MM-yyyy', + }, + { + label: 'Unix timestamp in seconds (1694008283)', + value: 'X', + }, + { + label: 'Unix timestamp in milliseconds (1694008306315)', + value: 'x', + }, +]; + +export default formatOptions; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/timezone.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/timezone.js new file mode 100644 index 0000000000000000000000000000000000000000..f48ec66824c6335944c0c632416112b0b00a1617 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/date-time/options/timezone.js @@ -0,0 +1,449 @@ +// The list from Intl.supportedValuesOf('timeZone') which is used by Luxon. + +const timezoneOptions = [ + { label: 'Africa/Abidjan', value: 'Africa/Abidjan' }, + { label: 'Africa/Accra', value: 'Africa/Accra' }, + { label: 'Africa/Addis_Ababa', value: 'Africa/Addis_Ababa' }, + { label: 'Africa/Algiers', value: 'Africa/Algiers' }, + { label: 'Africa/Asmera', value: 'Africa/Asmera' }, + { label: 'Africa/Bamako', value: 'Africa/Bamako' }, + { label: 'Africa/Bangui', value: 'Africa/Bangui' }, + { label: 'Africa/Banjul', value: 'Africa/Banjul' }, + { label: 'Africa/Bissau', value: 'Africa/Bissau' }, + { label: 'Africa/Blantyre', value: 'Africa/Blantyre' }, + { label: 'Africa/Brazzaville', value: 'Africa/Brazzaville' }, + { label: 'Africa/Bujumbura', value: 'Africa/Bujumbura' }, + { label: 'Africa/Cairo', value: 'Africa/Cairo' }, + { label: 'Africa/Casablanca', value: 'Africa/Casablanca' }, + { label: 'Africa/Ceuta', value: 'Africa/Ceuta' }, + { label: 'Africa/Conakry', value: 'Africa/Conakry' }, + { label: 'Africa/Dakar', value: 'Africa/Dakar' }, + { label: 'Africa/Dar_es_Salaam', value: 'Africa/Dar_es_Salaam' }, + { label: 'Africa/Djibouti', value: 'Africa/Djibouti' }, + { label: 'Africa/Douala', value: 'Africa/Douala' }, + { label: 'Africa/El_Aaiun', value: 'Africa/El_Aaiun' }, + { label: 'Africa/Freetown', value: 'Africa/Freetown' }, + { label: 'Africa/Gaborone', value: 'Africa/Gaborone' }, + { label: 'Africa/Harare', value: 'Africa/Harare' }, + { label: 'Africa/Johannesburg', value: 'Africa/Johannesburg' }, + { label: 'Africa/Juba', value: 'Africa/Juba' }, + { label: 'Africa/Kampala', value: 'Africa/Kampala' }, + { label: 'Africa/Khartoum', value: 'Africa/Khartoum' }, + { label: 'Africa/Kigali', value: 'Africa/Kigali' }, + { label: 'Africa/Kinshasa', value: 'Africa/Kinshasa' }, + { label: 'Africa/Lagos', value: 'Africa/Lagos' }, + { label: 'Africa/Libreville', value: 'Africa/Libreville' }, + { label: 'Africa/Lome', value: 'Africa/Lome' }, + { label: 'Africa/Luanda', value: 'Africa/Luanda' }, + { label: 'Africa/Lubumbashi', value: 'Africa/Lubumbashi' }, + { label: 'Africa/Lusaka', value: 'Africa/Lusaka' }, + { label: 'Africa/Malabo', value: 'Africa/Malabo' }, + { label: 'Africa/Maputo', value: 'Africa/Maputo' }, + { label: 'Africa/Maseru', value: 'Africa/Maseru' }, + { label: 'Africa/Mbabane', value: 'Africa/Mbabane' }, + { label: 'Africa/Mogadishu', value: 'Africa/Mogadishu' }, + { label: 'Africa/Monrovia', value: 'Africa/Monrovia' }, + { label: 'Africa/Nairobi', value: 'Africa/Nairobi' }, + { label: 'Africa/Ndjamena', value: 'Africa/Ndjamena' }, + { label: 'Africa/Niamey', value: 'Africa/Niamey' }, + { label: 'Africa/Nouakchott', value: 'Africa/Nouakchott' }, + { label: 'Africa/Ouagadougou', value: 'Africa/Ouagadougou' }, + { label: 'Africa/Porto-Novo', value: 'Africa/Porto-Novo' }, + { label: 'Africa/Sao_Tome', value: 'Africa/Sao_Tome' }, + { label: 'Africa/Tripoli', value: 'Africa/Tripoli' }, + { label: 'Africa/Tunis', value: 'Africa/Tunis' }, + { label: 'Africa/Windhoek', value: 'Africa/Windhoek' }, + { label: 'America/Adak', value: 'America/Adak' }, + { label: 'America/Anchorage', value: 'America/Anchorage' }, + { label: 'America/Anguilla', value: 'America/Anguilla' }, + { label: 'America/Antigua', value: 'America/Antigua' }, + { label: 'America/Araguaina', value: 'America/Araguaina' }, + { label: 'America/Argentina/La_Rioja', value: 'America/Argentina/La_Rioja' }, + { + label: 'America/Argentina/Rio_Gallegos', + value: 'America/Argentina/Rio_Gallegos', + }, + { label: 'America/Argentina/Salta', value: 'America/Argentina/Salta' }, + { label: 'America/Argentina/San_Juan', value: 'America/Argentina/San_Juan' }, + { label: 'America/Argentina/San_Luis', value: 'America/Argentina/San_Luis' }, + { label: 'America/Argentina/Tucuman', value: 'America/Argentina/Tucuman' }, + { label: 'America/Argentina/Ushuaia', value: 'America/Argentina/Ushuaia' }, + { label: 'America/Aruba', value: 'America/Aruba' }, + { label: 'America/Asuncion', value: 'America/Asuncion' }, + { label: 'America/Bahia', value: 'America/Bahia' }, + { label: 'America/Bahia_Banderas', value: 'America/Bahia_Banderas' }, + { label: 'America/Barbados', value: 'America/Barbados' }, + { label: 'America/Belem', value: 'America/Belem' }, + { label: 'America/Belize', value: 'America/Belize' }, + { label: 'America/Blanc-Sablon', value: 'America/Blanc-Sablon' }, + { label: 'America/Boa_Vista', value: 'America/Boa_Vista' }, + { label: 'America/Bogota', value: 'America/Bogota' }, + { label: 'America/Boise', value: 'America/Boise' }, + { label: 'America/Buenos_Aires', value: 'America/Buenos_Aires' }, + { label: 'America/Cambridge_Bay', value: 'America/Cambridge_Bay' }, + { label: 'America/Campo_Grande', value: 'America/Campo_Grande' }, + { label: 'America/Cancun', value: 'America/Cancun' }, + { label: 'America/Caracas', value: 'America/Caracas' }, + { label: 'America/Catamarca', value: 'America/Catamarca' }, + { label: 'America/Cayenne', value: 'America/Cayenne' }, + { label: 'America/Cayman', value: 'America/Cayman' }, + { label: 'America/Chicago', value: 'America/Chicago' }, + { label: 'America/Chihuahua', value: 'America/Chihuahua' }, + { label: 'America/Ciudad_Juarez', value: 'America/Ciudad_Juarez' }, + { label: 'America/Coral_Harbour', value: 'America/Coral_Harbour' }, + { label: 'America/Cordoba', value: 'America/Cordoba' }, + { label: 'America/Costa_Rica', value: 'America/Costa_Rica' }, + { label: 'America/Creston', value: 'America/Creston' }, + { label: 'America/Cuiaba', value: 'America/Cuiaba' }, + { label: 'America/Curacao', value: 'America/Curacao' }, + { label: 'America/Danmarkshavn', value: 'America/Danmarkshavn' }, + { label: 'America/Dawson', value: 'America/Dawson' }, + { label: 'America/Dawson_Creek', value: 'America/Dawson_Creek' }, + { label: 'America/Denver', value: 'America/Denver' }, + { label: 'America/Detroit', value: 'America/Detroit' }, + { label: 'America/Dominica', value: 'America/Dominica' }, + { label: 'America/Edmonton', value: 'America/Edmonton' }, + { label: 'America/Eirunepe', value: 'America/Eirunepe' }, + { label: 'America/El_Salvador', value: 'America/El_Salvador' }, + { label: 'America/Fort_Nelson', value: 'America/Fort_Nelson' }, + { label: 'America/Fortaleza', value: 'America/Fortaleza' }, + { label: 'America/Glace_Bay', value: 'America/Glace_Bay' }, + { label: 'America/Godthab', value: 'America/Godthab' }, + { label: 'America/Goose_Bay', value: 'America/Goose_Bay' }, + { label: 'America/Grand_Turk', value: 'America/Grand_Turk' }, + { label: 'America/Grenada', value: 'America/Grenada' }, + { label: 'America/Guadeloupe', value: 'America/Guadeloupe' }, + { label: 'America/Guatemala', value: 'America/Guatemala' }, + { label: 'America/Guayaquil', value: 'America/Guayaquil' }, + { label: 'America/Guyana', value: 'America/Guyana' }, + { label: 'America/Halifax', value: 'America/Halifax' }, + { label: 'America/Havana', value: 'America/Havana' }, + { label: 'America/Hermosillo', value: 'America/Hermosillo' }, + { label: 'America/Indiana/Knox', value: 'America/Indiana/Knox' }, + { label: 'America/Indiana/Marengo', value: 'America/Indiana/Marengo' }, + { label: 'America/Indiana/Petersburg', value: 'America/Indiana/Petersburg' }, + { label: 'America/Indiana/Tell_City', value: 'America/Indiana/Tell_City' }, + { label: 'America/Indiana/Vevay', value: 'America/Indiana/Vevay' }, + { label: 'America/Indiana/Vincennes', value: 'America/Indiana/Vincennes' }, + { label: 'America/Indiana/Winamac', value: 'America/Indiana/Winamac' }, + { label: 'America/Indianapolis', value: 'America/Indianapolis' }, + { label: 'America/Inuvik', value: 'America/Inuvik' }, + { label: 'America/Iqaluit', value: 'America/Iqaluit' }, + { label: 'America/Jamaica', value: 'America/Jamaica' }, + { label: 'America/Jujuy', value: 'America/Jujuy' }, + { label: 'America/Juneau', value: 'America/Juneau' }, + { + label: 'America/Kentucky/Monticello', + value: 'America/Kentucky/Monticello', + }, + { label: 'America/Kralendijk', value: 'America/Kralendijk' }, + { label: 'America/La_Paz', value: 'America/La_Paz' }, + { label: 'America/Lima', value: 'America/Lima' }, + { label: 'America/Los_Angeles', value: 'America/Los_Angeles' }, + { label: 'America/Louisville', value: 'America/Louisville' }, + { label: 'America/Lower_Princes', value: 'America/Lower_Princes' }, + { label: 'America/Maceio', value: 'America/Maceio' }, + { label: 'America/Managua', value: 'America/Managua' }, + { label: 'America/Manaus', value: 'America/Manaus' }, + { label: 'America/Marigot', value: 'America/Marigot' }, + { label: 'America/Martinique', value: 'America/Martinique' }, + { label: 'America/Matamoros', value: 'America/Matamoros' }, + { label: 'America/Mazatlan', value: 'America/Mazatlan' }, + { label: 'America/Mendoza', value: 'America/Mendoza' }, + { label: 'America/Menominee', value: 'America/Menominee' }, + { label: 'America/Merida', value: 'America/Merida' }, + { label: 'America/Metlakatla', value: 'America/Metlakatla' }, + { label: 'America/Mexico_City', value: 'America/Mexico_City' }, + { label: 'America/Miquelon', value: 'America/Miquelon' }, + { label: 'America/Moncton', value: 'America/Moncton' }, + { label: 'America/Monterrey', value: 'America/Monterrey' }, + { label: 'America/Montevideo', value: 'America/Montevideo' }, + { label: 'America/Montserrat', value: 'America/Montserrat' }, + { label: 'America/Nassau', value: 'America/Nassau' }, + { label: 'America/New_York', value: 'America/New_York' }, + { label: 'America/Nipigon', value: 'America/Nipigon' }, + { label: 'America/Nome', value: 'America/Nome' }, + { label: 'America/Noronha', value: 'America/Noronha' }, + { + label: 'America/North_Dakota/Beulah', + value: 'America/North_Dakota/Beulah', + }, + { + label: 'America/North_Dakota/Center', + value: 'America/North_Dakota/Center', + }, + { + label: 'America/North_Dakota/New_Salem', + value: 'America/North_Dakota/New_Salem', + }, + { label: 'America/Ojinaga', value: 'America/Ojinaga' }, + { label: 'America/Panama', value: 'America/Panama' }, + { label: 'America/Pangnirtung', value: 'America/Pangnirtung' }, + { label: 'America/Paramaribo', value: 'America/Paramaribo' }, + { label: 'America/Phoenix', value: 'America/Phoenix' }, + { label: 'America/Port-au-Prince', value: 'America/Port-au-Prince' }, + { label: 'America/Port_of_Spain', value: 'America/Port_of_Spain' }, + { label: 'America/Porto_Velho', value: 'America/Porto_Velho' }, + { label: 'America/Puerto_Rico', value: 'America/Puerto_Rico' }, + { label: 'America/Punta_Arenas', value: 'America/Punta_Arenas' }, + { label: 'America/Rainy_River', value: 'America/Rainy_River' }, + { label: 'America/Rankin_Inlet', value: 'America/Rankin_Inlet' }, + { label: 'America/Recife', value: 'America/Recife' }, + { label: 'America/Regina', value: 'America/Regina' }, + { label: 'America/Resolute', value: 'America/Resolute' }, + { label: 'America/Rio_Branco', value: 'America/Rio_Branco' }, + { label: 'America/Santa_Isabel', value: 'America/Santa_Isabel' }, + { label: 'America/Santarem', value: 'America/Santarem' }, + { label: 'America/Santiago', value: 'America/Santiago' }, + { label: 'America/Santo_Domingo', value: 'America/Santo_Domingo' }, + { label: 'America/Sao_Paulo', value: 'America/Sao_Paulo' }, + { label: 'America/Scoresbysund', value: 'America/Scoresbysund' }, + { label: 'America/Sitka', value: 'America/Sitka' }, + { label: 'America/St_Barthelemy', value: 'America/St_Barthelemy' }, + { label: 'America/St_Johns', value: 'America/St_Johns' }, + { label: 'America/St_Kitts', value: 'America/St_Kitts' }, + { label: 'America/St_Lucia', value: 'America/St_Lucia' }, + { label: 'America/St_Thomas', value: 'America/St_Thomas' }, + { label: 'America/St_Vincent', value: 'America/St_Vincent' }, + { label: 'America/Swift_Current', value: 'America/Swift_Current' }, + { label: 'America/Tegucigalpa', value: 'America/Tegucigalpa' }, + { label: 'America/Thule', value: 'America/Thule' }, + { label: 'America/Thunder_Bay', value: 'America/Thunder_Bay' }, + { label: 'America/Tijuana', value: 'America/Tijuana' }, + { label: 'America/Toronto', value: 'America/Toronto' }, + { label: 'America/Tortola', value: 'America/Tortola' }, + { label: 'America/Vancouver', value: 'America/Vancouver' }, + { label: 'America/Whitehorse', value: 'America/Whitehorse' }, + { label: 'America/Winnipeg', value: 'America/Winnipeg' }, + { label: 'America/Yakutat', value: 'America/Yakutat' }, + { label: 'America/Yellowknife', value: 'America/Yellowknife' }, + { label: 'Antarctica/Casey', value: 'Antarctica/Casey' }, + { label: 'Antarctica/Davis', value: 'Antarctica/Davis' }, + { label: 'Antarctica/DumontDUrville', value: 'Antarctica/DumontDUrville' }, + { label: 'Antarctica/Macquarie', value: 'Antarctica/Macquarie' }, + { label: 'Antarctica/Mawson', value: 'Antarctica/Mawson' }, + { label: 'Antarctica/McMurdo', value: 'Antarctica/McMurdo' }, + { label: 'Antarctica/Palmer', value: 'Antarctica/Palmer' }, + { label: 'Antarctica/Rothera', value: 'Antarctica/Rothera' }, + { label: 'Antarctica/Syowa', value: 'Antarctica/Syowa' }, + { label: 'Antarctica/Troll', value: 'Antarctica/Troll' }, + { label: 'Antarctica/Vostok', value: 'Antarctica/Vostok' }, + { label: 'Arctic/Longyearbyen', value: 'Arctic/Longyearbyen' }, + { label: 'Asia/Aden', value: 'Asia/Aden' }, + { label: 'Asia/Almaty', value: 'Asia/Almaty' }, + { label: 'Asia/Amman', value: 'Asia/Amman' }, + { label: 'Asia/Anadyr', value: 'Asia/Anadyr' }, + { label: 'Asia/Aqtau', value: 'Asia/Aqtau' }, + { label: 'Asia/Aqtobe', value: 'Asia/Aqtobe' }, + { label: 'Asia/Ashgabat', value: 'Asia/Ashgabat' }, + { label: 'Asia/Atyrau', value: 'Asia/Atyrau' }, + { label: 'Asia/Baghdad', value: 'Asia/Baghdad' }, + { label: 'Asia/Bahrain', value: 'Asia/Bahrain' }, + { label: 'Asia/Baku', value: 'Asia/Baku' }, + { label: 'Asia/Bangkok', value: 'Asia/Bangkok' }, + { label: 'Asia/Barnaul', value: 'Asia/Barnaul' }, + { label: 'Asia/Beirut', value: 'Asia/Beirut' }, + { label: 'Asia/Bishkek', value: 'Asia/Bishkek' }, + { label: 'Asia/Brunei', value: 'Asia/Brunei' }, + { label: 'Asia/Calcutta', value: 'Asia/Calcutta' }, + { label: 'Asia/Chita', value: 'Asia/Chita' }, + { label: 'Asia/Choibalsan', value: 'Asia/Choibalsan' }, + { label: 'Asia/Colombo', value: 'Asia/Colombo' }, + { label: 'Asia/Damascus', value: 'Asia/Damascus' }, + { label: 'Asia/Dhaka', value: 'Asia/Dhaka' }, + { label: 'Asia/Dili', value: 'Asia/Dili' }, + { label: 'Asia/Dubai', value: 'Asia/Dubai' }, + { label: 'Asia/Dushanbe', value: 'Asia/Dushanbe' }, + { label: 'Asia/Famagusta', value: 'Asia/Famagusta' }, + { label: 'Asia/Gaza', value: 'Asia/Gaza' }, + { label: 'Asia/Hebron', value: 'Asia/Hebron' }, + { label: 'Asia/Hong_Kong', value: 'Asia/Hong_Kong' }, + { label: 'Asia/Hovd', value: 'Asia/Hovd' }, + { label: 'Asia/Irkutsk', value: 'Asia/Irkutsk' }, + { label: 'Asia/Jakarta', value: 'Asia/Jakarta' }, + { label: 'Asia/Jayapura', value: 'Asia/Jayapura' }, + { label: 'Asia/Jerusalem', value: 'Asia/Jerusalem' }, + { label: 'Asia/Kabul', value: 'Asia/Kabul' }, + { label: 'Asia/Kamchatka', value: 'Asia/Kamchatka' }, + { label: 'Asia/Karachi', value: 'Asia/Karachi' }, + { label: 'Asia/Katmandu', value: 'Asia/Katmandu' }, + { label: 'Asia/Khandyga', value: 'Asia/Khandyga' }, + { label: 'Asia/Krasnoyarsk', value: 'Asia/Krasnoyarsk' }, + { label: 'Asia/Kuala_Lumpur', value: 'Asia/Kuala_Lumpur' }, + { label: 'Asia/Kuching', value: 'Asia/Kuching' }, + { label: 'Asia/Kuwait', value: 'Asia/Kuwait' }, + { label: 'Asia/Macau', value: 'Asia/Macau' }, + { label: 'Asia/Magadan', value: 'Asia/Magadan' }, + { label: 'Asia/Makassar', value: 'Asia/Makassar' }, + { label: 'Asia/Manila', value: 'Asia/Manila' }, + { label: 'Asia/Muscat', value: 'Asia/Muscat' }, + { label: 'Asia/Nicosia', value: 'Asia/Nicosia' }, + { label: 'Asia/Novokuznetsk', value: 'Asia/Novokuznetsk' }, + { label: 'Asia/Novosibirsk', value: 'Asia/Novosibirsk' }, + { label: 'Asia/Omsk', value: 'Asia/Omsk' }, + { label: 'Asia/Oral', value: 'Asia/Oral' }, + { label: 'Asia/Phnom_Penh', value: 'Asia/Phnom_Penh' }, + { label: 'Asia/Pontianak', value: 'Asia/Pontianak' }, + { label: 'Asia/Pyongyang', value: 'Asia/Pyongyang' }, + { label: 'Asia/Qatar', value: 'Asia/Qatar' }, + { label: 'Asia/Qostanay', value: 'Asia/Qostanay' }, + { label: 'Asia/Qyzylorda', value: 'Asia/Qyzylorda' }, + { label: 'Asia/Rangoon', value: 'Asia/Rangoon' }, + { label: 'Asia/Riyadh', value: 'Asia/Riyadh' }, + { label: 'Asia/Saigon', value: 'Asia/Saigon' }, + { label: 'Asia/Sakhalin', value: 'Asia/Sakhalin' }, + { label: 'Asia/Samarkand', value: 'Asia/Samarkand' }, + { label: 'Asia/Seoul', value: 'Asia/Seoul' }, + { label: 'Asia/Shanghai', value: 'Asia/Shanghai' }, + { label: 'Asia/Singapore', value: 'Asia/Singapore' }, + { label: 'Asia/Srednekolymsk', value: 'Asia/Srednekolymsk' }, + { label: 'Asia/Taipei', value: 'Asia/Taipei' }, + { label: 'Asia/Tashkent', value: 'Asia/Tashkent' }, + { label: 'Asia/Tbilisi', value: 'Asia/Tbilisi' }, + { label: 'Asia/Tehran', value: 'Asia/Tehran' }, + { label: 'Asia/Thimphu', value: 'Asia/Thimphu' }, + { label: 'Asia/Tokyo', value: 'Asia/Tokyo' }, + { label: 'Asia/Tomsk', value: 'Asia/Tomsk' }, + { label: 'Asia/Ulaanbaatar', value: 'Asia/Ulaanbaatar' }, + { label: 'Asia/Urumqi', value: 'Asia/Urumqi' }, + { label: 'Asia/Ust-Nera', value: 'Asia/Ust-Nera' }, + { label: 'Asia/Vientiane', value: 'Asia/Vientiane' }, + { label: 'Asia/Vladivostok', value: 'Asia/Vladivostok' }, + { label: 'Asia/Yakutsk', value: 'Asia/Yakutsk' }, + { label: 'Asia/Yekaterinburg', value: 'Asia/Yekaterinburg' }, + { label: 'Asia/Yerevan', value: 'Asia/Yerevan' }, + { label: 'Atlantic/Azores', value: 'Atlantic/Azores' }, + { label: 'Atlantic/Bermuda', value: 'Atlantic/Bermuda' }, + { label: 'Atlantic/Canary', value: 'Atlantic/Canary' }, + { label: 'Atlantic/Cape_Verde', value: 'Atlantic/Cape_Verde' }, + { label: 'Atlantic/Faeroe', value: 'Atlantic/Faeroe' }, + { label: 'Atlantic/Madeira', value: 'Atlantic/Madeira' }, + { label: 'Atlantic/Reykjavik', value: 'Atlantic/Reykjavik' }, + { label: 'Atlantic/South_Georgia', value: 'Atlantic/South_Georgia' }, + { label: 'Atlantic/St_Helena', value: 'Atlantic/St_Helena' }, + { label: 'Atlantic/Stanley', value: 'Atlantic/Stanley' }, + { label: 'Australia/Adelaide', value: 'Australia/Adelaide' }, + { label: 'Australia/Brisbane', value: 'Australia/Brisbane' }, + { label: 'Australia/Broken_Hill', value: 'Australia/Broken_Hill' }, + { label: 'Australia/Currie', value: 'Australia/Currie' }, + { label: 'Australia/Darwin', value: 'Australia/Darwin' }, + { label: 'Australia/Eucla', value: 'Australia/Eucla' }, + { label: 'Australia/Hobart', value: 'Australia/Hobart' }, + { label: 'Australia/Lindeman', value: 'Australia/Lindeman' }, + { label: 'Australia/Lord_Howe', value: 'Australia/Lord_Howe' }, + { label: 'Australia/Melbourne', value: 'Australia/Melbourne' }, + { label: 'Australia/Perth', value: 'Australia/Perth' }, + { label: 'Australia/Sydney', value: 'Australia/Sydney' }, + { label: 'Europe/Amsterdam', value: 'Europe/Amsterdam' }, + { label: 'Europe/Andorra', value: 'Europe/Andorra' }, + { label: 'Europe/Astrakhan', value: 'Europe/Astrakhan' }, + { label: 'Europe/Athens', value: 'Europe/Athens' }, + { label: 'Europe/Belgrade', value: 'Europe/Belgrade' }, + { label: 'Europe/Berlin', value: 'Europe/Berlin' }, + { label: 'Europe/Bratislava', value: 'Europe/Bratislava' }, + { label: 'Europe/Brussels', value: 'Europe/Brussels' }, + { label: 'Europe/Bucharest', value: 'Europe/Bucharest' }, + { label: 'Europe/Budapest', value: 'Europe/Budapest' }, + { label: 'Europe/Busingen', value: 'Europe/Busingen' }, + { label: 'Europe/Chisinau', value: 'Europe/Chisinau' }, + { label: 'Europe/Copenhagen', value: 'Europe/Copenhagen' }, + { label: 'Europe/Dublin', value: 'Europe/Dublin' }, + { label: 'Europe/Gibraltar', value: 'Europe/Gibraltar' }, + { label: 'Europe/Guernsey', value: 'Europe/Guernsey' }, + { label: 'Europe/Helsinki', value: 'Europe/Helsinki' }, + { label: 'Europe/Isle_of_Man', value: 'Europe/Isle_of_Man' }, + { label: 'Europe/Istanbul', value: 'Europe/Istanbul' }, + { label: 'Europe/Jersey', value: 'Europe/Jersey' }, + { label: 'Europe/Kaliningrad', value: 'Europe/Kaliningrad' }, + { label: 'Europe/Kiev', value: 'Europe/Kiev' }, + { label: 'Europe/Kirov', value: 'Europe/Kirov' }, + { label: 'Europe/Lisbon', value: 'Europe/Lisbon' }, + { label: 'Europe/Ljubljana', value: 'Europe/Ljubljana' }, + { label: 'Europe/London', value: 'Europe/London' }, + { label: 'Europe/Luxembourg', value: 'Europe/Luxembourg' }, + { label: 'Europe/Madrid', value: 'Europe/Madrid' }, + { label: 'Europe/Malta', value: 'Europe/Malta' }, + { label: 'Europe/Mariehamn', value: 'Europe/Mariehamn' }, + { label: 'Europe/Minsk', value: 'Europe/Minsk' }, + { label: 'Europe/Monaco', value: 'Europe/Monaco' }, + { label: 'Europe/Moscow', value: 'Europe/Moscow' }, + { label: 'Europe/Oslo', value: 'Europe/Oslo' }, + { label: 'Europe/Paris', value: 'Europe/Paris' }, + { label: 'Europe/Podgorica', value: 'Europe/Podgorica' }, + { label: 'Europe/Prague', value: 'Europe/Prague' }, + { label: 'Europe/Riga', value: 'Europe/Riga' }, + { label: 'Europe/Rome', value: 'Europe/Rome' }, + { label: 'Europe/Samara', value: 'Europe/Samara' }, + { label: 'Europe/San_Marino', value: 'Europe/San_Marino' }, + { label: 'Europe/Sarajevo', value: 'Europe/Sarajevo' }, + { label: 'Europe/Saratov', value: 'Europe/Saratov' }, + { label: 'Europe/Simferopol', value: 'Europe/Simferopol' }, + { label: 'Europe/Skopje', value: 'Europe/Skopje' }, + { label: 'Europe/Sofia', value: 'Europe/Sofia' }, + { label: 'Europe/Stockholm', value: 'Europe/Stockholm' }, + { label: 'Europe/Tallinn', value: 'Europe/Tallinn' }, + { label: 'Europe/Tirane', value: 'Europe/Tirane' }, + { label: 'Europe/Ulyanovsk', value: 'Europe/Ulyanovsk' }, + { label: 'Europe/Uzhgorod', value: 'Europe/Uzhgorod' }, + { label: 'Europe/Vaduz', value: 'Europe/Vaduz' }, + { label: 'Europe/Vatican', value: 'Europe/Vatican' }, + { label: 'Europe/Vienna', value: 'Europe/Vienna' }, + { label: 'Europe/Vilnius', value: 'Europe/Vilnius' }, + { label: 'Europe/Volgograd', value: 'Europe/Volgograd' }, + { label: 'Europe/Warsaw', value: 'Europe/Warsaw' }, + { label: 'Europe/Zagreb', value: 'Europe/Zagreb' }, + { label: 'Europe/Zaporozhye', value: 'Europe/Zaporozhye' }, + { label: 'Europe/Zurich', value: 'Europe/Zurich' }, + { label: 'Indian/Antananarivo', value: 'Indian/Antananarivo' }, + { label: 'Indian/Chagos', value: 'Indian/Chagos' }, + { label: 'Indian/Christmas', value: 'Indian/Christmas' }, + { label: 'Indian/Cocos', value: 'Indian/Cocos' }, + { label: 'Indian/Comoro', value: 'Indian/Comoro' }, + { label: 'Indian/Kerguelen', value: 'Indian/Kerguelen' }, + { label: 'Indian/Mahe', value: 'Indian/Mahe' }, + { label: 'Indian/Maldives', value: 'Indian/Maldives' }, + { label: 'Indian/Mauritius', value: 'Indian/Mauritius' }, + { label: 'Indian/Mayotte', value: 'Indian/Mayotte' }, + { label: 'Indian/Reunion', value: 'Indian/Reunion' }, + { label: 'Pacific/Apia', value: 'Pacific/Apia' }, + { label: 'Pacific/Auckland', value: 'Pacific/Auckland' }, + { label: 'Pacific/Bougainville', value: 'Pacific/Bougainville' }, + { label: 'Pacific/Chatham', value: 'Pacific/Chatham' }, + { label: 'Pacific/Easter', value: 'Pacific/Easter' }, + { label: 'Pacific/Efate', value: 'Pacific/Efate' }, + { label: 'Pacific/Enderbury', value: 'Pacific/Enderbury' }, + { label: 'Pacific/Fakaofo', value: 'Pacific/Fakaofo' }, + { label: 'Pacific/Fiji', value: 'Pacific/Fiji' }, + { label: 'Pacific/Funafuti', value: 'Pacific/Funafuti' }, + { label: 'Pacific/Galapagos', value: 'Pacific/Galapagos' }, + { label: 'Pacific/Gambier', value: 'Pacific/Gambier' }, + { label: 'Pacific/Guadalcanal', value: 'Pacific/Guadalcanal' }, + { label: 'Pacific/Guam', value: 'Pacific/Guam' }, + { label: 'Pacific/Honolulu', value: 'Pacific/Honolulu' }, + { label: 'Pacific/Johnston', value: 'Pacific/Johnston' }, + { label: 'Pacific/Kiritimati', value: 'Pacific/Kiritimati' }, + { label: 'Pacific/Kosrae', value: 'Pacific/Kosrae' }, + { label: 'Pacific/Kwajalein', value: 'Pacific/Kwajalein' }, + { label: 'Pacific/Majuro', value: 'Pacific/Majuro' }, + { label: 'Pacific/Marquesas', value: 'Pacific/Marquesas' }, + { label: 'Pacific/Midway', value: 'Pacific/Midway' }, + { label: 'Pacific/Nauru', value: 'Pacific/Nauru' }, + { label: 'Pacific/Niue', value: 'Pacific/Niue' }, + { label: 'Pacific/Norfolk', value: 'Pacific/Norfolk' }, + { label: 'Pacific/Noumea', value: 'Pacific/Noumea' }, + { label: 'Pacific/Pago_Pago', value: 'Pacific/Pago_Pago' }, + { label: 'Pacific/Palau', value: 'Pacific/Palau' }, + { label: 'Pacific/Pitcairn', value: 'Pacific/Pitcairn' }, + { label: 'Pacific/Ponape', value: 'Pacific/Ponape' }, + { label: 'Pacific/Port_Moresby', value: 'Pacific/Port_Moresby' }, + { label: 'Pacific/Rarotonga', value: 'Pacific/Rarotonga' }, + { label: 'Pacific/Saipan', value: 'Pacific/Saipan' }, + { label: 'Pacific/Tahiti', value: 'Pacific/Tahiti' }, + { label: 'Pacific/Tarawa', value: 'Pacific/Tarawa' }, + { label: 'Pacific/Tongatapu', value: 'Pacific/Tongatapu' }, + { label: 'Pacific/Truk', value: 'Pacific/Truk' }, + { label: 'Pacific/Wake', value: 'Pacific/Wake' }, + { label: 'Pacific/Wallis', value: 'Pacific/Wallis' }, +]; + +export default timezoneOptions; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/index.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bb38179f7d9cffadb78cc6182b2fc626b5a34b4d --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/index.js @@ -0,0 +1,50 @@ +import base64ToString from './text/base64-to-string.js'; +import capitalize from './text/capitalize.js'; +import encodeUriComponent from './text/encode-uri-component.js'; +import extractEmailAddress from './text/extract-email-address.js'; +import extractNumber from './text/extract-number.js'; +import htmlToMarkdown from './text/html-to-markdown.js'; +import lowercase from './text/lowercase.js'; +import markdownToHtml from './text/markdown-to-html.js'; +import pluralize from './text/pluralize.js'; +import replace from './text/replace.js'; +import stringToBase64 from './text/string-to-base64.js'; +import encodeUri from './text/encode-uri.js'; +import trimWhitespace from './text/trim-whitespace.js'; +import useDefaultValue from './text/use-default-value.js'; +import performMathOperation from './numbers/perform-math-operation.js'; +import randomNumber from './numbers/random-number.js'; +import formatNumber from './numbers/format-number.js'; +import formatPhoneNumber from './numbers/format-phone-number.js'; +import formatDateTime from './date-time/format-date-time.js'; + +const options = { + base64ToString, + capitalize, + encodeUriComponent, + extractEmailAddress, + extractNumber, + htmlToMarkdown, + lowercase, + markdownToHtml, + pluralize, + replace, + stringToBase64, + encodeUri, + trimWhitespace, + useDefaultValue, + performMathOperation, + randomNumber, + formatNumber, + formatPhoneNumber, + formatDateTime, +}; + +export default { + name: 'List fields after transform', + key: 'listTransformOptions', + + async run($) { + return options[$.step.parameters.transform]; + }, +}; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-number.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-number.js new file mode 100644 index 0000000000000000000000000000000000000000..9fca34fd131eff3ba2751b2d2a6a495dca87343e --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-number.js @@ -0,0 +1,38 @@ +const formatNumber = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'The number you want to format.', + variables: true, + }, + { + label: 'Input Decimal Mark', + key: 'inputDecimalMark', + type: 'dropdown', + required: true, + description: 'The decimal mark of the input number.', + variables: true, + options: [ + { label: 'Comma', value: ',' }, + { label: 'Period', value: '.' }, + ], + }, + { + label: 'To Format', + key: 'toFormat', + type: 'dropdown', + required: true, + description: 'The format you want to convert the number to.', + variables: true, + options: [ + { label: 'Comma for grouping & period for decimal', value: '0' }, + { label: 'Period for grouping & comma for decimal', value: '1' }, + { label: 'Space for grouping & period for decimal', value: '2' }, + { label: 'Space for grouping & comma for decimal', value: '3' }, + ], + }, +]; + +export default formatNumber; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-phone-number.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-phone-number.js new file mode 100644 index 0000000000000000000000000000000000000000..c96b126eadb497ddd40ed6ab9084f4489fe25c7b --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/format-phone-number.js @@ -0,0 +1,36 @@ +import phoneNumberCountryCodes from '../../../common/phone-number-country-codes.js'; + +const formatPhoneNumber = [ + { + label: 'Phone Number', + key: 'phoneNumber', + type: 'string', + required: true, + description: 'The phone number you want to format.', + variables: true, + }, + { + label: 'To Format', + key: 'toFormat', + type: 'dropdown', + required: true, + description: 'The format you want to convert the number to.', + variables: true, + options: [ + { label: '+491632223344 (E164)', value: 'e164' }, + { label: '+49 163 2223344 (International)', value: 'international' }, + { label: '0163 2223344 (National)', value: 'national' }, + ], + }, + { + label: 'Phone Number Country Code', + key: 'phoneNumberCountryCode', + type: 'dropdown', + required: true, + description: 'The country code of the phone number. The default is US.', + variables: true, + options: phoneNumberCountryCodes, + }, +]; + +export default formatPhoneNumber; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/perform-math-operation.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/perform-math-operation.js new file mode 100644 index 0000000000000000000000000000000000000000..85378bb3f3ac1ed1ac3c74cc0ec5678e46c9fe22 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/perform-math-operation.js @@ -0,0 +1,36 @@ +const performMathOperation = [ + { + label: 'Math Operation', + key: 'mathOperation', + type: 'dropdown', + required: true, + description: 'The math operation to perform.', + variables: true, + options: [ + { label: 'Add', value: 'add' }, + { label: 'Divide', value: 'divide' }, + { label: 'Make Negative', value: 'makeNegative' }, + { label: 'Multiply', value: 'multiply' }, + { label: 'Subtract', value: 'subtract' }, + ], + }, + { + label: 'Values', + key: 'values', + type: 'dynamic', + required: false, + description: 'Add or remove numbers as needed.', + fields: [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'The number to perform the math operation on.', + variables: true, + }, + ], + }, +]; + +export default performMathOperation; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/random-number.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/random-number.js new file mode 100644 index 0000000000000000000000000000000000000000..ba3d7142b2b63af083d2efae3bd872d820a1c781 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/numbers/random-number.js @@ -0,0 +1,29 @@ +const randomNumber = [ + { + label: 'Lower range', + key: 'lowerRange', + type: 'string', + required: true, + description: 'The lowest number to generate.', + variables: true, + }, + { + label: 'Upper range', + key: 'upperRange', + type: 'string', + required: true, + description: 'The highest number to generate.', + variables: true, + }, + { + label: 'Decimal points', + key: 'decimalPoints', + type: 'string', + required: false, + description: + 'The number of digits after the decimal point. It can be an integer between 0 and 15.', + variables: true, + }, +]; + +export default randomNumber; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/base64-to-string.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/base64-to-string.js new file mode 100644 index 0000000000000000000000000000000000000000..f96def87b522c576e666c89bde269a8a9cf112e3 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/base64-to-string.js @@ -0,0 +1,12 @@ +const base64ToString = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be converted from Base64 to string.', + variables: true, + }, +]; + +export default base64ToString; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/capitalize.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/capitalize.js new file mode 100644 index 0000000000000000000000000000000000000000..523d8b09686269c7b8044074b164be8e73e835ff --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/capitalize.js @@ -0,0 +1,12 @@ +const capitalize = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be capitalized.', + variables: true, + }, +]; + +export default capitalize; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri-component.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri-component.js new file mode 100644 index 0000000000000000000000000000000000000000..3d6a1833be26c1327c064d73fca2dbf389838e15 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri-component.js @@ -0,0 +1,12 @@ +const encodeUriComponent = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'URI Component to encode', + variables: true, + }, +]; + +export default encodeUriComponent; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri.js new file mode 100644 index 0000000000000000000000000000000000000000..6ee02fc614713f4e988da371f70efe9d20d1b63b --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/encode-uri.js @@ -0,0 +1,12 @@ +const encodeUri = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'URI to encode', + variables: true, + }, +]; + +export default encodeUri; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-email-address.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-email-address.js new file mode 100644 index 0000000000000000000000000000000000000000..9f0f5d822200039a39639abb46116de85a89346b --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-email-address.js @@ -0,0 +1,12 @@ +const extractEmailAddress = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be searched for an email address.', + variables: true, + }, +]; + +export default extractEmailAddress; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-number.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-number.js new file mode 100644 index 0000000000000000000000000000000000000000..2fe0ba636d2d84d73f4188afa652360fcc100ef7 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/extract-number.js @@ -0,0 +1,12 @@ +const extractNumber = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be searched for a number.', + variables: true, + }, +]; + +export default extractNumber; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/html-to-markdown.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/html-to-markdown.js new file mode 100644 index 0000000000000000000000000000000000000000..77b0ba9ea69766f88337a75aacc54c19f3829c54 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/html-to-markdown.js @@ -0,0 +1,12 @@ +const htmlToMarkdown = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'HTML that will be converted to Markdown.', + variables: true, + }, +]; + +export default htmlToMarkdown; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/lowercase.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/lowercase.js new file mode 100644 index 0000000000000000000000000000000000000000..d37eb2ecc955c444bbfced2ad51b84e101a378b4 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/lowercase.js @@ -0,0 +1,12 @@ +const lowercase = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be lowercased.', + variables: true, + }, +]; + +export default lowercase; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/markdown-to-html.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/markdown-to-html.js new file mode 100644 index 0000000000000000000000000000000000000000..ad17ed738e2b86bc9e2e662e03566f734749dda4 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/markdown-to-html.js @@ -0,0 +1,12 @@ +const markdownToHtml = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Markdown text that will be converted to HTML.', + variables: true, + }, +]; + +export default markdownToHtml; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/pluralize.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/pluralize.js new file mode 100644 index 0000000000000000000000000000000000000000..36e5df3a249dad46c23a47611325b1133c849f87 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/pluralize.js @@ -0,0 +1,12 @@ +const pluralize = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be pluralized.', + variables: true, + }, +]; + +export default pluralize; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/replace.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/replace.js new file mode 100644 index 0000000000000000000000000000000000000000..dfeb39ae92bf4170a08c33598d6f603eaf22253e --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/replace.js @@ -0,0 +1,28 @@ +const replace = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that you want to search for and replace values.', + variables: true, + }, + { + label: 'Find', + key: 'find', + type: 'string', + required: true, + description: 'Text that will be searched for.', + variables: true, + }, + { + label: 'Replace', + key: 'replace', + type: 'string', + required: false, + description: 'Text that will replace the found text.', + variables: true, + }, +]; + +export default replace; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/string-to-base64.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/string-to-base64.js new file mode 100644 index 0000000000000000000000000000000000000000..1fb5695117f86a013aecef915b1798de50dd1902 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/string-to-base64.js @@ -0,0 +1,12 @@ +const stringToBase64 = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text that will be converted to Base64.', + variables: true, + }, +]; + +export default stringToBase64; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/trim-whitespace.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/trim-whitespace.js new file mode 100644 index 0000000000000000000000000000000000000000..538cb26abd2c1331c14e307fbc759ce17dd19ae7 --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/trim-whitespace.js @@ -0,0 +1,12 @@ +const trimWhitespace = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text you want to remove leading and trailing spaces.', + variables: true, + }, +]; + +export default trimWhitespace; diff --git a/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/use-default-value.js b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/use-default-value.js new file mode 100644 index 0000000000000000000000000000000000000000..e2edc1f0ed4ccbabe7c90cd6ce6756268416e43a --- /dev/null +++ b/packages/backend/src/apps/formatter/dynamic-fields/list-transform-options/text/use-default-value.js @@ -0,0 +1,21 @@ +const useDefaultValue = [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'Text you want to check whether it is empty or not.', + variables: true, + }, + { + label: 'Default Value', + key: 'defaultValue', + type: 'string', + required: true, + description: + 'Text that will be used as a default value if the input is empty.', + variables: true, + }, +]; + +export default useDefaultValue; diff --git a/packages/backend/src/apps/formatter/index.js b/packages/backend/src/apps/formatter/index.js new file mode 100644 index 0000000000000000000000000000000000000000..96a6a39e6430e4784ec6f4cc586052123163935f --- /dev/null +++ b/packages/backend/src/apps/formatter/index.js @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; +import dynamicFields from './dynamic-fields/index.js'; + +export default defineApp({ + name: 'Formatter', + key: 'formatter', + iconUrl: '{BASE_URL}/apps/formatter/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/formatter/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '001F52', + actions, + dynamicFields, +}); diff --git a/packages/backend/src/apps/ghost/assets/favicon.svg b/packages/backend/src/apps/ghost/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e98fb6fd106a6f940e6dff55298e10744d52f1a2 --- /dev/null +++ b/packages/backend/src/apps/ghost/assets/favicon.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/packages/backend/src/apps/ghost/auth/index.js b/packages/backend/src/apps/ghost/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c9399c0ebacb2198db280886aefef001d7231c03 --- /dev/null +++ b/packages/backend/src/apps/ghost/auth/index.js @@ -0,0 +1,32 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'instanceUrl', + label: 'Instance URL', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'Admin API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/ghost/auth/is-still-verified.js b/packages/backend/src/apps/ghost/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/ghost/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/ghost/auth/verify-credentials.js b/packages/backend/src/apps/ghost/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..13e46b43e6430e11b9f5495accdc1181728c277d --- /dev/null +++ b/packages/backend/src/apps/ghost/auth/verify-credentials.js @@ -0,0 +1,14 @@ +const verifyCredentials = async ($) => { + const site = await $.http.get('/admin/site/'); + const screenName = [site.data.site.title, site.data.site.url] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + screenName, + }); + + await $.http.get('/admin/pages/'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/ghost/common/add-auth-header.js b/packages/backend/src/apps/ghost/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..60b65a2bc196c5135029e56da4a6d56dbdd9bc20 --- /dev/null +++ b/packages/backend/src/apps/ghost/common/add-auth-header.js @@ -0,0 +1,22 @@ +import jwt from 'jsonwebtoken'; + +const addAuthHeader = ($, requestConfig) => { + const key = $.auth.data?.apiKey; + + if (key) { + const [id, secret] = key.split(':'); + + const token = jwt.sign({}, Buffer.from(secret, 'hex'), { + keyid: id, + algorithm: 'HS256', + expiresIn: '1h', + audience: `/admin/`, + }); + + requestConfig.headers.Authorization = `Ghost ${token}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/ghost/common/set-base-url.js b/packages/backend/src/apps/ghost/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..a8f668dbe0616dbfafad43f75ba449214189d286 --- /dev/null +++ b/packages/backend/src/apps/ghost/common/set-base-url.js @@ -0,0 +1,10 @@ +const setBaseUrl = ($, requestConfig) => { + const instanceUrl = $.auth.data.instanceUrl; + if (instanceUrl) { + requestConfig.baseURL = `${instanceUrl}/ghost/api`; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/ghost/index.js b/packages/backend/src/apps/ghost/index.js new file mode 100644 index 0000000000000000000000000000000000000000..81019b932014bdf0cc9d04b8b4cf87e91393b71a --- /dev/null +++ b/packages/backend/src/apps/ghost/index.js @@ -0,0 +1,19 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Ghost', + key: 'ghost', + baseUrl: 'https://ghost.org', + apiBaseUrl: '', + iconUrl: '{BASE_URL}/apps/ghost/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/ghost/connection', + primaryColor: '15171A', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, +}); diff --git a/packages/backend/src/apps/ghost/triggers/index.js b/packages/backend/src/apps/ghost/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5505ef002337741616c02c55895fccd36662b7f9 --- /dev/null +++ b/packages/backend/src/apps/ghost/triggers/index.js @@ -0,0 +1,3 @@ +import newPostPublished from './new-post-published/index.js'; + +export default [newPostPublished]; diff --git a/packages/backend/src/apps/ghost/triggers/new-post-published/index.js b/packages/backend/src/apps/ghost/triggers/new-post-published/index.js new file mode 100644 index 0000000000000000000000000000000000000000..12ad9d9571b71b7c429b57d96ed9fc6b60896ad3 --- /dev/null +++ b/packages/backend/src/apps/ghost/triggers/new-post-published/index.js @@ -0,0 +1,55 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New post published', + key: 'newPostPublished', + type: 'webhook', + description: 'Triggers when a new post is published.', + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const payload = { + webhooks: [ + { + event: 'post.published', + target_url: $.webhookUrl, + name: `Flow ID: ${$.flow.id}`, + }, + ], + }; + + const response = await $.http.post('/admin/webhooks/', payload); + const id = response.data.webhooks[0].id; + + await $.flow.setRemoteWebhookId(id); + }, + + async unregisterHook($) { + await $.http.delete(`/admin/webhooks/${$.flow.remoteWebhookId}/`); + }, +}); diff --git a/packages/backend/src/apps/github/actions/create-issue/index.js b/packages/backend/src/apps/github/actions/create-issue/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f6d9704862ba9ff7c5883a2635d27807769ae86b --- /dev/null +++ b/packages/backend/src/apps/github/actions/create-issue/index.js @@ -0,0 +1,58 @@ +import defineAction from '../../../../helpers/define-action.js'; +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; + +export default defineAction({ + name: 'Create issue', + key: 'createIssue', + description: 'Creates a new issue.', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: false, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Body', + key: 'body', + type: 'string', + required: true, + variables: true, + }, + ], + + async run($) { + const repoParameter = $.step.parameters.repo; + const title = $.step.parameters.title; + const body = $.step.parameters.body; + + if (!repoParameter) throw new Error('A repo must be set!'); + if (!title) throw new Error('A title must be set!'); + + const { repoOwner, repo } = getRepoOwnerAndRepo(repoParameter); + const response = await $.http.post(`/repos/${repoOwner}/${repo}/issues`, { + title, + body, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/github/actions/index.js b/packages/backend/src/apps/github/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..095990d07f2e5fda93f62a12966635b08304893e --- /dev/null +++ b/packages/backend/src/apps/github/actions/index.js @@ -0,0 +1,3 @@ +import createIssue from './create-issue/index.js'; + +export default [createIssue]; diff --git a/packages/backend/src/apps/github/assets/favicon.svg b/packages/backend/src/apps/github/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..8a75650cbdebb3aa9210243e439769942a3cd895 --- /dev/null +++ b/packages/backend/src/apps/github/assets/favicon.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/github/auth/generate-auth-url.js b/packages/backend/src/apps/github/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..3b845369d77c86741f7f54ffb2a146aa1baa1d30 --- /dev/null +++ b/packages/backend/src/apps/github/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const scopes = ['read:org', 'repo', 'user']; + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.consumerKey, + redirect_uri: redirectUri, + scope: scopes.join(','), + }); + + const url = `${ + $.app.baseUrl + }/login/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/github/auth/index.js b/packages/backend/src/apps/github/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..59731eb212b213bddde29bcad5a8d238663d8e56 --- /dev/null +++ b/packages/backend/src/apps/github/auth/index.js @@ -0,0 +1,49 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/github/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Github OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/github#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/github#client-id', + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/github#client-secret', + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/github/auth/is-still-verified.js b/packages/backend/src/apps/github/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/github/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/github/auth/verify-credentials.js b/packages/backend/src/apps/github/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..b07549b6cafa75e084908a882982f45fe8fdd598 --- /dev/null +++ b/packages/backend/src/apps/github/auth/verify-credentials.js @@ -0,0 +1,35 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const response = await $.http.post( + `${$.app.baseUrl}/login/oauth/access_token`, + { + client_id: $.auth.data.consumerKey, + client_secret: $.auth.data.consumerSecret, + code: $.auth.data.code, + }, + { + headers: { + Accept: 'application/json', + }, + } + ); + + const data = response.data; + + $.auth.data.accessToken = data.access_token; + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + consumerKey: $.auth.data.consumerKey, + consumerSecret: $.auth.data.consumerSecret, + accessToken: data.access_token, + scope: data.scope, + tokenType: data.token_type, + userId: currentUser.id, + screenName: currentUser.login, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/github/common/add-auth-header.js b/packages/backend/src/apps/github/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..9d56bd1934e9af5c7323235f51c0dae236e8a0ff --- /dev/null +++ b/packages/backend/src/apps/github/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.headers && $.auth.data?.accessToken) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/github/common/get-current-user.js b/packages/backend/src/apps/github/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..f09b39831ba6f54db0c2f64fb1ede927a43d4537 --- /dev/null +++ b/packages/backend/src/apps/github/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/user'); + + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/github/common/get-repo-owner-and-repo.js b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.js new file mode 100644 index 0000000000000000000000000000000000000000..ee00b24449654643bfda5e26eca5929244596ebf --- /dev/null +++ b/packages/backend/src/apps/github/common/get-repo-owner-and-repo.js @@ -0,0 +1,10 @@ +export default function getRepoOwnerAndRepo(repoFullName) { + if (!repoFullName) return {}; + + const [repoOwner, repo] = repoFullName.split('/'); + + return { + repoOwner, + repo, + }; +} diff --git a/packages/backend/src/apps/github/common/paginate-all.js b/packages/backend/src/apps/github/common/paginate-all.js new file mode 100644 index 0000000000000000000000000000000000000000..e609b2d3e3bd41c00b5270540a5aa84e96049d76 --- /dev/null +++ b/packages/backend/src/apps/github/common/paginate-all.js @@ -0,0 +1,22 @@ +import parseLinkHeader from '../../../helpers/parse-header-link.js'; + +export default async function paginateAll($, request) { + const response = await request; + const aggregatedResponse = { + data: [...response.data], + }; + + let links = parseLinkHeader(response.headers.link); + + while (links.next) { + const nextPageResponse = await $.http.request({ + ...response.config, + url: links.next.uri, + }); + + aggregatedResponse.data.push(...nextPageResponse.data); + links = parseLinkHeader(nextPageResponse.headers.link); + } + + return aggregatedResponse; +} diff --git a/packages/backend/src/apps/github/dynamic-data/index.js b/packages/backend/src/apps/github/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..04afcbdd62b14e6a1621bb42388456a83cdbdd7e --- /dev/null +++ b/packages/backend/src/apps/github/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listLabels from './list-labels/index.js'; +import listRepos from './list-repos/index.js'; + +export default [listLabels, listRepos]; diff --git a/packages/backend/src/apps/github/dynamic-data/list-labels/index.js b/packages/backend/src/apps/github/dynamic-data/list-labels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d9ab38bd21125b83003325c3f50d6a9c91c9a31b --- /dev/null +++ b/packages/backend/src/apps/github/dynamic-data/list-labels/index.js @@ -0,0 +1,25 @@ +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; +import paginateAll from '../../common/paginate-all.js'; + +export default { + name: 'List labels', + key: 'listLabels', + + async run($) { + const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo); + + if (!repo) return { data: [] }; + + const firstPageRequest = $.http.get(`/repos/${repoOwner}/${repo}/labels`); + const response = await paginateAll($, firstPageRequest); + + response.data = response.data.map((repo) => { + return { + value: repo.name, + name: repo.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/github/dynamic-data/list-repos/index.js b/packages/backend/src/apps/github/dynamic-data/list-repos/index.js new file mode 100644 index 0000000000000000000000000000000000000000..06c5d477b9d16d4561b6ed82c730fbc8749ce8db --- /dev/null +++ b/packages/backend/src/apps/github/dynamic-data/list-repos/index.js @@ -0,0 +1,20 @@ +import paginateAll from '../../common/paginate-all.js'; + +export default { + name: 'List repos', + key: 'listRepos', + + async run($) { + const firstPageRequest = $.http.get('/user/repos'); + const response = await paginateAll($, firstPageRequest); + + response.data = response.data.map((repo) => { + return { + value: repo.full_name, + name: repo.full_name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/github/index.js b/packages/backend/src/apps/github/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c7dbb18f0892d2b7511c2da35ff4ea12e2c234dd --- /dev/null +++ b/packages/backend/src/apps/github/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'GitHub', + key: 'github', + baseUrl: 'https://github.com', + apiBaseUrl: 'https://api.github.com', + iconUrl: '{BASE_URL}/apps/github/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/github/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/github/triggers/index.js b/packages/backend/src/apps/github/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..62b86bf19622b162b14df0db5466aa015f665468 --- /dev/null +++ b/packages/backend/src/apps/github/triggers/index.js @@ -0,0 +1,6 @@ +import newIssues from './new-issues/index.js'; +import newPullRequests from './new-pull-requests/index.js'; +import newStargazers from './new-stargazers/index.js'; +import newWatchers from './new-watchers/index.js'; + +export default [newIssues, newPullRequests, newStargazers, newWatchers]; diff --git a/packages/backend/src/apps/github/triggers/new-issues/index.js b/packages/backend/src/apps/github/triggers/new-issues/index.js new file mode 100644 index 0000000000000000000000000000000000000000..699f8894f6833d0675735d4388579ba9d7b1c00b --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-issues/index.js @@ -0,0 +1,86 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newIssues from './new-issues.js'; + +export default defineTrigger({ + name: 'New issues', + key: 'newIssues', + pollInterval: 15, + description: 'Triggers when a new issue is created', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + }, + }, + { + label: 'Which types of issues should this trigger on?', + key: 'issueType', + type: 'dropdown', + description: 'Defaults to any issue you can see.', + required: true, + variables: false, + value: 'all', + options: [ + { + label: 'Any issue you can see', + value: 'all', + }, + { + label: 'Only issues assigned to you', + value: 'assigned', + }, + { + label: 'Only issues created by you', + value: 'created', + }, + { + label: `Only issues you're mentioned in`, + value: 'mentioned', + }, + { + label: `Only issues you're subscribed to`, + value: 'subscribed', + }, + ], + }, + { + label: 'Label', + key: 'label', + type: 'dropdown', + description: 'Only trigger on issues when this label is added.', + required: false, + variables: false, + dependsOn: ['parameters.repo'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLabels', + }, + { + name: 'parameters.repo', + value: '{parameters.repo}', + }, + ], + }, + }, + ], + + async run($) { + await newIssues($); + }, +}); diff --git a/packages/backend/src/apps/github/triggers/new-issues/new-issues.js b/packages/backend/src/apps/github/triggers/new-issues/new-issues.js new file mode 100644 index 0000000000000000000000000000000000000000..ee5c17d273af06618035f807e714fc18c547a589 --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-issues/new-issues.js @@ -0,0 +1,47 @@ +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; +import parseLinkHeader from '../../../../helpers/parse-header-link.js'; + +function getPathname($) { + const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo); + + if (repoOwner && repo) { + return `/repos/${repoOwner}/${repo}/issues`; + } + + return '/issues'; +} + +const newIssues = async ($) => { + const pathname = getPathname($); + const params = { + labels: $.step.parameters.label, + filter: 'all', + state: 'all', + sort: 'created', + direction: 'desc', + per_page: 100, + }; + + let links; + do { + const response = await $.http.get(pathname, { params }); + links = parseLinkHeader(response.headers.link); + + if (response.data.length) { + for (const issue of response.data) { + const issueId = issue.id; + + const dataItem = { + raw: issue, + meta: { + internalId: issueId.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (links.next); +}; + +export default newIssues; diff --git a/packages/backend/src/apps/github/triggers/new-pull-requests/index.js b/packages/backend/src/apps/github/triggers/new-pull-requests/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7b7f4b022bf057020170ff9604e6ed16732d8ebc --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-pull-requests/index.js @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newPullRequests from './new-pull-requests.js'; + +export default defineTrigger({ + name: 'New pull requests', + key: 'newPullRequests', + pollInterval: 15, + description: 'Triggers when a new pull request is created', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: true, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + }, + }, + ], + + async run($) { + await newPullRequests($); + }, +}); diff --git a/packages/backend/src/apps/github/triggers/new-pull-requests/new-pull-requests.js b/packages/backend/src/apps/github/triggers/new-pull-requests/new-pull-requests.js new file mode 100644 index 0000000000000000000000000000000000000000..bbd6d8e9c6e2052de7f2c984163a3a5b9471119d --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-pull-requests/new-pull-requests.js @@ -0,0 +1,41 @@ +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; +import parseLinkHeader from '../../../../helpers/parse-header-link.js'; + +const newPullRequests = async ($) => { + const repoParameter = $.step.parameters.repo; + + if (!repoParameter) throw new Error('A repo must be set!'); + + const { repoOwner, repo } = getRepoOwnerAndRepo(repoParameter); + + const pathname = `/repos/${repoOwner}/${repo}/pulls`; + const params = { + state: 'all', + sort: 'created', + direction: 'desc', + per_page: 100, + }; + + let links; + do { + const response = await $.http.get(pathname, { params }); + links = parseLinkHeader(response.headers.link); + + if (response.data.length) { + for (const pullRequest of response.data) { + const pullRequestId = pullRequest.id; + + const dataItem = { + raw: pullRequest, + meta: { + internalId: pullRequestId.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (links.next); +}; + +export default newPullRequests; diff --git a/packages/backend/src/apps/github/triggers/new-stargazers/index.js b/packages/backend/src/apps/github/triggers/new-stargazers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0953e557e09648c24d5a2dba96d8eb14d7d70b65 --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-stargazers/index.js @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newStargazers from './new-stargazers.js'; + +export default defineTrigger({ + name: 'New stargazers', + key: 'newStargazers', + pollInterval: 15, + description: 'Triggers when a user stars a repository', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: true, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + }, + }, + ], + + async run($) { + await newStargazers($); + }, +}); diff --git a/packages/backend/src/apps/github/triggers/new-stargazers/new-stargazers.js b/packages/backend/src/apps/github/triggers/new-stargazers/new-stargazers.js new file mode 100644 index 0000000000000000000000000000000000000000..f83e0dd27fd312399a676579ee0a8a053180cb0d --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-stargazers/new-stargazers.js @@ -0,0 +1,51 @@ +import { DateTime } from 'luxon'; + +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; +import parseLinkHeader from '../../../../helpers/parse-header-link.js'; + +const newStargazers = async ($) => { + const { repoOwner, repo } = getRepoOwnerAndRepo($.step.parameters.repo); + const firstPagePathname = `/repos/${repoOwner}/${repo}/stargazers`; + const requestConfig = { + params: { + per_page: 100, + }, + headers: { + // needed to get `starred_at` time + Accept: 'application/vnd.github.star+json', + }, + }; + + const firstPageResponse = await $.http.get(firstPagePathname, requestConfig); + const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link); + + // in case there is only single page to fetch + let pathname = firstPageLinks.last?.uri || firstPagePathname; + + do { + const response = await $.http.get(pathname, requestConfig); + const links = parseLinkHeader(response.headers.link); + pathname = links.prev?.uri; + + if (response.data.length) { + // to iterate reverse-chronologically + response.data.reverse(); + + for (const starEntry of response.data) { + const { starred_at, user } = starEntry; + const timestamp = DateTime.fromISO(starred_at).toMillis(); + + const dataItem = { + raw: user, + meta: { + internalId: timestamp.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (pathname); +}; + +export default newStargazers; diff --git a/packages/backend/src/apps/github/triggers/new-watchers/index.js b/packages/backend/src/apps/github/triggers/new-watchers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..62602e946450371a456130d6e63dddb4f7db100c --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-watchers/index.js @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newWatchers from './new-watchers.js'; + +export default defineTrigger({ + name: 'New watchers', + key: 'newWatchers', + pollInterval: 15, + description: 'Triggers when a user watches a repository', + arguments: [ + { + label: 'Repo', + key: 'repo', + type: 'dropdown', + required: true, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRepos', + }, + ], + }, + }, + ], + + async run($) { + await newWatchers($); + }, +}); diff --git a/packages/backend/src/apps/github/triggers/new-watchers/new-watchers.js b/packages/backend/src/apps/github/triggers/new-watchers/new-watchers.js new file mode 100644 index 0000000000000000000000000000000000000000..3ccfba4314ef474fae214b7faa7c559abcbeae3b --- /dev/null +++ b/packages/backend/src/apps/github/triggers/new-watchers/new-watchers.js @@ -0,0 +1,49 @@ +import getRepoOwnerAndRepo from '../../common/get-repo-owner-and-repo.js'; +import parseLinkHeader from '../../../../helpers/parse-header-link.js'; + +const newWatchers = async ($) => { + const repoParameter = $.step.parameters.repo; + + if (!repoParameter) throw new Error('A repo must be set!'); + + const { repoOwner, repo } = getRepoOwnerAndRepo(repoParameter); + + const firstPagePathname = `/repos/${repoOwner}/${repo}/subscribers`; + const requestConfig = { + params: { + per_page: 100, + }, + }; + + const firstPageResponse = await $.http.get(firstPagePathname, requestConfig); + const firstPageLinks = parseLinkHeader(firstPageResponse.headers.link); + + // in case there is only single page to fetch + let pathname = firstPageLinks.last?.uri || firstPagePathname; + + do { + const response = await $.http.get(pathname, requestConfig); + const links = parseLinkHeader(response.headers.link); + pathname = links.prev?.uri; + + if (response.data.length) { + // to iterate reverse-chronologically + response.data.reverse(); + + for (const watcher of response.data) { + const watcherId = watcher.id.toString(); + + const dataItem = { + raw: watcher, + meta: { + internalId: watcherId, + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (pathname); +}; + +export default newWatchers; diff --git a/packages/backend/src/apps/gitlab/assets/favicon.svg b/packages/backend/src/apps/gitlab/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..b1108696e4f99378761b48de6a63ad2860be8529 --- /dev/null +++ b/packages/backend/src/apps/gitlab/assets/favicon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/backend/src/apps/gitlab/auth/generate-auth-url.js b/packages/backend/src/apps/gitlab/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..f3a7fb7ba3ba0c75ee464dc4ed8f1fe1271f140d --- /dev/null +++ b/packages/backend/src/apps/gitlab/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URL, URLSearchParams } from 'url'; +import getBaseUrl from '../common/get-base-url.js'; + +export default async function generateAuthUrl($) { + // ref: https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + + const scopes = ['api', 'read_user']; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: $.auth.data.oAuthRedirectUrl, + scope: scopes.join(' '), + response_type: 'code', + state: Date.now().toString(), + }); + + const baseUrl = getBaseUrl($); + const path = `/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url: new URL(path, baseUrl).toString(), + }); +} diff --git a/packages/backend/src/apps/gitlab/auth/index.js b/packages/backend/src/apps/gitlab/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f975699756ee2c0ff8206b4ede457e610c49417b --- /dev/null +++ b/packages/backend/src/apps/gitlab/auth/index.js @@ -0,0 +1,63 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/gitlab/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Gitlab OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/gitlab#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'instanceUrl', + label: 'Gitlab instance URL', + type: 'string', + required: false, + readOnly: false, + value: 'https://gitlab.com', + placeholder: 'https://gitlab.com', + description: 'Your Gitlab instance URL. Default is https://gitlab.com.', + docUrl: 'https://automatisch.io/docs/gitlab#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/gitlab#client-id', + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/gitlab#client-secret', + clickToCopy: false, + }, + ], + + generateAuthUrl, + refreshToken, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/gitlab/auth/is-still-verified.js b/packages/backend/src/apps/gitlab/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/gitlab/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/gitlab/auth/refresh-token.js b/packages/backend/src/apps/gitlab/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..09b74b49af8515de26b6b6fce529e4ca40cfa750 --- /dev/null +++ b/packages/backend/src/apps/gitlab/auth/refresh-token.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; + +const refreshToken = async ($) => { + // ref: https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post('/oauth/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + tokenType: data.token_type, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/gitlab/auth/verify-credentials.js b/packages/backend/src/apps/gitlab/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..02e794f7954a182794a2a6bd1a71ef6bf30e14f0 --- /dev/null +++ b/packages/backend/src/apps/gitlab/auth/verify-credentials.js @@ -0,0 +1,43 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + // ref: https://docs.gitlab.com/ee/api/oauth2.html#authorization-code-flow + + const response = await $.http.post( + '/oauth/token', + { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: $.auth.data.oAuthRedirectUrl, + }, + { + headers: { + Accept: 'application/json', + }, + } + ); + + const data = response.data; + + $.auth.data.accessToken = data.access_token; + + const currentUser = await getCurrentUser($); + const screenName = [currentUser.username, $.auth.data.instanceUrl] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: data.access_token, + refreshToken: data.refresh_token, + scope: data.scope, + tokenType: data.token_type, + userId: currentUser.id, + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/gitlab/common/add-auth-header.js b/packages/backend/src/apps/gitlab/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..150b2b4ea595c3ce66a18a8d3820d0a5010a72b3 --- /dev/null +++ b/packages/backend/src/apps/gitlab/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers = requestConfig.headers || {}; + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/gitlab/common/get-base-url.js b/packages/backend/src/apps/gitlab/common/get-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..44e82c47b5296f6071a6e182fdae64f5fe7c724e --- /dev/null +++ b/packages/backend/src/apps/gitlab/common/get-base-url.js @@ -0,0 +1,13 @@ +const getBaseUrl = ($) => { + if ($.auth.data.instanceUrl) { + return $.auth.data.instanceUrl; + } + + if ($.app.apiBaseUrl) { + return $.app.apiBaseUrl; + } + + return $.app.baseUrl; +}; + +export default getBaseUrl; diff --git a/packages/backend/src/apps/gitlab/common/get-current-user.js b/packages/backend/src/apps/gitlab/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..f464d6a629195781db827c0bb22375fa353aa824 --- /dev/null +++ b/packages/backend/src/apps/gitlab/common/get-current-user.js @@ -0,0 +1,9 @@ +const getCurrentUser = async ($) => { + // ref: https://docs.gitlab.com/ee/api/users.html#list-current-user + + const response = await $.http.get('/api/v4/user'); + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/gitlab/common/paginate-all.js b/packages/backend/src/apps/gitlab/common/paginate-all.js new file mode 100644 index 0000000000000000000000000000000000000000..060aa2f036eed1a41c99cd21766c5ea9123847d0 --- /dev/null +++ b/packages/backend/src/apps/gitlab/common/paginate-all.js @@ -0,0 +1,23 @@ +import parseLinkHeader from '../../../helpers/parse-header-link.js'; + +export default async function paginateAll($, request) { + const response = await request; + + const aggregatedResponse = { + data: [...response.data], + }; + + let links = parseLinkHeader(response.headers.link); + + while (links.next) { + const nextPageResponse = await $.http.request({ + ...response.config, + url: links.next.uri, + }); + + aggregatedResponse.data.push(...nextPageResponse.data); + links = parseLinkHeader(nextPageResponse.headers.link); + } + + return aggregatedResponse; +} diff --git a/packages/backend/src/apps/gitlab/common/set-base-url.js b/packages/backend/src/apps/gitlab/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..135149b1ad4ffed1d24996b6f531ba1014a9fa56 --- /dev/null +++ b/packages/backend/src/apps/gitlab/common/set-base-url.js @@ -0,0 +1,11 @@ +const setBaseUrl = ($, requestConfig) => { + if ($.auth.data.instanceUrl) { + requestConfig.baseURL = $.auth.data.instanceUrl; + } else if ($.app.apiBaseUrl) { + requestConfig.baseURL = $.app.apiBaseUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/gitlab/dynamic-data/index.js b/packages/backend/src/apps/gitlab/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ed07bb054ee1749534ed133f7e567603ef7c8f55 --- /dev/null +++ b/packages/backend/src/apps/gitlab/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listProjects from './list-projects/index.js'; + +export default [listProjects]; diff --git a/packages/backend/src/apps/gitlab/dynamic-data/list-projects/index.js b/packages/backend/src/apps/gitlab/dynamic-data/list-projects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..522f7adf4505c44cd3662dcbbdd9d1f7f7b5b1ca --- /dev/null +++ b/packages/backend/src/apps/gitlab/dynamic-data/list-projects/index.js @@ -0,0 +1,32 @@ +import paginateAll from '../../common/paginate-all.js'; + +export default { + name: 'List projects', + key: 'listProjects', + + async run($) { + // ref: + // - https://docs.gitlab.com/ee/api/projects.html#list-all-projects + // - https://docs.gitlab.com/ee/api/rest/index.html#keyset-based-pagination + const firstPageRequest = $.http.get('/api/v4/projects', { + params: { + simple: true, + pagination: 'keyset', + membership: true, + order_by: 'id', + sort: 'asc', + }, + }); + + const response = await paginateAll($, firstPageRequest); + + response.data = response.data.map((repo) => { + return { + value: repo.id, + name: repo.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/gitlab/index.js b/packages/backend/src/apps/gitlab/index.js new file mode 100644 index 0000000000000000000000000000000000000000..793da675fe0e88b3a372baa199d416fcaa3f5242 --- /dev/null +++ b/packages/backend/src/apps/gitlab/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'GitLab', + key: 'gitlab', + baseUrl: 'https://gitlab.com', + apiBaseUrl: 'https://gitlab.com', + iconUrl: '{BASE_URL}/apps/gitlab/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/gitlab/connection', + primaryColor: 'FC6D26', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/index.js b/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ec94b3206961c3a4057510c1fd6ffdf36f637b6b --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/index.js @@ -0,0 +1,28 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +// confidential_issues_events has the same event data as issues_events +import data from './issue_event.js'; + +export const triggerDescriptor = { + name: 'Confidential issue event', + description: + 'Confidential issue event (triggered when a new confidential issue is created or an existing issue is updated, closed, or reopened)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#issue-events', + key: GITLAB_EVENT_TYPE.confidential_issues_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.confidential_issues_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/issue_event.js b/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/issue_event.js new file mode 100644 index 0000000000000000000000000000000000000000..75a243b3da2c8df47d8b2ba3e8ec32ca4ed3ae34 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/confidential-issue-event/issue_event.js @@ -0,0 +1,159 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#issue-events + +export default { + object_kind: 'issue', + event_type: 'issue', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + email: 'admin@example.com', + }, + project: { + id: 1, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + ci_config_path: null, + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + object_attributes: { + id: 301, + title: 'New API: create/update/delete file', + assignee_ids: [51], + assignee_id: 51, + author_id: 51, + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + updated_by_id: 1, + last_edited_at: null, + last_edited_by_id: null, + relative_position: 0, + description: 'Create new API for manipulations with repository', + milestone_id: null, + state_id: 1, + confidential: false, + discussion_locked: true, + due_date: null, + moved_to_id: null, + duplicated_to_id: null, + time_estimate: 0, + total_time_spent: 0, + time_change: 0, + human_total_time_spent: null, + human_time_estimate: null, + human_time_change: null, + weight: null, + iid: 23, + url: 'http://example.com/diaspora/issues/23', + state: 'opened', + action: 'open', + severity: 'high', + escalation_status: 'triggered', + escalation_policy: { + id: 18, + name: 'Engineering On-call', + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + }, + repository: { + name: 'Gitlab Test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + description: 'Aut reprehenderit ut est.', + homepage: 'http://example.com/gitlabhq/gitlab-test', + }, + assignees: [ + { + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + ], + assignee: { + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + changes: { + updated_by_id: { + previous: null, + current: 1, + }, + updated_at: { + previous: '2017-09-15 16:50:55 UTC', + current: '2017-09-15 16:52:00 UTC', + }, + labels: { + previous: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + current: [ + { + id: 205, + title: 'Platform', + color: '#123123', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'Platform related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + }, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/confidential-note-event/index.js b/packages/backend/src/apps/gitlab/triggers/confidential-note-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4e4fbd9db5f7eaa704811d291a27dd106c34f189 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/confidential-note-event/index.js @@ -0,0 +1,28 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +// confidential_note_events has the same event data as note_events +import data from './note_event.js'; + +export const triggerDescriptor = { + name: 'Confidential comment event', + description: + 'Confidential comment event (triggered when a new confidential comment is made on commits, merge requests, issues, and code snippets)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#comment-events', + key: GITLAB_EVENT_TYPE.confidential_note_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.confidential_note_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/confidential-note-event/note_event.js b/packages/backend/src/apps/gitlab/triggers/confidential-note-event/note_event.js new file mode 100644 index 0000000000000000000000000000000000000000..593188f2e4055250e9e2040135dfa1fefdc311b2 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/confidential-note-event/note_event.js @@ -0,0 +1,74 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#comment-events + +export default { + object_kind: 'note', + event_type: 'note', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + email: 'admin@example.com', + }, + project_id: 5, + project: { + id: 5, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + repository: { + name: 'Gitlab Test', + url: 'http://example.com/gitlab-org/gitlab-test.git', + description: 'Aut reprehenderit ut est.', + homepage: 'http://example.com/gitlab-org/gitlab-test', + }, + object_attributes: { + id: 1243, + note: 'This is a commit comment. How does this work?', + noteable_type: 'Commit', + author_id: 1, + created_at: '2015-05-17 18:08:09 UTC', + updated_at: '2015-05-17 18:08:09 UTC', + project_id: 5, + attachment: null, + line_code: 'bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1', + commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + noteable_id: null, + system: false, + st_diff: { + diff: '--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n', + new_path: 'six', + old_path: 'six', + a_mode: '0', + b_mode: '160000', + new_file: true, + renamed_file: false, + deleted_file: false, + }, + url: 'http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243', + }, + commit: { + id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + message: + 'Add submodule\n\nSigned-off-by: Example User \u003cuser@example.com.com\u003e\n', + timestamp: '2014-02-27T10:06:20+02:00', + url: 'http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + author: { + name: 'Example User', + email: 'user@example.com', + }, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/deployment-event/deployment_event.js b/packages/backend/src/apps/gitlab/triggers/deployment-event/deployment_event.js new file mode 100644 index 0000000000000000000000000000000000000000..0c2d2858002a9abd1fb55cae8476ca85dc6b6456 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/deployment-event/deployment_event.js @@ -0,0 +1,45 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#deployment-events + +export default { + object_kind: 'deployment', + status: 'success', + status_changed_at: '2021-04-28 21:50:00 +0200', + deployment_id: 15, + deployable_id: 796, + deployable_url: + 'http://10.126.0.2:3000/root/test-deployment-webhooks/-/jobs/796', + environment: 'staging', + environment_slug: 'staging', + environment_external_url: 'https://staging.example.com', + project: { + id: 30, + name: 'test-deployment-webhooks', + description: '', + web_url: 'http://10.126.0.2:3000/root/test-deployment-webhooks', + avatar_url: null, + git_ssh_url: 'ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git', + git_http_url: 'http://10.126.0.2:3000/root/test-deployment-webhooks.git', + namespace: 'Administrator', + visibility_level: 0, + path_with_namespace: 'root/test-deployment-webhooks', + default_branch: 'master', + ci_config_path: '', + homepage: 'http://10.126.0.2:3000/root/test-deployment-webhooks', + url: 'ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git', + ssh_url: 'ssh://vlad@10.126.0.2:2222/root/test-deployment-webhooks.git', + http_url: 'http://10.126.0.2:3000/root/test-deployment-webhooks.git', + }, + short_sha: '279484c0', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + email: 'admin@example.com', + }, + user_url: 'http://10.126.0.2:3000/root', + commit_url: + 'http://10.126.0.2:3000/root/test-deployment-webhooks/-/commit/279484c09fbe69ededfced8c1bb6e6d24616b468', + commit_title: 'Add new file', +}; diff --git a/packages/backend/src/apps/gitlab/triggers/deployment-event/index.js b/packages/backend/src/apps/gitlab/triggers/deployment-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ecf122c3656c5d04c13601de5d5f4a0a3c53d8d7 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/deployment-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './deployment_event.js'; + +export const triggerDescriptor = { + name: 'Deployment event', + description: + 'Deployment event (triggered when a deployment starts, succeeds, fails or is canceled)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#deployment-events', + key: GITLAB_EVENT_TYPE.deployment_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.deployment_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/feature-flag-event/feature_flag_event.js b/packages/backend/src/apps/gitlab/triggers/feature-flag-event/feature_flag_event.js new file mode 100644 index 0000000000000000000000000000000000000000..bff890889c53c25557c0aa7338acf017cfe98a48 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/feature-flag-event/feature_flag_event.js @@ -0,0 +1,38 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#feature-flag-events + +export default { + object_kind: 'feature_flag', + project: { + id: 1, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + ci_config_path: null, + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + email: 'admin@example.com', + }, + user_url: 'http://example.com/root', + object_attributes: { + id: 6, + name: 'test-feature-flag', + description: 'test-feature-flag-description', + active: true, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/feature-flag-event/index.js b/packages/backend/src/apps/gitlab/triggers/feature-flag-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5c2ea03736e3f491d0cae09d73987a50be43ca06 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/feature-flag-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './feature_flag_event.js'; + +export const triggerDescriptor = { + name: 'Feature flag event', + description: + 'Feature flag event (triggered when a feature flag is turned on or off)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#feature-flag-events', + key: GITLAB_EVENT_TYPE.feature_flag_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.feature_flag_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/index.js b/packages/backend/src/apps/gitlab/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..18ffd7d579cd1bdab493d6da99be7a76af5e41d9 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/index.js @@ -0,0 +1,29 @@ +import confidentialIssueEvent from './confidential-issue-event/index.js'; +import confidentialNoteEvent from './confidential-note-event/index.js'; +import deploymentEvent from './deployment-event/index.js'; +import featureFlagEvent from './feature-flag-event/index.js'; +import issueEvent from './issue-event/index.js'; +import jobEvent from './job-event/index.js'; +import mergeRequestEvent from './merge-request-event/index.js'; +import noteEvent from './note-event/index.js'; +import pipelineEvent from './pipeline-event/index.js'; +import pushEvent from './push-event/index.js'; +import releaseEvent from './release-event/index.js'; +import tagPushEvent from './tag-push-event/index.js'; +import wikiPageEvent from './wiki-page-event/index.js'; + +export default [ + confidentialIssueEvent, + confidentialNoteEvent, + deploymentEvent, + featureFlagEvent, + issueEvent, + jobEvent, + mergeRequestEvent, + noteEvent, + pipelineEvent, + pushEvent, + releaseEvent, + tagPushEvent, + wikiPageEvent, +]; diff --git a/packages/backend/src/apps/gitlab/triggers/issue-event/index.js b/packages/backend/src/apps/gitlab/triggers/issue-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a273df9bc896042c5b9ccc37f87ebf3be1a07f19 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/issue-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './issue_event.js'; + +export const triggerDescriptor = { + name: 'Issue event', + description: + 'Issue event (triggered when a new issue is created or an existing issue is updated, closed, or reopened)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#issue-events', + key: GITLAB_EVENT_TYPE.issues_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.issues_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/issue-event/issue_event.js b/packages/backend/src/apps/gitlab/triggers/issue-event/issue_event.js new file mode 100644 index 0000000000000000000000000000000000000000..75a243b3da2c8df47d8b2ba3e8ec32ca4ed3ae34 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/issue-event/issue_event.js @@ -0,0 +1,159 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#issue-events + +export default { + object_kind: 'issue', + event_type: 'issue', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + email: 'admin@example.com', + }, + project: { + id: 1, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + ci_config_path: null, + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + object_attributes: { + id: 301, + title: 'New API: create/update/delete file', + assignee_ids: [51], + assignee_id: 51, + author_id: 51, + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + updated_by_id: 1, + last_edited_at: null, + last_edited_by_id: null, + relative_position: 0, + description: 'Create new API for manipulations with repository', + milestone_id: null, + state_id: 1, + confidential: false, + discussion_locked: true, + due_date: null, + moved_to_id: null, + duplicated_to_id: null, + time_estimate: 0, + total_time_spent: 0, + time_change: 0, + human_total_time_spent: null, + human_time_estimate: null, + human_time_change: null, + weight: null, + iid: 23, + url: 'http://example.com/diaspora/issues/23', + state: 'opened', + action: 'open', + severity: 'high', + escalation_status: 'triggered', + escalation_policy: { + id: 18, + name: 'Engineering On-call', + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + }, + repository: { + name: 'Gitlab Test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + description: 'Aut reprehenderit ut est.', + homepage: 'http://example.com/gitlabhq/gitlab-test', + }, + assignees: [ + { + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + ], + assignee: { + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + changes: { + updated_by_id: { + previous: null, + current: 1, + }, + updated_at: { + previous: '2017-09-15 16:50:55 UTC', + current: '2017-09-15 16:52:00 UTC', + }, + labels: { + previous: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + current: [ + { + id: 205, + title: 'Platform', + color: '#123123', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'Platform related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + }, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/job-event/index.js b/packages/backend/src/apps/gitlab/triggers/job-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f7c4f5af45a5e42e612954972b24c022a90a3da1 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/job-event/index.js @@ -0,0 +1,26 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './job_event.js'; + +export const triggerDescriptor = { + name: 'Job event', + description: 'Job event (triggered when the status of a job changes)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#job-events', + key: GITLAB_EVENT_TYPE.job_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.job_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/job-event/job_event.js b/packages/backend/src/apps/gitlab/triggers/job-event/job_event.js new file mode 100644 index 0000000000000000000000000000000000000000..bc23866d24c867ddaac9b6b3fa9d81f719443882 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/job-event/job_event.js @@ -0,0 +1,60 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#job-events + +export default { + object_kind: 'build', + ref: 'gitlab-script-trigger', + tag: false, + before_sha: '2293ada6b400935a1378653304eaf6221e0fdb8f', + sha: '2293ada6b400935a1378653304eaf6221e0fdb8f', + build_id: 1977, + build_name: 'test', + build_stage: 'test', + build_status: 'created', + build_created_at: '2021-02-23T02:41:37.886Z', + build_started_at: null, + build_finished_at: null, + build_duration: null, + build_queued_duration: 1095.588715, // duration in seconds + build_allow_failure: false, + build_failure_reason: 'script_failure', + retries_count: 2, // the second retry of this job + pipeline_id: 2366, + project_id: 380, + project_name: 'gitlab-org/gitlab-test', + user: { + id: 3, + name: 'User', + email: 'user@gitlab.com', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + }, + commit: { + id: 2366, + name: 'Build pipeline', + sha: '2293ada6b400935a1378653304eaf6221e0fdb8f', + message: 'test\n', + author_name: 'User', + author_email: 'user@gitlab.com', + status: 'created', + duration: null, + started_at: null, + finished_at: null, + }, + repository: { + name: 'gitlab_test', + description: 'Atque in sunt eos similique dolores voluptatem.', + homepage: 'http://192.168.64.1:3005/gitlab-org/gitlab-test', + git_ssh_url: 'git@192.168.64.1:gitlab-org/gitlab-test.git', + git_http_url: 'http://192.168.64.1:3005/gitlab-org/gitlab-test.git', + visibility_level: 20, + }, + runner: { + active: true, + runner_type: 'project_type', + is_shared: false, + id: 380987, + description: 'shared-runners-manager-6.gitlab.com', + tags: ['linux', 'docker'], + }, + environment: null, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/lib.js b/packages/backend/src/apps/gitlab/triggers/lib.js new file mode 100644 index 0000000000000000000000000000000000000000..1203722e5a496b187da7e7058b848c58053466e4 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/lib.js @@ -0,0 +1,94 @@ +import Crypto from 'crypto'; +import appConfig from '../../../config/app.js'; + +export const projectArgumentDescriptor = { + label: 'Project', + key: 'projectId', + type: 'dropdown', + required: true, + description: 'Pick a project to receive events from', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, +}; + +export const getRunFn = async ($) => { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); +}; + +export const getTestRunFn = (eventData) => ($) => { + /* + Not fetching actual events from gitlab and using static event data from documentation + as there is no way to filter out events of one category using gitlab event types, + filtering is very limited and uses different grouping than what is applicable when creating a webhook. + + ref: + - https://docs.gitlab.com/ee/api/events.html#target-types + - https://docs.gitlab.com/ee/api/projects.html#add-project-hook + */ + + if (!eventData) { + return; + } + + const dataItem = { + raw: eventData, + meta: { + // there is no distinct id on gitlab event object thus creating it + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + + return Promise.resolve(); +}; + +export const getRegisterHookFn = (eventType) => async ($) => { + // ref: https://docs.gitlab.com/ee/api/projects.html#add-project-hook + + const subscriptionPayload = { + url: $.webhookUrl, + token: appConfig.webhookSecretKey, + enable_ssl_verification: true, + [eventType]: true, + }; + + if ( + ['wildcard', 'regex'].includes($.step.parameters.branch_filter_strategy) + ) { + subscriptionPayload.branch_filter_strategy = + $.step.parameters.branch_filter_strategy; + subscriptionPayload.push_events_branch_filter = + $.step.parameters.push_events_branch_filter; + } + + const { data } = await $.http.post( + `/api/v4/projects/${$.step.parameters.projectId}/hooks`, + subscriptionPayload + ); + + await $.flow.setRemoteWebhookId(data.id.toString()); +}; + +export const unregisterHook = async ($) => { + // ref: https://docs.gitlab.com/ee/api/projects.html#delete-project-hook + await $.http.delete( + `/api/v4/projects/${$.step.parameters.projectId}/hooks/${$.flow.remoteWebhookId}` + ); +}; diff --git a/packages/backend/src/apps/gitlab/triggers/merge-request-event/index.js b/packages/backend/src/apps/gitlab/triggers/merge-request-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c3ec583d2d3327fa1efac1a61c89c2c673b5dce2 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/merge-request-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './merge_request_event.js'; + +export const triggerDescriptor = { + name: 'Merge request event', + description: + 'Merge request event (triggered when merge request is created, updated, or closed)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#merge-request-events', + key: GITLAB_EVENT_TYPE.merge_requests_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.merge_requests_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/merge-request-event/merge_request_event.js b/packages/backend/src/apps/gitlab/triggers/merge-request-event/merge_request_event.js new file mode 100644 index 0000000000000000000000000000000000000000..e0f3f48faeb5d133295d1ebe2e490d6e28e59dbf --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/merge-request-event/merge_request_event.js @@ -0,0 +1,208 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#merge-request-events + +export default { + object_kind: 'merge_request', + event_type: 'merge_request', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + email: 'admin@example.com', + }, + project: { + id: 1, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + ci_config_path: '', + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + repository: { + name: 'Gitlab Test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + description: 'Aut reprehenderit ut est.', + homepage: 'http://example.com/gitlabhq/gitlab-test', + }, + object_attributes: { + id: 99, + iid: 1, + target_branch: 'master', + source_branch: 'ms-viewport', + source_project_id: 14, + author_id: 51, + assignee_ids: [6], + assignee_id: 6, + reviewer_ids: [6], + title: 'MS-Viewport', + created_at: '2013-12-03T17:23:34Z', + updated_at: '2013-12-03T17:23:34Z', + last_edited_at: '2013-12-03T17:23:34Z', + last_edited_by_id: 1, + milestone_id: null, + state_id: 1, + state: 'opened', + blocking_discussions_resolved: true, + work_in_progress: false, + first_contribution: true, + merge_status: 'unchecked', + target_project_id: 14, + description: '', + total_time_spent: 1800, + time_change: 30, + human_total_time_spent: '30m', + human_time_change: '30s', + human_time_estimate: '30m', + url: 'http://example.com/diaspora/merge_requests/1', + source: { + name: 'Awesome Project', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/awesome_space/awesome_project', + avatar_url: null, + git_ssh_url: 'git@example.com:awesome_space/awesome_project.git', + git_http_url: 'http://example.com/awesome_space/awesome_project.git', + namespace: 'Awesome Space', + visibility_level: 20, + path_with_namespace: 'awesome_space/awesome_project', + default_branch: 'master', + homepage: 'http://example.com/awesome_space/awesome_project', + url: 'http://example.com/awesome_space/awesome_project.git', + ssh_url: 'git@example.com:awesome_space/awesome_project.git', + http_url: 'http://example.com/awesome_space/awesome_project.git', + }, + target: { + name: 'Awesome Project', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/awesome_space/awesome_project', + avatar_url: null, + git_ssh_url: 'git@example.com:awesome_space/awesome_project.git', + git_http_url: 'http://example.com/awesome_space/awesome_project.git', + namespace: 'Awesome Space', + visibility_level: 20, + path_with_namespace: 'awesome_space/awesome_project', + default_branch: 'master', + homepage: 'http://example.com/awesome_space/awesome_project', + url: 'http://example.com/awesome_space/awesome_project.git', + ssh_url: 'git@example.com:awesome_space/awesome_project.git', + http_url: 'http://example.com/awesome_space/awesome_project.git', + }, + last_commit: { + id: 'da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + message: 'fixed readme', + title: 'Update file README.md', + timestamp: '2012-01-03T23:36:29+02:00', + url: 'http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + author: { + name: 'GitLab dev user', + email: 'gitlabdev@dv6700.(none)', + }, + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + action: 'open', + detailed_merge_status: 'mergeable', + }, + labels: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + changes: { + updated_by_id: { + previous: null, + current: 1, + }, + updated_at: { + previous: '2017-09-15 16:50:55 UTC', + current: '2017-09-15 16:52:00 UTC', + }, + labels: { + previous: [ + { + id: 206, + title: 'API', + color: '#ffffff', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'API related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + current: [ + { + id: 205, + title: 'Platform', + color: '#123123', + project_id: 14, + created_at: '2013-12-03T17:15:43Z', + updated_at: '2013-12-03T17:15:43Z', + template: false, + description: 'Platform related issues', + type: 'ProjectLabel', + group_id: 41, + }, + ], + }, + last_edited_at: { + previous: null, + current: '2023-03-15 00:00:10 UTC', + }, + last_edited_by_id: { + previous: null, + current: 3278533, + }, + }, + assignees: [ + { + id: 6, + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + ], + reviewers: [ + { + id: 6, + name: 'User1', + username: 'user1', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + }, + ], +}; diff --git a/packages/backend/src/apps/gitlab/triggers/note-event/index.js b/packages/backend/src/apps/gitlab/triggers/note-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a74490a6feb5d66e1e3f782cd15d177917564399 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/note-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './note_event.js'; + +export const triggerDescriptor = { + name: 'Comment event', + description: + 'Comment event (triggered when a new comment is made on commits, merge requests, issues, and code snippets)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#comment-events', + key: GITLAB_EVENT_TYPE.note_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.note_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/note-event/note_event.js b/packages/backend/src/apps/gitlab/triggers/note-event/note_event.js new file mode 100644 index 0000000000000000000000000000000000000000..593188f2e4055250e9e2040135dfa1fefdc311b2 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/note-event/note_event.js @@ -0,0 +1,74 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#comment-events + +export default { + object_kind: 'note', + event_type: 'note', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon', + email: 'admin@example.com', + }, + project_id: 5, + project: { + id: 5, + name: 'Gitlab Test', + description: 'Aut reprehenderit ut est.', + web_url: 'http://example.com/gitlabhq/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + git_http_url: 'http://example.com/gitlabhq/gitlab-test.git', + namespace: 'GitlabHQ', + visibility_level: 20, + path_with_namespace: 'gitlabhq/gitlab-test', + default_branch: 'master', + homepage: 'http://example.com/gitlabhq/gitlab-test', + url: 'http://example.com/gitlabhq/gitlab-test.git', + ssh_url: 'git@example.com:gitlabhq/gitlab-test.git', + http_url: 'http://example.com/gitlabhq/gitlab-test.git', + }, + repository: { + name: 'Gitlab Test', + url: 'http://example.com/gitlab-org/gitlab-test.git', + description: 'Aut reprehenderit ut est.', + homepage: 'http://example.com/gitlab-org/gitlab-test', + }, + object_attributes: { + id: 1243, + note: 'This is a commit comment. How does this work?', + noteable_type: 'Commit', + author_id: 1, + created_at: '2015-05-17 18:08:09 UTC', + updated_at: '2015-05-17 18:08:09 UTC', + project_id: 5, + attachment: null, + line_code: 'bec9703f7a456cd2b4ab5fb3220ae016e3e394e3_0_1', + commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + noteable_id: null, + system: false, + st_diff: { + diff: '--- /dev/null\n+++ b/six\n@@ -0,0 +1 @@\n+Subproject commit 409f37c4f05865e4fb208c771485f211a22c4c2d\n', + new_path: 'six', + old_path: 'six', + a_mode: '0', + b_mode: '160000', + new_file: true, + renamed_file: false, + deleted_file: false, + }, + url: 'http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660#note_1243', + }, + commit: { + id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + message: + 'Add submodule\n\nSigned-off-by: Example User \u003cuser@example.com.com\u003e\n', + timestamp: '2014-02-27T10:06:20+02:00', + url: 'http://example.com/gitlab-org/gitlab-test/commit/cfe32cf61b73a0d5e9f13e774abde7ff789b1660', + author: { + name: 'Example User', + email: 'user@example.com', + }, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/pipeline-event/index.js b/packages/backend/src/apps/gitlab/triggers/pipeline-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..216ddc5bf08be0d5505c91731c43d5b74c856fd3 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/pipeline-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './pipeline_event.js'; + +export const triggerDescriptor = { + name: 'Pipeline event', + description: + 'Pipeline event (triggered when the status of a pipeline changes)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#pipeline-events', + key: GITLAB_EVENT_TYPE.pipeline_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.pipeline_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/pipeline-event/pipeline_event.js b/packages/backend/src/apps/gitlab/triggers/pipeline-event/pipeline_event.js new file mode 100644 index 0000000000000000000000000000000000000000..4a29b41b854182157b2b7f9935051086ea04daf9 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/pipeline-event/pipeline_event.js @@ -0,0 +1,254 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#pipeline-events + +export default { + object_kind: 'pipeline', + object_attributes: { + id: 31, + iid: 3, + ref: 'master', + tag: false, + sha: 'bcbb5ec396a2c0f828686f14fac9b80b780504f2', + before_sha: 'bcbb5ec396a2c0f828686f14fac9b80b780504f2', + source: 'merge_request_event', + status: 'success', + stages: ['build', 'test', 'deploy'], + created_at: '2016-08-12 15:23:28 UTC', + finished_at: '2016-08-12 15:26:29 UTC', + duration: 63, + variables: [ + { + key: 'NESTOR_PROD_ENVIRONMENT', + value: 'us-west-1', + }, + ], + }, + merge_request: { + id: 1, + iid: 1, + title: 'Test', + source_branch: 'test', + source_project_id: 1, + target_branch: 'master', + target_project_id: 1, + state: 'opened', + merge_status: 'can_be_merged', + detailed_merge_status: 'mergeable', + url: 'http://192.168.64.1:3005/gitlab-org/gitlab-test/merge_requests/1', + }, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'user_email@gitlab.com', + }, + project: { + id: 1, + name: 'Gitlab Test', + description: 'Atque in sunt eos similique dolores voluptatem.', + web_url: 'http://192.168.64.1:3005/gitlab-org/gitlab-test', + avatar_url: null, + git_ssh_url: 'git@192.168.64.1:gitlab-org/gitlab-test.git', + git_http_url: 'http://192.168.64.1:3005/gitlab-org/gitlab-test.git', + namespace: 'Gitlab Org', + visibility_level: 20, + path_with_namespace: 'gitlab-org/gitlab-test', + default_branch: 'master', + }, + commit: { + id: 'bcbb5ec396a2c0f828686f14fac9b80b780504f2', + message: 'test\n', + timestamp: '2016-08-12T17:23:21+02:00', + url: 'http://example.com/gitlab-org/gitlab-test/commit/bcbb5ec396a2c0f828686f14fac9b80b780504f2', + author: { + name: 'User', + email: 'user@gitlab.com', + }, + }, + source_pipeline: { + project: { + id: 41, + web_url: 'https://gitlab.example.com/gitlab-org/upstream-project', + path_with_namespace: 'gitlab-org/upstream-project', + }, + pipeline_id: 30, + job_id: 3401, + }, + builds: [ + { + id: 380, + stage: 'deploy', + name: 'production', + status: 'skipped', + created_at: '2016-08-12 15:23:28 UTC', + started_at: null, + finished_at: null, + duration: null, + queued_duration: null, + failure_reason: null, + when: 'manual', + manual: true, + allow_failure: false, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + runner: null, + artifacts_file: { + filename: null, + size: null, + }, + environment: { + name: 'production', + action: 'start', + deployment_tier: 'production', + }, + }, + { + id: 377, + stage: 'test', + name: 'test-image', + status: 'success', + created_at: '2016-08-12 15:23:28 UTC', + started_at: '2016-08-12 15:26:12 UTC', + finished_at: '2016-08-12 15:26:29 UTC', + duration: 17.0, + queued_duration: 196.0, + failure_reason: null, + when: 'on_success', + manual: false, + allow_failure: false, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + runner: { + id: 380987, + description: 'shared-runners-manager-6.gitlab.com', + active: true, + runner_type: 'instance_type', + is_shared: true, + tags: ['linux', 'docker', 'shared-runner'], + }, + artifacts_file: { + filename: null, + size: null, + }, + environment: null, + }, + { + id: 378, + stage: 'test', + name: 'test-build', + status: 'failed', + created_at: '2016-08-12 15:23:28 UTC', + started_at: '2016-08-12 15:26:12 UTC', + finished_at: '2016-08-12 15:26:29 UTC', + duration: 17.0, + queued_duration: 196.0, + failure_reason: 'script_failure', + when: 'on_success', + manual: false, + allow_failure: false, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + runner: { + id: 380987, + description: 'shared-runners-manager-6.gitlab.com', + active: true, + runner_type: 'instance_type', + is_shared: true, + tags: ['linux', 'docker'], + }, + artifacts_file: { + filename: null, + size: null, + }, + environment: null, + }, + { + id: 376, + stage: 'build', + name: 'build-image', + status: 'success', + created_at: '2016-08-12 15:23:28 UTC', + started_at: '2016-08-12 15:24:56 UTC', + finished_at: '2016-08-12 15:25:26 UTC', + duration: 17.0, + queued_duration: 196.0, + failure_reason: null, + when: 'on_success', + manual: false, + allow_failure: false, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + runner: { + id: 380987, + description: 'shared-runners-manager-6.gitlab.com', + active: true, + runner_type: 'instance_type', + is_shared: true, + tags: ['linux', 'docker'], + }, + artifacts_file: { + filename: null, + size: null, + }, + environment: null, + }, + { + id: 379, + stage: 'deploy', + name: 'staging', + status: 'created', + created_at: '2016-08-12 15:23:28 UTC', + started_at: null, + finished_at: null, + duration: null, + queued_duration: null, + failure_reason: null, + when: 'on_success', + manual: false, + allow_failure: false, + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e32bd13e2add097461cb96824b7a829c?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + runner: null, + artifacts_file: { + filename: null, + size: null, + }, + environment: { + name: 'staging', + action: 'start', + deployment_tier: 'staging', + }, + }, + ], +}; diff --git a/packages/backend/src/apps/gitlab/triggers/push-event/index.js b/packages/backend/src/apps/gitlab/triggers/push-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..83d3339f0d1fe4519022a4e028f8a52e7e29968c --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/push-event/index.js @@ -0,0 +1,63 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './push_event.js'; + +export const branchFilterStrategyArgumentDescriptor = { + label: 'What type of filter to use?', + key: 'branch_filter_strategy', + type: 'dropdown', + description: 'Defaults to including all branches', + required: true, + variables: false, + value: 'all_branches', + options: [ + { + label: 'All branches', + value: 'all_branches', + }, + { + label: 'Wildcard pattern (ex: *-stable)', + value: 'wildcard', + }, + { + label: 'Regular expression (ex: ^(feature|hotfix)/)', + value: 'regex', + }, + ], +}; + +export const pushEventsBranchFilterArgumentDescriptor = { + label: 'Filter value', + key: 'push_events_branch_filter', + description: 'Leave empty when using "all branches"', + type: 'string', + required: false, + variables: false, +}; + +export const triggerDescriptor = { + name: 'Push event', + description: 'Push event (triggered when you push to the repository)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#push-events', + key: GITLAB_EVENT_TYPE.push_events, + type: 'webhook', + arguments: [ + projectArgumentDescriptor, + branchFilterStrategyArgumentDescriptor, + pushEventsBranchFilterArgumentDescriptor, + ], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.push_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/push-event/push_event.js b/packages/backend/src/apps/gitlab/triggers/push-event/push_event.js new file mode 100644 index 0000000000000000000000000000000000000000..2951c153aa5e1776bc2a33110be18893375c1f64 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/push-event/push_event.js @@ -0,0 +1,75 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#push-events + +export default { + object_kind: 'push', + event_name: 'push', + before: '95790bf891e76fee5e1747ab589903a6a1f80f22', + after: 'da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + ref: 'refs/heads/master', + checkout_sha: 'da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + user_id: 4, + user_name: 'John Smith', + user_username: 'jsmith', + user_email: 'john@example.com', + user_avatar: + 'https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80', + project_id: 15, + project: { + id: 15, + name: 'Diaspora', + description: '', + web_url: 'http://example.com/mike/diaspora', + avatar_url: null, + git_ssh_url: 'git@example.com:mike/diaspora.git', + git_http_url: 'http://example.com/mike/diaspora.git', + namespace: 'Mike', + visibility_level: 0, + path_with_namespace: 'mike/diaspora', + default_branch: 'master', + homepage: 'http://example.com/mike/diaspora', + url: 'git@example.com:mike/diaspora.git', + ssh_url: 'git@example.com:mike/diaspora.git', + http_url: 'http://example.com/mike/diaspora.git', + }, + repository: { + name: 'Diaspora', + url: 'git@example.com:mike/diaspora.git', + description: '', + homepage: 'http://example.com/mike/diaspora', + git_http_url: 'http://example.com/mike/diaspora.git', + git_ssh_url: 'git@example.com:mike/diaspora.git', + visibility_level: 0, + }, + commits: [ + { + id: 'b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327', + message: + 'Update Catalan translation to e38cb41.\n\nSee https://gitlab.com/gitlab-org/gitlab for more information', + title: 'Update Catalan translation to e38cb41.', + timestamp: '2011-12-12T14:27:31+02:00', + url: 'http://example.com/mike/diaspora/commit/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327', + author: { + name: 'Jordi Mallach', + email: 'jordi@softcatala.org', + }, + added: ['CHANGELOG'], + modified: ['app/controller/application.rb'], + removed: [], + }, + { + id: 'da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + message: 'fixed readme', + title: 'fixed readme', + timestamp: '2012-01-03T23:36:29+02:00', + url: 'http://example.com/mike/diaspora/commit/da1560886d4f094c3e6c9ef40349f7d38b5d27d7', + author: { + name: 'GitLab dev user', + email: 'gitlabdev@dv6700.(none)', + }, + added: ['CHANGELOG'], + modified: ['app/controller/application.rb'], + removed: [], + }, + ], + total_commits_count: 4, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/release-event/index.js b/packages/backend/src/apps/gitlab/triggers/release-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..87e7c85c3e66adb2e009464916ccfdf92c8eac50 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/release-event/index.js @@ -0,0 +1,26 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './release_event.js'; + +export const triggerDescriptor = { + name: 'Release event', + description: 'Release event (triggered when a release is created or updated)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#release-events', + key: GITLAB_EVENT_TYPE.releases_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.releases_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/release-event/release_event.js b/packages/backend/src/apps/gitlab/triggers/release-event/release_event.js new file mode 100644 index 0000000000000000000000000000000000000000..90c758b30c4c89efdcf232d9a6691d48b970dd16 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/release-event/release_event.js @@ -0,0 +1,72 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#release-events + +export default { + object_kind: 'release', + id: 1, + created_at: '2020-11-02 12:55:12 UTC', + description: 'v1.1 has been released', + name: 'v1.1', + released_at: '2020-11-02 12:55:12 UTC', + tag: 'v1.1', + project: { + id: 2, + name: 'release-webhook-example', + description: '', + web_url: 'https://example.com/gitlab-org/release-webhook-example', + avatar_url: null, + git_ssh_url: 'ssh://git@example.com/gitlab-org/release-webhook-example.git', + git_http_url: 'https://example.com/gitlab-org/release-webhook-example.git', + namespace: 'Gitlab', + visibility_level: 0, + path_with_namespace: 'gitlab-org/release-webhook-example', + default_branch: 'master', + ci_config_path: null, + homepage: 'https://example.com/gitlab-org/release-webhook-example', + url: 'ssh://git@example.com/gitlab-org/release-webhook-example.git', + ssh_url: 'ssh://git@example.com/gitlab-org/release-webhook-example.git', + http_url: 'https://example.com/gitlab-org/release-webhook-example.git', + }, + url: 'https://example.com/gitlab-org/release-webhook-example/-/releases/v1.1', + action: 'create', + assets: { + count: 5, + links: [ + { + id: 1, + external: true, // deprecated in GitLab 15.9, will be removed in GitLab 16.0. + link_type: 'other', + name: 'Changelog', + url: 'https://example.net/changelog', + }, + ], + sources: [ + { + format: 'zip', + url: 'https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.zip', + }, + { + format: 'tar.gz', + url: 'https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.gz', + }, + { + format: 'tar.bz2', + url: 'https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar.bz2', + }, + { + format: 'tar', + url: 'https://example.com/gitlab-org/release-webhook-example/-/archive/v1.1/release-webhook-example-v1.1.tar', + }, + ], + }, + commit: { + id: 'ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8', + message: 'Release v1.1', + title: 'Release v1.1', + timestamp: '2020-10-31T14:58:32+11:00', + url: 'https://example.com/gitlab-org/release-webhook-example/-/commit/ee0a3fb31ac16e11b9dbb596ad16d4af654d08f8', + author: { + name: 'Example User', + email: 'user@example.com', + }, + }, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/tag-push-event/index.js b/packages/backend/src/apps/gitlab/triggers/tag-push-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a64dfb33284926a3b6610b04cbb1ae58ab55eed1 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/tag-push-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './tag_push_event.js'; + +export const triggerDescriptor = { + name: 'Tag event', + description: + 'Tag event (triggered when you create or delete tags in the repository)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#tag-events', + key: GITLAB_EVENT_TYPE.tag_push_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.tag_push_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/tag-push-event/tag_push_event.js b/packages/backend/src/apps/gitlab/triggers/tag-push-event/tag_push_event.js new file mode 100644 index 0000000000000000000000000000000000000000..8dd94cfcb432dc10a1031e7f54ed63be2281398a --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/tag-push-event/tag_push_event.js @@ -0,0 +1,43 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#tag-events + +export default { + object_kind: 'tag_push', + event_name: 'tag_push', + before: '0000000000000000000000000000000000000000', + after: '82b3d5ae55f7080f1e6022629cdb57bfae7cccc7', + ref: 'refs/tags/v1.0.0', + checkout_sha: '82b3d5ae55f7080f1e6022629cdb57bfae7cccc7', + user_id: 1, + user_name: 'John Smith', + user_avatar: + 'https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80', + project_id: 1, + project: { + id: 1, + name: 'Example', + description: '', + web_url: 'http://example.com/jsmith/example', + avatar_url: null, + git_ssh_url: 'git@example.com:jsmith/example.git', + git_http_url: 'http://example.com/jsmith/example.git', + namespace: 'Jsmith', + visibility_level: 0, + path_with_namespace: 'jsmith/example', + default_branch: 'master', + homepage: 'http://example.com/jsmith/example', + url: 'git@example.com:jsmith/example.git', + ssh_url: 'git@example.com:jsmith/example.git', + http_url: 'http://example.com/jsmith/example.git', + }, + repository: { + name: 'Example', + url: 'ssh://git@example.com/jsmith/example.git', + description: '', + homepage: 'http://example.com/jsmith/example', + git_http_url: 'http://example.com/jsmith/example.git', + git_ssh_url: 'git@example.com:jsmith/example.git', + visibility_level: 0, + }, + commits: [], + total_commits_count: 0, +}; diff --git a/packages/backend/src/apps/gitlab/triggers/types.js b/packages/backend/src/apps/gitlab/triggers/types.js new file mode 100644 index 0000000000000000000000000000000000000000..cadc42e1f6d148f11a873fcb1999a2a4a36211a3 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/types.js @@ -0,0 +1,15 @@ +export const GITLAB_EVENT_TYPE = { + confidential_issues_events: 'confidential_issues_events', + confidential_note_events: 'confidential_note_events', + deployment_events: 'deployment_events', + feature_flag_events: 'feature_flag_events', + issues_events: 'issues_events', + job_events: 'job_events', + merge_requests_events: 'merge_requests_events', + note_events: 'note_events', + pipeline_events: 'pipeline_events', + push_events: 'push_events', + releases_events: 'releases_events', + tag_push_events: 'tag_push_events', + wiki_page_events: 'wiki_page_events', +}; diff --git a/packages/backend/src/apps/gitlab/triggers/wiki-page-event/index.js b/packages/backend/src/apps/gitlab/triggers/wiki-page-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e92f8caf891fe175110cc656197837ecb52a1dbd --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/wiki-page-event/index.js @@ -0,0 +1,27 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import { GITLAB_EVENT_TYPE } from '../types.js'; +import { + getRegisterHookFn, + getRunFn, + getTestRunFn, + projectArgumentDescriptor, + unregisterHook, +} from '../lib.js'; + +import data from './wiki_page_event.js'; + +export const triggerDescriptor = { + name: 'Wiki page event', + description: + 'Wiki page event (triggered when a wiki page is created, updated, or deleted)', + // info: 'https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#wiki-page-events', + key: GITLAB_EVENT_TYPE.wiki_page_events, + type: 'webhook', + arguments: [projectArgumentDescriptor], + run: ($) => getRunFn($), + testRun: getTestRunFn(data), + registerHook: getRegisterHookFn(GITLAB_EVENT_TYPE.wiki_page_events), + unregisterHook, +}; + +export default defineTrigger(triggerDescriptor); diff --git a/packages/backend/src/apps/gitlab/triggers/wiki-page-event/wiki_page_event.js b/packages/backend/src/apps/gitlab/triggers/wiki-page-event/wiki_page_event.js new file mode 100644 index 0000000000000000000000000000000000000000..3058eea2863a1051e5be58af4a1cb38de898ffe0 --- /dev/null +++ b/packages/backend/src/apps/gitlab/triggers/wiki-page-event/wiki_page_event.js @@ -0,0 +1,48 @@ +// https://docs.gitlab.com/ee/user/project/integrations/webhook_events.html#wiki-page-events + +export default { + object_kind: 'wiki_page', + user: { + id: 1, + name: 'Administrator', + username: 'root', + avatar_url: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + email: 'admin@example.com', + }, + project: { + id: 1, + name: 'awesome-project', + description: 'This is awesome', + web_url: 'http://example.com/root/awesome-project', + avatar_url: null, + git_ssh_url: 'git@example.com:root/awesome-project.git', + git_http_url: 'http://example.com/root/awesome-project.git', + namespace: 'root', + visibility_level: 0, + path_with_namespace: 'root/awesome-project', + default_branch: 'master', + homepage: 'http://example.com/root/awesome-project', + url: 'git@example.com:root/awesome-project.git', + ssh_url: 'git@example.com:root/awesome-project.git', + http_url: 'http://example.com/root/awesome-project.git', + }, + wiki: { + web_url: 'http://example.com/root/awesome-project/-/wikis/home', + git_ssh_url: 'git@example.com:root/awesome-project.wiki.git', + git_http_url: 'http://example.com/root/awesome-project.wiki.git', + path_with_namespace: 'root/awesome-project.wiki', + default_branch: 'master', + }, + object_attributes: { + title: 'Awesome', + content: 'awesome content goes here', + format: 'markdown', + message: 'adding an awesome page to the wiki', + slug: 'awesome', + url: 'http://example.com/root/awesome-project/-/wikis/awesome', + action: 'create', + diff_url: + 'http://example.com/root/awesome-project/-/wikis/home/diff?version_id=78ee4a6705abfbff4f4132c6646dbaae9c8fb6ec', + }, +}; diff --git a/packages/backend/src/apps/google-calendar/assets/favicon.svg b/packages/backend/src/apps/google-calendar/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..14b505ab8f71be5d04cb93d0fe91655888aab268 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/assets/favicon.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/packages/backend/src/apps/google-calendar/auth/generate-auth-url.js b/packages/backend/src/apps/google-calendar/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c972ae169d964393279e8942a2f6fd417c87c645 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + prompt: 'select_account', + scope: authScope.join(' '), + response_type: 'code', + access_type: 'offline', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/google-calendar/auth/index.js b/packages/backend/src/apps/google-calendar/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a481e43eb535b84596281e253d8b32693e9d279d --- /dev/null +++ b/packages/backend/src/apps/google-calendar/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/google-calendar/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/google-calendar/auth/is-still-verified.js b/packages/backend/src/apps/google-calendar/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/google-calendar/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/google-calendar/auth/refresh-token.js b/packages/backend/src/apps/google-calendar/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7020e9ed24981513c8786ed786f136d476eb --- /dev/null +++ b/packages/backend/src/apps/google-calendar/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/google-calendar/auth/verify-credentials.js b/packages/backend/src/apps/google-calendar/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/google-calendar/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/google-calendar/common/add-auth-header.js b/packages/backend/src/apps/google-calendar/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/google-calendar/common/auth-scope.js b/packages/backend/src/apps/google-calendar/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..421c26f4cfee7fde458d334e1f996cb8275509d6 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/common/auth-scope.js @@ -0,0 +1,7 @@ +const authScope = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/google-calendar/common/get-current-user.js b/packages/backend/src/apps/google-calendar/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/google-calendar/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/google-calendar/dynamic-data/index.js b/packages/backend/src/apps/google-calendar/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cf6ba7d2b3f9b9f299c938bfa5e57999b825073 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listCalendars from './list-calendars/index.js'; + +export default [listCalendars]; diff --git a/packages/backend/src/apps/google-calendar/dynamic-data/list-calendars/index.js b/packages/backend/src/apps/google-calendar/dynamic-data/list-calendars/index.js new file mode 100644 index 0000000000000000000000000000000000000000..afed03f1d9f1af41d3e38eccb7ff99319c29603b --- /dev/null +++ b/packages/backend/src/apps/google-calendar/dynamic-data/list-calendars/index.js @@ -0,0 +1,32 @@ +export default { + name: 'List calendars', + key: 'listCalendars', + + async run($) { + const drives = { + data: [], + }; + + const params = { + pageToken: undefined, + }; + + do { + const { data } = await $.http.get(`/v3/users/me/calendarList`, { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items) { + for (const calendar of data.items) { + drives.data.push({ + value: calendar.id, + name: calendar.summary, + }); + } + } + } while (params.pageToken); + + return drives; + }, +}; diff --git a/packages/backend/src/apps/google-calendar/index.js b/packages/backend/src/apps/google-calendar/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00fa076a5043fe2e32be45ebf13cec4943a91677 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Google Calendar', + key: 'google-calendar', + baseUrl: 'https://calendar.google.com', + apiBaseUrl: 'https://www.googleapis.com/calendar', + iconUrl: '{BASE_URL}/apps/google-calendar/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/google-calendar/connection', + primaryColor: '448AFF', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/google-calendar/triggers/index.js b/packages/backend/src/apps/google-calendar/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bb5106633a540588ac4e81a786c3998a55a500fe --- /dev/null +++ b/packages/backend/src/apps/google-calendar/triggers/index.js @@ -0,0 +1,4 @@ +import newCalendar from './new-calendar/index.js'; +import newEvent from './new-event/index.js'; + +export default [newCalendar, newEvent]; diff --git a/packages/backend/src/apps/google-calendar/triggers/new-calendar/index.js b/packages/backend/src/apps/google-calendar/triggers/new-calendar/index.js new file mode 100644 index 0000000000000000000000000000000000000000..53ea73146a43c78633077dd1b2b6b606df2b6216 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/triggers/new-calendar/index.js @@ -0,0 +1,34 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New calendar', + key: 'newCalendar', + pollInterval: 15, + description: 'Triggers when a new calendar is created.', + arguments: [], + + async run($) { + const params = { + pageToken: undefined, + maxResults: 250, + }; + + do { + const { data } = await $.http.get('/v3/users/me/calendarList', { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items?.length) { + for (const calendar of data.items.reverse()) { + $.pushTriggerItem({ + raw: calendar, + meta: { + internalId: calendar.etag, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/google-calendar/triggers/new-event/index.js b/packages/backend/src/apps/google-calendar/triggers/new-event/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1c012191f8e9bee7ce85f2858b87bc83270b0773 --- /dev/null +++ b/packages/backend/src/apps/google-calendar/triggers/new-event/index.js @@ -0,0 +1,55 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New event', + key: 'newEvent', + pollInterval: 15, + description: 'Triggers when a new event is created.', + arguments: [ + { + label: 'Calendar', + key: 'calendarId', + type: 'dropdown', + required: true, + description: '', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCalendars', + }, + ], + }, + }, + ], + + async run($) { + const calendarId = $.step.parameters.calendarId; + + const params = { + pageToken: undefined, + orderBy: 'updated', + }; + + do { + const { data } = await $.http.get(`/v3/calendars/${calendarId}/events`, { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items?.length) { + for (const event of data.items.reverse()) { + $.pushTriggerItem({ + raw: event, + meta: { + internalId: event.etag, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/google-drive/assets/favicon.svg b/packages/backend/src/apps/google-drive/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..a8cefd5b28bf56133b0cb8fb440072e5832ca6ad --- /dev/null +++ b/packages/backend/src/apps/google-drive/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/google-drive/auth/generate-auth-url.js b/packages/backend/src/apps/google-drive/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c972ae169d964393279e8942a2f6fd417c87c645 --- /dev/null +++ b/packages/backend/src/apps/google-drive/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + prompt: 'select_account', + scope: authScope.join(' '), + response_type: 'code', + access_type: 'offline', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/google-drive/auth/index.js b/packages/backend/src/apps/google-drive/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9dd547140d6e353b35c533445077639ebf4faa17 --- /dev/null +++ b/packages/backend/src/apps/google-drive/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/google-drive/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/google-drive/auth/is-still-verified.js b/packages/backend/src/apps/google-drive/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/google-drive/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/google-drive/auth/refresh-token.js b/packages/backend/src/apps/google-drive/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7020e9ed24981513c8786ed786f136d476eb --- /dev/null +++ b/packages/backend/src/apps/google-drive/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/google-drive/auth/verify-credentials.js b/packages/backend/src/apps/google-drive/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/google-drive/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/google-drive/common/add-auth-header.js b/packages/backend/src/apps/google-drive/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/google-drive/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/google-drive/common/auth-scope.js b/packages/backend/src/apps/google-drive/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..5bd605377bb2b5592ae51d0df83da9f50e547f3f --- /dev/null +++ b/packages/backend/src/apps/google-drive/common/auth-scope.js @@ -0,0 +1,7 @@ +const authScope = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/google-drive/common/get-current-user.js b/packages/backend/src/apps/google-drive/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/google-drive/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/google-drive/dynamic-data/index.js b/packages/backend/src/apps/google-drive/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..96e1d64439c01f281052992683cddec417a55a7e --- /dev/null +++ b/packages/backend/src/apps/google-drive/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listFolders from './list-folders/index.js'; +import listDrives from './list-drives/index.js'; + +export default [listFolders, listDrives]; diff --git a/packages/backend/src/apps/google-drive/dynamic-data/list-drives/index.js b/packages/backend/src/apps/google-drive/dynamic-data/list-drives/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2454433de65fc2049043861a739a89d03dafed2f --- /dev/null +++ b/packages/backend/src/apps/google-drive/dynamic-data/list-drives/index.js @@ -0,0 +1,31 @@ +export default { + name: 'List drives', + key: 'listDrives', + + async run($) { + const drives = { + data: [{ value: null, name: 'My Google Drive' }], + }; + + const params = { + pageSize: 100, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get(`/v3/drives`, { params }); + params.pageToken = data.nextPageToken; + + if (data.drives) { + for (const drive of data.drives) { + drives.data.push({ + value: drive.id, + name: drive.name, + }); + } + } + } while (params.pageToken); + + return drives; + }, +}; diff --git a/packages/backend/src/apps/google-drive/dynamic-data/list-folders/index.js b/packages/backend/src/apps/google-drive/dynamic-data/list-folders/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c52817bd88645223aad0c38d61c18797da5d31ef --- /dev/null +++ b/packages/backend/src/apps/google-drive/dynamic-data/list-folders/index.js @@ -0,0 +1,43 @@ +export default { + name: 'List folders', + key: 'listFolders', + + async run($) { + const folders = { + data: [], + }; + + const params = { + q: `mimeType='application/vnd.google-apps.folder'`, + orderBy: 'createdTime desc', + pageToken: undefined, + pageSize: 1000, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get( + `https://www.googleapis.com/drive/v3/files`, + { + params, + } + ); + params.pageToken = data.nextPageToken; + + for (const file of data.files) { + folders.data.push({ + value: file.id, + name: file.name, + }); + } + } while (params.pageToken); + + return folders; + }, +}; diff --git a/packages/backend/src/apps/google-drive/index.js b/packages/backend/src/apps/google-drive/index.js new file mode 100644 index 0000000000000000000000000000000000000000..93e0833b3cfffb5ce050447a1460ad08f33a9ad5 --- /dev/null +++ b/packages/backend/src/apps/google-drive/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Google Drive', + key: 'google-drive', + baseUrl: 'https://drive.google.com', + apiBaseUrl: 'https://www.googleapis.com/drive', + iconUrl: '{BASE_URL}/apps/google-drive/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/google-drive/connection', + primaryColor: '1FA463', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/google-drive/triggers/index.js b/packages/backend/src/apps/google-drive/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1f60196856fe4f661f0cf6f0d4c520027526583c --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/index.js @@ -0,0 +1,6 @@ +import newFiles from './new-files/index.js'; +import newFilesInFolder from './new-files-in-folder/index.js'; +import newFolders from './new-folders/index.js'; +import updatedFiles from './updated-files/index.js'; + +export default [newFiles, newFilesInFolder, newFolders, updatedFiles]; diff --git a/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/index.js b/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/index.js new file mode 100644 index 0000000000000000000000000000000000000000..182a08e128beacda35640e3d2e2081e9564d6764 --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newFilesInFolder from './new-files-in-folder.js'; + +export default defineTrigger({ + name: 'New files in folder', + key: 'newFilesInFolder', + pollInterval: 15, + description: + 'Triggers when a new file is added directly to a specific folder (but not its subfolder).', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Folder', + key: 'folderId', + type: 'dropdown', + required: false, + dependsOn: ['parameters.driveId'], + description: + 'Check a specific folder for new files. Please note: new files added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFolders', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + ], + + async run($) { + await newFilesInFolder($); + }, +}); diff --git a/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/new-files-in-folder.js b/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/new-files-in-folder.js new file mode 100644 index 0000000000000000000000000000000000000000..fcad7f81127bc2224582c76ffb24b2892d2cec7a --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-files-in-folder/new-files-in-folder.js @@ -0,0 +1,40 @@ +const newFilesInFolder = async ($) => { + let q = "mimeType!='application/vnd.google-apps.folder'"; + if ($.step.parameters.folderId) { + q += ` and '${$.step.parameters.folderId}' in parents`; + } else { + q += ` and parents in 'root'`; + } + const params = { + pageToken: undefined, + orderBy: 'createdTime desc', + fields: '*', + pageSize: 1000, + q, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get(`/v3/files`, { params }); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + $.pushTriggerItem({ + raw: file, + meta: { + internalId: file.id, + }, + }); + } + } + } while (params.pageToken); +}; + +export default newFilesInFolder; diff --git a/packages/backend/src/apps/google-drive/triggers/new-files/index.js b/packages/backend/src/apps/google-drive/triggers/new-files/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f5533ae4d573490102d7b3028b645a465392f3f3 --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-files/index.js @@ -0,0 +1,34 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newFiles from './new-files.js'; + +export default defineTrigger({ + name: 'New files', + key: 'newFiles', + pollInterval: 15, + description: 'Triggers when any new file is added (inside of any folder).', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + ], + + async run($) { + await newFiles($); + }, +}); diff --git a/packages/backend/src/apps/google-drive/triggers/new-files/new-files.js b/packages/backend/src/apps/google-drive/triggers/new-files/new-files.js new file mode 100644 index 0000000000000000000000000000000000000000..dc0fce8d57845526dddd449ed9d15265509d4a5b --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-files/new-files.js @@ -0,0 +1,34 @@ +const newFiles = async ($) => { + const params = { + pageToken: undefined, + orderBy: 'createdTime desc', + fields: '*', + pageSize: 1000, + q: `mimeType!='application/vnd.google-apps.folder'`, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get('/v3/files', { params }); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + $.pushTriggerItem({ + raw: file, + meta: { + internalId: file.id, + }, + }); + } + } + } while (params.pageToken); +}; + +export default newFiles; diff --git a/packages/backend/src/apps/google-drive/triggers/new-folders/index.js b/packages/backend/src/apps/google-drive/triggers/new-folders/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d2b8c7e41b45ace3aca89c975edb780443be574c --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-folders/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newFolders from './new-folders.js'; + +export default defineTrigger({ + name: 'New folders', + key: 'newFolders', + pollInterval: 15, + description: + 'Triggers when a new folder is added directly to a specific folder (but not its subfolder).', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Folder', + key: 'folderId', + type: 'dropdown', + required: false, + dependsOn: ['parameters.driveId'], + description: + 'Check a specific folder for new subfolders. Please note: new folders added to subfolders inside the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFolders', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + ], + + async run($) { + await newFolders($); + }, +}); diff --git a/packages/backend/src/apps/google-drive/triggers/new-folders/new-folders.js b/packages/backend/src/apps/google-drive/triggers/new-folders/new-folders.js new file mode 100644 index 0000000000000000000000000000000000000000..d371dfe85347f5a33955ca0e69231ef7d509858a --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/new-folders/new-folders.js @@ -0,0 +1,41 @@ +const newFolders = async ($) => { + let q = "mimeType='application/vnd.google-apps.folder'"; + if ($.step.parameters.folderId) { + q += ` and '${$.step.parameters.folderId}' in parents`; + } else { + q += ` and parents in 'root'`; + } + + const params = { + pageToken: undefined, + orderBy: 'createdTime desc', + fields: '*', + pageSize: 1000, + q, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get(`/v3/files`, { params }); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + $.pushTriggerItem({ + raw: file, + meta: { + internalId: file.id, + }, + }); + } + } + } while (params.pageToken); +}; + +export default newFolders; diff --git a/packages/backend/src/apps/google-drive/triggers/updated-files/index.js b/packages/backend/src/apps/google-drive/triggers/updated-files/index.js new file mode 100644 index 0000000000000000000000000000000000000000..18d43334291c2ad27d32829aacb5d3f1efac0c76 --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/updated-files/index.js @@ -0,0 +1,76 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import updatedFiles from './updated-files.js'; + +export default defineTrigger({ + name: 'Updated files', + key: 'updatedFiles', + pollInterval: 15, + description: + 'Triggers when a file is updated in a specific folder (but not its subfolder).', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your file resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Folder', + key: 'folderId', + type: 'dropdown', + required: false, + dependsOn: ['parameters.driveId'], + description: + 'Check a specific folder for updated files. Please note: files located in subfolders of the folder you choose here will NOT trigger this flow. Defaults to the top-level folder if none is picked.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFolders', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + { + label: 'Include Deleted', + key: 'includeDeleted', + type: 'dropdown', + required: true, + value: true, + description: 'Should this trigger also on files that are deleted?', + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + ], + + async run($) { + await updatedFiles($); + }, +}); diff --git a/packages/backend/src/apps/google-drive/triggers/updated-files/updated-files.js b/packages/backend/src/apps/google-drive/triggers/updated-files/updated-files.js new file mode 100644 index 0000000000000000000000000000000000000000..ab5cad1cba5a472cf2d565d0f4f83049369d1f02 --- /dev/null +++ b/packages/backend/src/apps/google-drive/triggers/updated-files/updated-files.js @@ -0,0 +1,45 @@ +const updatedFiles = async ($) => { + let q = `mimeType!='application/vnd.google-apps.folder'`; + if ($.step.parameters.includeDeleted === false) { + q += ` and trashed=${$.step.parameters.includeDeleted}`; + } + + if ($.step.parameters.folderId) { + q += ` and '${$.step.parameters.folderId}' in parents`; + } else { + q += ` and parents in 'root'`; + } + + const params = { + pageToken: undefined, + orderBy: 'modifiedTime desc', + fields: '*', + pageSize: 1000, + q, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get(`/v3/files`, { params }); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + $.pushTriggerItem({ + raw: file, + meta: { + internalId: `${file.id}-${file.modifiedTime}`, + }, + }); + } + } + } while (params.pageToken); +}; + +export default updatedFiles; diff --git a/packages/backend/src/apps/google-forms/assets/favicon.svg b/packages/backend/src/apps/google-forms/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..5413d433f5fd9361a21e2b8648f5c57b0e70196d --- /dev/null +++ b/packages/backend/src/apps/google-forms/assets/favicon.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/backend/src/apps/google-forms/auth/generate-auth-url.js b/packages/backend/src/apps/google-forms/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c972ae169d964393279e8942a2f6fd417c87c645 --- /dev/null +++ b/packages/backend/src/apps/google-forms/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + prompt: 'select_account', + scope: authScope.join(' '), + response_type: 'code', + access_type: 'offline', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/google-forms/auth/index.js b/packages/backend/src/apps/google-forms/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dc57085467132da87fe182c2e974e914ee275f1d --- /dev/null +++ b/packages/backend/src/apps/google-forms/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/google-forms/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/google-forms/auth/is-still-verified.js b/packages/backend/src/apps/google-forms/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/google-forms/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/google-forms/auth/refresh-token.js b/packages/backend/src/apps/google-forms/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7020e9ed24981513c8786ed786f136d476eb --- /dev/null +++ b/packages/backend/src/apps/google-forms/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/google-forms/auth/verify-credentials.js b/packages/backend/src/apps/google-forms/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/google-forms/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/google-forms/common/add-auth-header.js b/packages/backend/src/apps/google-forms/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..6c86424c30473e860c84c2306e9cb9f0070bb8e2 --- /dev/null +++ b/packages/backend/src/apps/google-forms/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.headers && $.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/google-forms/common/auth-scope.js b/packages/backend/src/apps/google-forms/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..655ae747be07a6f9eefe230d6270a0b2acef62bf --- /dev/null +++ b/packages/backend/src/apps/google-forms/common/auth-scope.js @@ -0,0 +1,9 @@ +const authScope = [ + 'https://www.googleapis.com/auth/forms.body.readonly', + 'https://www.googleapis.com/auth/forms.responses.readonly', + 'https://www.googleapis.com/auth/drive.readonly', + 'https://www.googleapis.com/auth/userinfo.email', + 'profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/google-forms/common/get-current-user.js b/packages/backend/src/apps/google-forms/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/google-forms/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/google-forms/dynamic-data/index.js b/packages/backend/src/apps/google-forms/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0a58430e6d8b4532f1277ea8336d8dad3340061f --- /dev/null +++ b/packages/backend/src/apps/google-forms/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listForms from './list-forms/index.js'; + +export default [listForms]; diff --git a/packages/backend/src/apps/google-forms/dynamic-data/list-forms/index.js b/packages/backend/src/apps/google-forms/dynamic-data/list-forms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00250819bef019c9bc9011b864f93df1b0336c6a --- /dev/null +++ b/packages/backend/src/apps/google-forms/dynamic-data/list-forms/index.js @@ -0,0 +1,33 @@ +export default { + name: 'List forms', + key: 'listForms', + + async run($) { + const forms = { + data: [], + }; + + const params = { + q: `mimeType='application/vnd.google-apps.form'`, + spaces: 'drive', + pageToken: undefined, + }; + + do { + const { data } = await $.http.get( + `https://www.googleapis.com/drive/v3/files`, + { params } + ); + params.pageToken = data.nextPageToken; + + for (const file of data.files) { + forms.data.push({ + value: file.id, + name: file.name, + }); + } + } while (params.pageToken); + + return forms; + }, +}; diff --git a/packages/backend/src/apps/google-forms/index.js b/packages/backend/src/apps/google-forms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ae4fc49ab7920bcbb9b3048874dda0f33d55560f --- /dev/null +++ b/packages/backend/src/apps/google-forms/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Google Forms', + key: 'google-forms', + baseUrl: 'https://docs.google.com/forms', + apiBaseUrl: 'https://forms.googleapis.com', + iconUrl: '{BASE_URL}/apps/google-forms/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/google-forms/connection', + primaryColor: '673AB7', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/google-forms/triggers/index.js b/packages/backend/src/apps/google-forms/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..41e904874f7e7198d17366704a4b125c3931a386 --- /dev/null +++ b/packages/backend/src/apps/google-forms/triggers/index.js @@ -0,0 +1,3 @@ +import newFormResponses from './new-form-responses/index.js'; + +export default [newFormResponses]; diff --git a/packages/backend/src/apps/google-forms/triggers/new-form-responses/index.js b/packages/backend/src/apps/google-forms/triggers/new-form-responses/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ac16701dbdf626b5def1f205b88b7a4aa08158f3 --- /dev/null +++ b/packages/backend/src/apps/google-forms/triggers/new-form-responses/index.js @@ -0,0 +1,33 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newFormResponses from './new-form-responses.js'; + +export default defineTrigger({ + name: 'New form responses', + key: 'newFormResponses', + pollInterval: 15, + description: 'Triggers when a new form response is submitted.', + arguments: [ + { + label: 'Form', + key: 'formId', + type: 'dropdown', + required: true, + description: 'Pick a form to receive form responses.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listForms', + }, + ], + }, + }, + ], + + async run($) { + await newFormResponses($); + }, +}); diff --git a/packages/backend/src/apps/google-forms/triggers/new-form-responses/new-form-responses.js b/packages/backend/src/apps/google-forms/triggers/new-form-responses/new-form-responses.js new file mode 100644 index 0000000000000000000000000000000000000000..26487b412e422cb93f714db03b296059e64593f0 --- /dev/null +++ b/packages/backend/src/apps/google-forms/triggers/new-form-responses/new-form-responses.js @@ -0,0 +1,26 @@ +const newResponses = async ($) => { + const params = { + pageToken: undefined, + }; + + do { + const pathname = `/v1/forms/${$.step.parameters.formId}/responses`; + const { data } = await $.http.get(pathname, { params }); + params.pageToken = data.nextPageToken; + + if (data.responses?.length) { + for (const formResponse of data.responses) { + const dataItem = { + raw: formResponse, + meta: { + internalId: formResponse.responseId, + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (params.pageToken); +}; + +export default newResponses; diff --git a/packages/backend/src/apps/google-sheets/actions/create-spreadsheet-row/index.js b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet-row/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be18a160811224a398ecb4e1d4a9d1d2e2485d65 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet-row/index.js @@ -0,0 +1,134 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create spreadsheet row', + key: 'createSpreadsheetRow', + description: 'Creates a new row in a specified spreadsheet.', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Spreadsheet', + key: 'spreadsheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.driveId'], + description: 'The spreadsheets in your Google Drive.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + { + label: 'Worksheet', + key: 'worksheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.spreadsheetId'], + description: 'The worksheets in your selected spreadsheet.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listWorksheets', + }, + { + name: 'parameters.spreadsheetId', + value: '{parameters.spreadsheetId}', + }, + ], + }, + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listSheetHeaders', + }, + { + name: 'parameters.worksheetId', + value: '{parameters.worksheetId}', + }, + { + name: 'parameters.spreadsheetId', + value: '{parameters.spreadsheetId}', + }, + ], + }, + }, + ], + + async run($) { + const { + data: { sheets }, + } = await $.http.get(`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`); + + const selectedSheet = sheets.find( + (sheet) => sheet.properties.sheetId === $.step.parameters.worksheetId + ); + + const sheetName = selectedSheet.properties.title; + + const range = sheetName; + + const dataValues = Object.entries($.step.parameters) + .filter((entry) => entry[0].startsWith('header-')) + .map((value) => value[1]); + + const values = [dataValues]; + + const params = { + valueInputOption: 'USER_ENTERED', + insertDataOption: 'INSERT_ROWS', + includeValuesInResponse: true, + }; + + const body = { + majorDimension: 'ROWS', + range, + values, + }; + + const { data } = await $.http.post( + `/v4/spreadsheets/${$.step.parameters.spreadsheetId}/values/${range}:append`, + body, + { params } + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.js b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bf2692001cba881f5a2fc70f38a0da199ef24d0d --- /dev/null +++ b/packages/backend/src/apps/google-sheets/actions/create-spreadsheet/index.js @@ -0,0 +1,100 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create spreadsheet', + key: 'createSpreadsheet', + description: + 'Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Spreadsheet to copy', + key: 'spreadsheetId', + type: 'dropdown', + required: false, + description: 'Choose a spreadsheet to copy its data.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + ], + }, + }, + { + label: 'Headers', + key: 'headers', + type: 'dynamic', + required: false, + description: + 'These headers are ignored if "Spreadsheet to Copy" is selected.', + fields: [ + { + label: 'Header', + key: 'header', + type: 'string', + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + if ($.step.parameters.spreadsheetId) { + const body = { name: $.step.parameters.title }; + + const { data } = await $.http.post( + `https://www.googleapis.com/drive/v3/files/${$.step.parameters.spreadsheetId}/copy`, + body + ); + + $.setActionItem({ + raw: data, + }); + } else { + const headers = $.step.parameters.headers; + const values = headers.map((entry) => entry.header); + + const spreadsheetBody = { + properties: { + title: $.step.parameters.title, + }, + sheets: [ + { + data: [ + { + startRow: 0, + startColumn: 0, + rowData: [ + { + values: values.map((header) => ({ + userEnteredValue: { stringValue: header }, + })), + }, + ], + }, + ], + }, + ], + }; + + const { data } = await $.http.post('/v4/spreadsheets', spreadsheetBody); + + $.setActionItem({ + raw: data, + }); + } + }, +}); diff --git a/packages/backend/src/apps/google-sheets/actions/create-worksheet/index.js b/packages/backend/src/apps/google-sheets/actions/create-worksheet/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ceed6f24f336be7d2d3882036bd5c085848f5daf --- /dev/null +++ b/packages/backend/src/apps/google-sheets/actions/create-worksheet/index.js @@ -0,0 +1,171 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create worksheet', + key: 'createWorksheet', + description: + 'Create a blank worksheet with a title. Optionally, provide headers.', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Spreadsheet', + key: 'spreadsheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.driveId'], + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Headers', + key: 'headers', + type: 'dynamic', + required: false, + fields: [ + { + label: 'Header', + key: 'header', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Overwrite', + key: 'overwrite', + type: 'dropdown', + required: false, + value: false, + description: + 'If a worksheet with the specified title exists, its content would be lost. Please, use with caution.', + variables: true, + options: [ + { + label: 'Yes', + value: 'true', + }, + { + label: 'No', + value: 'false', + }, + ], + }, + ], + + async run($) { + const { + data: { sheets }, + } = await $.http.get(`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`); + + const selectedSheet = sheets.find( + (sheet) => sheet.properties.title === $.step.parameters.title + ); + const headers = $.step.parameters.headers; + const values = headers.map((entry) => entry.header); + + const body = { + requests: [ + { + addSheet: { + properties: { + title: $.step.parameters.title, + }, + }, + }, + ], + }; + + if ($.step.parameters.overwrite === 'true' && selectedSheet) { + body.requests.unshift({ + deleteSheet: { + sheetId: selectedSheet.properties.sheetId, + }, + }); + } + + const { data } = await $.http.post( + `https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`, + body + ); + + if (values.length) { + const body = { + requests: [ + { + updateCells: { + rows: [ + { + values: values.map((header) => ({ + userEnteredValue: { stringValue: header }, + })), + }, + ], + fields: '*', + start: { + sheetId: + data.replies[data.replies.length - 1].addSheet.properties + .sheetId, + rowIndex: 0, + columnIndex: 0, + }, + }, + }, + ], + }; + + const { data: response } = await $.http.post( + `https://sheets.googleapis.com/v4/spreadsheets/${$.step.parameters.spreadsheetId}:batchUpdate`, + body + ); + + $.setActionItem({ + raw: response, + }); + return; + } + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/google-sheets/actions/index.js b/packages/backend/src/apps/google-sheets/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b5143cc288f74014e69135726574ee253c8a95c0 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/actions/index.js @@ -0,0 +1,5 @@ +import createSpreadsheet from './create-spreadsheet/index.js'; +import createSpreadsheetRow from './create-spreadsheet-row/index.js'; +import createWorksheet from './create-worksheet/index.js'; + +export default [createSpreadsheet, createSpreadsheetRow, createWorksheet]; diff --git a/packages/backend/src/apps/google-sheets/assets/favicon.svg b/packages/backend/src/apps/google-sheets/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..bd5d938c78c543a3a611cc57c19d8ece11ce4bdb --- /dev/null +++ b/packages/backend/src/apps/google-sheets/assets/favicon.svg @@ -0,0 +1,89 @@ + + + + Sheets-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/google-sheets/auth/generate-auth-url.js b/packages/backend/src/apps/google-sheets/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c972ae169d964393279e8942a2f6fd417c87c645 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + prompt: 'select_account', + scope: authScope.join(' '), + response_type: 'code', + access_type: 'offline', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/google-sheets/auth/index.js b/packages/backend/src/apps/google-sheets/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45ea405809f950706951ca303e3d8f5ab793c5c6 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/google-sheets/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/google-sheets/auth/is-still-verified.js b/packages/backend/src/apps/google-sheets/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/google-sheets/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/google-sheets/auth/refresh-token.js b/packages/backend/src/apps/google-sheets/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7020e9ed24981513c8786ed786f136d476eb --- /dev/null +++ b/packages/backend/src/apps/google-sheets/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/google-sheets/auth/verify-credentials.js b/packages/backend/src/apps/google-sheets/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/google-sheets/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/google-sheets/common/add-auth-header.js b/packages/backend/src/apps/google-sheets/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/google-sheets/common/auth-scope.js b/packages/backend/src/apps/google-sheets/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..6c22818ae6c59dae47a845cb09a60fcba8c121af --- /dev/null +++ b/packages/backend/src/apps/google-sheets/common/auth-scope.js @@ -0,0 +1,8 @@ +const authScope = [ + 'https://www.googleapis.com/auth/drive', + 'https://www.googleapis.com/auth/spreadsheets', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/google-sheets/common/get-current-user.js b/packages/backend/src/apps/google-sheets/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/google-sheets/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/google-sheets/dynamic-data/index.js b/packages/backend/src/apps/google-sheets/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e0d6efaccc9fc513f621bb56bdf091a577ee3946 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-data/index.js @@ -0,0 +1,5 @@ +import listDrives from './list-drives/index.js'; +import listSpreadsheets from './list-spreadsheets/index.js'; +import listWorksheets from './list-worksheets/index.js'; + +export default [listDrives, listSpreadsheets, listWorksheets]; diff --git a/packages/backend/src/apps/google-sheets/dynamic-data/list-drives/index.js b/packages/backend/src/apps/google-sheets/dynamic-data/list-drives/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6d34bb70b44dae5d45d875aac90cbaecc56c06bb --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-data/list-drives/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List drives', + key: 'listDrives', + + async run($) { + const drives = { + data: [{ value: null, name: 'My Google Drive' }], + }; + + const params = { + pageSize: 100, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get( + `https://www.googleapis.com/drive/v3/drives`, + { params } + ); + params.pageToken = data.nextPageToken; + + if (data.drives) { + for (const drive of data.drives) { + drives.data.push({ + value: drive.id, + name: drive.name, + }); + } + } + } while (params.pageToken); + + return drives; + }, +}; diff --git a/packages/backend/src/apps/google-sheets/dynamic-data/list-spreadsheets/index.js b/packages/backend/src/apps/google-sheets/dynamic-data/list-spreadsheets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..97568c89f6caefc3872dae59356b1e4b6481d902 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-data/list-spreadsheets/index.js @@ -0,0 +1,43 @@ +export default { + name: 'List spreadsheets', + key: 'listSpreadsheets', + + async run($) { + const spreadsheets = { + data: [], + }; + + const params = { + q: `mimeType='application/vnd.google-apps.spreadsheet'`, + pageSize: 100, + pageToken: undefined, + orderBy: 'createdTime desc', + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get( + `https://www.googleapis.com/drive/v3/files`, + { params } + ); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + spreadsheets.data.push({ + value: file.id, + name: file.name, + }); + } + } + } while (params.pageToken); + + return spreadsheets; + }, +}; diff --git a/packages/backend/src/apps/google-sheets/dynamic-data/list-worksheets/index.js b/packages/backend/src/apps/google-sheets/dynamic-data/list-worksheets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..aee569241f7267cbee76458907bafd494a0e8cf0 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-data/list-worksheets/index.js @@ -0,0 +1,38 @@ +export default { + name: 'List worksheets', + key: 'listWorksheets', + + async run($) { + const spreadsheetId = $.step.parameters.spreadsheetId; + + const worksheets = { + data: [], + }; + + if (!spreadsheetId) { + return worksheets; + } + + const params = { + pageToken: undefined, + }; + + do { + const { data } = await $.http.get(`/v4/spreadsheets/${spreadsheetId}`, { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.sheets?.length) { + for (const sheet of data.sheets) { + worksheets.data.push({ + value: sheet.properties.sheetId, + name: sheet.properties.title, + }); + } + } + } while (params.pageToken); + + return worksheets; + }, +}; diff --git a/packages/backend/src/apps/google-sheets/dynamic-fields/index.js b/packages/backend/src/apps/google-sheets/dynamic-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0a8bc04b9f5cdb6910f0175f718fb6d1bb00349d --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-fields/index.js @@ -0,0 +1,3 @@ +import listSheetHeaders from './list-sheet-headers/index.js'; + +export default [listSheetHeaders]; diff --git a/packages/backend/src/apps/google-sheets/dynamic-fields/list-sheet-headers/index.js b/packages/backend/src/apps/google-sheets/dynamic-fields/list-sheet-headers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cbae65d09dfed73266d2aad3519ed21dc555301a --- /dev/null +++ b/packages/backend/src/apps/google-sheets/dynamic-fields/list-sheet-headers/index.js @@ -0,0 +1,55 @@ +const hasValue = (value) => value !== null && value !== undefined; + +export default { + name: 'List Sheet Headers', + key: 'listSheetHeaders', + + async run($) { + if ( + !hasValue($.step.parameters.spreadsheetId) || + !hasValue($.step.parameters.worksheetId) + ) { + return; + } + + const { + data: { sheets }, + } = await $.http.get(`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`); + + const selectedSheet = sheets.find( + (sheet) => sheet.properties.sheetId === $.step.parameters.worksheetId + ); + + if (!selectedSheet) return; + + const sheetName = selectedSheet.properties.title; + + const range = `${sheetName}!1:1`; + + const params = { + majorDimension: 'ROWS', + }; + + const { data } = await $.http.get( + `/v4/spreadsheets/${$.step.parameters.spreadsheetId}/values/${range}`, + { + params, + } + ); + + if (!data.values) { + return; + } + + const result = data.values[0].map((item, index) => ({ + label: item, + key: `header-${index}`, + type: 'string', + required: false, + value: item, + variables: true, + })); + + return result; + }, +}; diff --git a/packages/backend/src/apps/google-sheets/index.js b/packages/backend/src/apps/google-sheets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9412b5d3a6ef7ed500ad4da50e2bdb1cdd61d5eb --- /dev/null +++ b/packages/backend/src/apps/google-sheets/index.js @@ -0,0 +1,24 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; +import dynamicFields from './dynamic-fields/index.js'; + +export default defineApp({ + name: 'Google Sheets', + key: 'google-sheets', + baseUrl: 'https://docs.google.com/spreadsheets', + apiBaseUrl: 'https://sheets.googleapis.com', + iconUrl: '{BASE_URL}/apps/google-sheets/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/google-sheets/connection', + primaryColor: '0F9D58', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, + dynamicFields, +}); diff --git a/packages/backend/src/apps/google-sheets/triggers/index.js b/packages/backend/src/apps/google-sheets/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1d0683c75b59c6ae498903eed7e1a26c4b1b73e3 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/index.js @@ -0,0 +1,5 @@ +import newSpreadsheets from './new-spreadsheets/index.js'; +import newWorksheets from './new-worksheets/index.js'; +import newSpreadsheetRows from './new-spreadsheet-rows/index.js'; + +export default [newSpreadsheets, newWorksheets, newSpreadsheetRows]; diff --git a/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/index.js b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/index.js new file mode 100644 index 0000000000000000000000000000000000000000..164e79fd01700d9c5393fe8715647138baf4ace9 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/index.js @@ -0,0 +1,82 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newSpreadsheetRows from './new-spreadsheet-rows.js'; + +export default defineTrigger({ + name: 'New spreadsheet rows', + key: 'newSpreadsheetRows', + pollInterval: 15, + description: + 'Triggers when a new row is added to the bottom of a spreadsheet.', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Spreadsheet', + key: 'spreadsheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.driveId'], + description: 'The spreadsheets in your Google Drive.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + { + label: 'Worksheet', + key: 'worksheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.spreadsheetId'], + description: + 'The worksheets in your selected spreadsheet. You must have column headers.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listWorksheets', + }, + { + name: 'parameters.spreadsheetId', + value: '{parameters.spreadsheetId}', + }, + ], + }, + }, + ], + + async run($) { + await newSpreadsheetRows($); + }, +}); diff --git a/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/new-spreadsheet-rows.js b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/new-spreadsheet-rows.js new file mode 100644 index 0000000000000000000000000000000000000000..fd4c2d6cd108c2895eff08b0223a729f7d2f289b --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheet-rows/new-spreadsheet-rows.js @@ -0,0 +1,33 @@ +const newSpreadsheetRows = async ($) => { + const { + data: { sheets }, + } = await $.http.get(`/v4/spreadsheets/${$.step.parameters.spreadsheetId}`); + + const selectedSheet = sheets.find( + (sheet) => sheet.properties.sheetId === $.step.parameters.worksheetId + ); + + if (!selectedSheet) return; + + const sheetName = selectedSheet.properties.title; + + const range = sheetName; + + const { data } = await $.http.get( + `v4/spreadsheets/${$.step.parameters.spreadsheetId}/values/${range}` + ); + + if (data.values?.length) { + for (let index = data.values.length - 1; index > 0; index--) { + const value = data.values[index]; + $.pushTriggerItem({ + raw: { row: value }, + meta: { + internalId: index.toString(), + }, + }); + } + } +}; + +export default newSpreadsheetRows; diff --git a/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/index.js b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..861bdeecb80645598be1afcd3e4be150b0f76598 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/index.js @@ -0,0 +1,34 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newSpreadsheets from './new-spreadsheets.js'; + +export default defineTrigger({ + name: 'New spreadsheets', + key: 'newSpreadsheets', + pollInterval: 15, + description: 'Triggers when you create a new spreadsheet.', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + ], + + async run($) { + await newSpreadsheets($); + }, +}); diff --git a/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/new-spreadsheets.js b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/new-spreadsheets.js new file mode 100644 index 0000000000000000000000000000000000000000..f3d2c4a2edeed3f8fdb2389fb3513cecb76318b5 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-spreadsheets/new-spreadsheets.js @@ -0,0 +1,37 @@ +const newSpreadsheets = async ($) => { + const params = { + pageToken: undefined, + orderBy: 'createdTime desc', + q: `mimeType='application/vnd.google-apps.spreadsheet'`, + fields: '*', + pageSize: 1000, + driveId: $.step.parameters.driveId, + supportsAllDrives: true, + }; + + if ($.step.parameters.driveId) { + params.includeItemsFromAllDrives = true; + params.corpora = 'drive'; + } + + do { + const { data } = await $.http.get( + 'https://www.googleapis.com/drive/v3/files', + { params } + ); + params.pageToken = data.nextPageToken; + + if (data.files?.length) { + for (const file of data.files) { + $.pushTriggerItem({ + raw: file, + meta: { + internalId: file.id, + }, + }); + } + } + } while (params.pageToken); +}; + +export default newSpreadsheets; diff --git a/packages/backend/src/apps/google-sheets/triggers/new-worksheets/index.js b/packages/backend/src/apps/google-sheets/triggers/new-worksheets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8933ade663568e17ad4429c67ff0c55f466496f6 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-worksheets/index.js @@ -0,0 +1,57 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newWorksheets from './new-worksheets.js'; + +export default defineTrigger({ + name: 'New worksheets', + key: 'newWorksheets', + pollInterval: 15, + description: 'Triggers when you create a new worksheet in a spreadsheet.', + arguments: [ + { + label: 'Drive', + key: 'driveId', + type: 'dropdown', + required: false, + description: + 'The Google Drive where your spreadsheet resides. If nothing is selected, then your personal Google Drive will be used.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDrives', + }, + ], + }, + }, + { + label: 'Spreadsheet', + key: 'spreadsheetId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.driveId'], + description: 'The spreadsheets in your Google Drive.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSpreadsheets', + }, + { + name: 'parameters.driveId', + value: '{parameters.driveId}', + }, + ], + }, + }, + ], + + async run($) { + await newWorksheets($); + }, +}); diff --git a/packages/backend/src/apps/google-sheets/triggers/new-worksheets/new-worksheets.js b/packages/backend/src/apps/google-sheets/triggers/new-worksheets/new-worksheets.js new file mode 100644 index 0000000000000000000000000000000000000000..bfc57cc7c091bdab4a923b82eb217d87caa099a8 --- /dev/null +++ b/packages/backend/src/apps/google-sheets/triggers/new-worksheets/new-worksheets.js @@ -0,0 +1,26 @@ +const newWorksheets = async ($) => { + const params = { + pageToken: undefined, + }; + + do { + const { data } = await $.http.get( + `/v4/spreadsheets/${$.step.parameters.spreadsheetId}`, + { params } + ); + params.pageToken = data.nextPageToken; + + if (data.sheets?.length) { + for (const sheet of data.sheets.reverse()) { + $.pushTriggerItem({ + raw: sheet, + meta: { + internalId: sheet.properties.sheetId.toString(), + }, + }); + } + } + } while (params.pageToken); +}; + +export default newWorksheets; diff --git a/packages/backend/src/apps/google-tasks/actions/create-task-list/index.js b/packages/backend/src/apps/google-tasks/actions/create-task-list/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0d2571bd0a66ef8ef0d09e37fc5ad55e288c5886 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/actions/create-task-list/index.js @@ -0,0 +1,31 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create task list', + key: 'createTaskList', + description: 'Creates a new task list.', + arguments: [ + { + label: 'List Title', + key: 'listTitle', + type: 'string', + required: true, + description: '', + variables: true, + }, + ], + + async run($) { + const listTitle = $.step.parameters.listTitle; + + const body = { + title: listTitle, + }; + + const { data } = await $.http.post('/tasks/v1/users/@me/lists', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/actions/create-task/index.js b/packages/backend/src/apps/google-tasks/actions/create-task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2b03a20ebae0fedd3584717ba73775e2e07b5b0e --- /dev/null +++ b/packages/backend/src/apps/google-tasks/actions/create-task/index.js @@ -0,0 +1,70 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create task', + key: 'createTask', + description: 'Creates a new task.', + arguments: [ + { + label: 'Task List', + key: 'taskListId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTaskLists', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Notes', + key: 'notes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Due Date', + key: 'due', + type: 'string', + required: false, + description: 'RFC 3339 timestamp.', + variables: true, + }, + ], + + async run($) { + const { taskListId, title, notes, due } = $.step.parameters; + + const body = { + title, + notes, + due, + }; + + const { data } = await $.http.post( + `/tasks/v1/lists/${taskListId}/tasks`, + body + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/actions/find-task/index.js b/packages/backend/src/apps/google-tasks/actions/find-task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00c3f46da4ec15828d33a1995177bcadcfc8ca67 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/actions/find-task/index.js @@ -0,0 +1,57 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Find task', + key: 'findTask', + description: 'Looking for a specific task.', + arguments: [ + { + label: 'Task List', + key: 'taskListId', + type: 'dropdown', + required: true, + description: 'The list to be searched.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTaskLists', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + ], + + async run($) { + const taskListId = $.step.parameters.taskListId; + const title = $.step.parameters.title; + + const params = { + showCompleted: true, + showHidden: true, + }; + + const { data } = await $.http.get(`/tasks/v1/lists/${taskListId}/tasks`, { + params, + }); + + const filteredTask = data.items?.filter((task) => + task.title.includes(title) + ); + + $.setActionItem({ + raw: filteredTask[0], + }); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/actions/index.js b/packages/backend/src/apps/google-tasks/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ed71df7030193884234f7a43bb9d6b9bdbe00844 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/actions/index.js @@ -0,0 +1,6 @@ +import createTask from './create-task/index.js'; +import createTaskList from './create-task-list/index.js'; +import findTask from './find-task/index.js'; +import updateTask from './update-task/index.js'; + +export default [createTask, createTaskList, findTask, updateTask]; diff --git a/packages/backend/src/apps/google-tasks/actions/update-task/index.js b/packages/backend/src/apps/google-tasks/actions/update-task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e38aefaf7284030d1a0cc37017e95ab0c0a966b5 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/actions/update-task/index.js @@ -0,0 +1,108 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Update task', + key: 'updateTask', + description: 'Updates an existing task.', + arguments: [ + { + label: 'Task List', + key: 'taskListId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTaskLists', + }, + ], + }, + }, + { + label: 'Task', + key: 'taskId', + type: 'dropdown', + required: true, + description: 'Ensure that you choose a list before proceeding.', + variables: true, + dependsOn: ['parameters.taskListId'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTasks', + }, + { + name: 'parameters.taskListId', + value: '{parameters.taskListId}', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: false, + description: 'Provide a new title for the revised task.', + variables: true, + }, + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: false, + description: + 'Specify the status of the updated task. If you opt for a custom value, enter either "needsAttention" or "completed."', + variables: true, + options: [ + { label: 'Incomplete', value: 'needsAction' }, + { label: 'Complete', value: 'completed' }, + ], + }, + { + label: 'Notes', + key: 'notes', + type: 'string', + required: false, + description: 'Provide a note for the revised task.', + variables: true, + }, + { + label: 'Due Date', + key: 'due', + type: 'string', + required: false, + description: + 'Specify the deadline for the task (as a RFC 3339 timestamp).', + variables: true, + }, + ], + + async run($) { + const { taskListId, taskId, title, status, notes, due } = $.step.parameters; + + const body = { + title, + status, + notes, + due, + }; + + const { data } = await $.http.patch( + `/tasks/v1/lists/${taskListId}/tasks/${taskId}`, + body + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/assets/favicon.svg b/packages/backend/src/apps/google-tasks/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..1de5d7ab3e8fa819ad553863f613ee88a867419c --- /dev/null +++ b/packages/backend/src/apps/google-tasks/assets/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/packages/backend/src/apps/google-tasks/auth/generate-auth-url.js b/packages/backend/src/apps/google-tasks/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..c972ae169d964393279e8942a2f6fd417c87c645 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + prompt: 'select_account', + scope: authScope.join(' '), + response_type: 'code', + access_type: 'offline', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/google-tasks/auth/index.js b/packages/backend/src/apps/google-tasks/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..eefda57c958df59d9b0e7017fb3a8c1b6ec71a13 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/google-tasks/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/google-tasks/auth/is-still-verified.js b/packages/backend/src/apps/google-tasks/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/google-tasks/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/google-tasks/auth/refresh-token.js b/packages/backend/src/apps/google-tasks/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..f706ffa73109dfc60c69df7e5035f6b4bdda06bc --- /dev/null +++ b/packages/backend/src/apps/google-tasks/auth/refresh-token.js @@ -0,0 +1,25 @@ +import { URLSearchParams } from 'node:url'; +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/google-tasks/auth/verify-credentials.js b/packages/backend/src/apps/google-tasks/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/google-tasks/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/google-tasks/common/add-auth-header.js b/packages/backend/src/apps/google-tasks/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/google-tasks/common/auth-scope.js b/packages/backend/src/apps/google-tasks/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..030adb803f85f24a31ee456979e38686d1dbcc4f --- /dev/null +++ b/packages/backend/src/apps/google-tasks/common/auth-scope.js @@ -0,0 +1,7 @@ +const authScope = [ + 'https://www.googleapis.com/auth/tasks', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/google-tasks/common/get-current-user.js b/packages/backend/src/apps/google-tasks/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/google-tasks/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/google-tasks/dynamic-data/index.js b/packages/backend/src/apps/google-tasks/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..71940ffd92110cbc18ae575f4faa814c78b50827 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listTaskLists from './list-task-lists/index.js'; +import listTasks from './list-tasks/index.js'; + +export default [listTaskLists, listTasks]; diff --git a/packages/backend/src/apps/google-tasks/dynamic-data/list-task-lists/index.js b/packages/backend/src/apps/google-tasks/dynamic-data/list-task-lists/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a430a48e3ad189a1d2cda787b01717f406f4726d --- /dev/null +++ b/packages/backend/src/apps/google-tasks/dynamic-data/list-task-lists/index.js @@ -0,0 +1,33 @@ +export default { + name: 'List task lists', + key: 'listTaskLists', + + async run($) { + const taskLists = { + data: [], + }; + + const params = { + maxResults: 100, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get('/tasks/v1/users/@me/lists', { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items) { + for (const taskList of data.items) { + taskLists.data.push({ + value: taskList.id, + name: taskList.title, + }); + } + } + } while (params.pageToken); + + return taskLists; + }, +}; diff --git a/packages/backend/src/apps/google-tasks/dynamic-data/list-tasks/index.js b/packages/backend/src/apps/google-tasks/dynamic-data/list-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..534dbdbbbdaa838691511c9807f770fad7a8c615 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/dynamic-data/list-tasks/index.js @@ -0,0 +1,40 @@ +export default { + name: 'List tasks', + key: 'listTasks', + + async run($) { + const tasks = { + data: [], + }; + const taskListId = $.step.parameters.taskListId; + + const params = { + maxResults: 100, + pageToken: undefined, + }; + + if (!taskListId) { + return tasks; + } + + do { + const { data } = await $.http.get(`/tasks/v1/lists/${taskListId}/tasks`, { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items) { + for (const task of data.items) { + if (task.title !== '') { + tasks.data.push({ + value: task.id, + name: task.title, + }); + } + } + } + } while (params.pageToken); + + return tasks; + }, +}; diff --git a/packages/backend/src/apps/google-tasks/index.js b/packages/backend/src/apps/google-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cbc45587120495fa05db0b75f957cf5fe1e9e641 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Google Tasks', + key: 'google-tasks', + baseUrl: 'https://calendar.google.com/calendar/u/0/r/tasks', + apiBaseUrl: 'https://tasks.googleapis.com', + iconUrl: '{BASE_URL}/apps/google-tasks/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/google-tasks/connection', + primaryColor: '0066DA', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, + triggers, +}); diff --git a/packages/backend/src/apps/google-tasks/triggers/index.js b/packages/backend/src/apps/google-tasks/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3677b97cd54830fb9127070e989ac74cc7b96704 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/triggers/index.js @@ -0,0 +1,5 @@ +import newCompletedTasks from './new-completed-tasks/index.js'; +import newTaskLists from './new-task-lists/index.js'; +import newTasks from './new-tasks/index.js'; + +export default [newCompletedTasks, newTaskLists, newTasks]; diff --git a/packages/backend/src/apps/google-tasks/triggers/new-completed-tasks/index.js b/packages/backend/src/apps/google-tasks/triggers/new-completed-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..75979d8c201130e193e865ffa864dd97197b7012 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/triggers/new-completed-tasks/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New completed tasks', + key: 'newCompletedTasks', + pollInterval: 15, + description: 'Triggers when a task is finished within a specified task list.', + arguments: [ + { + label: 'Task List', + key: 'taskListId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTaskLists', + }, + ], + }, + }, + ], + + async run($) { + const taskListId = $.step.parameters.taskListId; + + const params = { + maxResults: 100, + showCompleted: true, + showHidden: true, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get(`/tasks/v1/lists/${taskListId}/tasks`, { + params, + }); + params.pageToken = data.nextPageToken; + + if (data.items?.length) { + for (const task of data.items) { + if (task.status === 'completed') { + $.pushTriggerItem({ + raw: task, + meta: { + internalId: task.id, + }, + }); + } + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/triggers/new-task-lists/index.js b/packages/backend/src/apps/google-tasks/triggers/new-task-lists/index.js new file mode 100644 index 0000000000000000000000000000000000000000..29d156b3bb3932d348599f092b4904175ca77db3 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/triggers/new-task-lists/index.js @@ -0,0 +1,31 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New task lists', + key: 'newTaskLists', + pollInterval: 15, + description: 'Triggers when a new task list is created.', + + async run($) { + const params = { + maxResults: 100, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get('/tasks/v1/users/@me/lists'); + params.pageToken = data.nextPageToken; + + if (data.items?.length) { + for (const taskList of data.items.reverse()) { + $.pushTriggerItem({ + raw: taskList, + meta: { + internalId: taskList.id, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/google-tasks/triggers/new-tasks/index.js b/packages/backend/src/apps/google-tasks/triggers/new-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..497a21e80fbcf71fde46d53988b9869bc3b9d4c0 --- /dev/null +++ b/packages/backend/src/apps/google-tasks/triggers/new-tasks/index.js @@ -0,0 +1,53 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New tasks', + key: 'newTasks', + pollInterval: 15, + description: 'Triggers when a new task is created.', + arguments: [ + { + label: 'Task List', + key: 'taskListId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTaskLists', + }, + ], + }, + }, + ], + + async run($) { + const taskListId = $.step.parameters.taskListId; + + const params = { + maxResults: 100, + pageToken: undefined, + }; + + do { + const { data } = await $.http.get(`/tasks/v1/lists/${taskListId}/tasks`); + params.pageToken = data.nextPageToken; + + if (data.items?.length) { + for (const task of data.items) { + $.pushTriggerItem({ + raw: task, + meta: { + internalId: task.id, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/helix/actions/index.js b/packages/backend/src/apps/helix/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8c1bb49b372f2ff3fe783b8838215c570c5690c4 --- /dev/null +++ b/packages/backend/src/apps/helix/actions/index.js @@ -0,0 +1,3 @@ +import newChat from './new-chat/index.js'; + +export default [newChat]; diff --git a/packages/backend/src/apps/helix/actions/new-chat/index.js b/packages/backend/src/apps/helix/actions/new-chat/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c430fbb0434728d12d4be4a09ba33b7b24974b54 --- /dev/null +++ b/packages/backend/src/apps/helix/actions/new-chat/index.js @@ -0,0 +1,55 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'New chat', + key: 'newChat', + description: 'Create a new chat session for Helix AI.', + arguments: [ + { + label: 'Session ID', + key: 'sessionId', + type: 'string', + required: false, + description: + 'ID of the chat session to continue. Leave empty to start a new chat.', + variables: true, + }, + { + label: 'System Prompt', + key: 'systemPrompt', + type: 'string', + required: false, + description: + 'Optional system prompt to start the chat with. It will be used only for new chat sessions.', + variables: true, + }, + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + description: 'User input to start the chat with.', + variables: true, + }, + ], + + async run($) { + const response = await $.http.post('/api/v1/sessions/chat', { + session_id: $.step.parameters.sessionId, + system: $.step.parameters.systemPrompt, + messages: [ + { + role: 'user', + content: { + content_type: 'text', + parts: [$.step.parameters.input], + }, + }, + ], + }); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/helix/assets/favicon.svg b/packages/backend/src/apps/helix/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e5e0b0b3887494f20e46232ee6168c8d32e205ab --- /dev/null +++ b/packages/backend/src/apps/helix/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/backend/src/apps/helix/auth/index.js b/packages/backend/src/apps/helix/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3850ff20f4bc237a0478fe2805a2075af627a40d --- /dev/null +++ b/packages/backend/src/apps/helix/auth/index.js @@ -0,0 +1,45 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'instanceUrl', + label: 'Helix instance URL', + type: 'string', + required: false, + readOnly: false, + value: 'https://app.tryhelix.ai', + placeholder: 'https://app.tryhelix.ai', + description: + 'Your Helix instance URL. Default is https://app.tryhelix.ai.', + clickToCopy: true, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Helix API Key of your account.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/helix/auth/is-still-verified.js b/packages/backend/src/apps/helix/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/helix/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/helix/auth/verify-credentials.js b/packages/backend/src/apps/helix/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..c8f53459179a2dfb9d90f4f75f7933e4bc2d8e72 --- /dev/null +++ b/packages/backend/src/apps/helix/auth/verify-credentials.js @@ -0,0 +1,9 @@ +const verifyCredentials = async ($) => { + await $.http.get('/api/v1/sessions'); + + await $.auth.set({ + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/helix/common/add-auth-header.js b/packages/backend/src/apps/helix/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..59ecf6b0d6dfca61acb3db1326aae1a0d2a566e8 --- /dev/null +++ b/packages/backend/src/apps/helix/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + const authorizationHeader = `Bearer ${$.auth.data.apiKey}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/helix/common/set-base-url.js b/packages/backend/src/apps/helix/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..135149b1ad4ffed1d24996b6f531ba1014a9fa56 --- /dev/null +++ b/packages/backend/src/apps/helix/common/set-base-url.js @@ -0,0 +1,11 @@ +const setBaseUrl = ($, requestConfig) => { + if ($.auth.data.instanceUrl) { + requestConfig.baseURL = $.auth.data.instanceUrl; + } else if ($.app.apiBaseUrl) { + requestConfig.baseURL = $.app.apiBaseUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/helix/index.js b/packages/backend/src/apps/helix/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9915abcde782e1dbc42e256e85e5f69c3e8c4151 --- /dev/null +++ b/packages/backend/src/apps/helix/index.js @@ -0,0 +1,19 @@ +import defineApp from '../../helpers/define-app.js'; +import setBaseUrl from './common/set-base-url.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Helix', + key: 'helix', + baseUrl: 'https://tryhelix.ai', + apiBaseUrl: 'https://app.tryhelix.ai', + iconUrl: '{BASE_URL}/apps/helix/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/helix/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/http-request/actions/custom-request/index.js b/packages/backend/src/apps/http-request/actions/custom-request/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c43b379239810a6baa03acaae7550ae692fad617 --- /dev/null +++ b/packages/backend/src/apps/http-request/actions/custom-request/index.js @@ -0,0 +1,150 @@ +import defineAction from '../../../../helpers/define-action.js'; + +function isPossiblyTextBased(contentType) { + if (!contentType) return false; + + return ( + contentType.startsWith('application/json') || + contentType.startsWith('text/') + ); +} + +function throwIfFileSizeExceedsLimit(contentLength) { + const maxFileSize = 25 * 1024 * 1024; // 25MB + + if (Number(contentLength) > maxFileSize) { + throw new Error( + `Response is too large. Maximum size is 25MB. Actual size is ${contentLength}` + ); + } +} + +export default defineAction({ + name: 'Custom request', + key: 'customRequest', + description: 'Makes a custom HTTP request by providing raw details.', + arguments: [ + { + label: 'Method', + key: 'method', + type: 'dropdown', + required: true, + description: `The HTTP method we'll use to perform the request.`, + value: 'GET', + options: [ + { label: 'DELETE', value: 'DELETE' }, + { label: 'GET', value: 'GET' }, + { label: 'PATCH', value: 'PATCH' }, + { label: 'POST', value: 'POST' }, + { label: 'PUT', value: 'PUT' }, + ], + }, + { + label: 'URL', + key: 'url', + type: 'string', + required: true, + description: 'Any URL with a querystring will be re-encoded properly.', + variables: true, + }, + { + label: 'Data', + key: 'data', + type: 'string', + required: false, + description: 'Place raw JSON data here.', + variables: true, + }, + { + label: 'Headers', + key: 'headers', + type: 'dynamic', + required: false, + description: 'Add or remove headers as needed', + value: [ + { + key: 'Content-Type', + value: 'application/json', + }, + ], + fields: [ + { + label: 'Key', + key: 'key', + type: 'string', + required: true, + description: 'Header key', + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + description: 'Header value', + variables: true, + }, + ], + }, + ], + + async run($) { + const method = $.step.parameters.method; + const data = $.step.parameters.data || null; + const url = $.step.parameters.url; + const headers = $.step.parameters.headers; + + const headersObject = headers.reduce((result, entry) => { + const key = entry.key?.toLowerCase(); + const value = entry.value; + + if (key && value) { + return { + ...result, + [entry.key?.toLowerCase()]: entry.value, + }; + } + + return result; + }, {}); + + let expectedResponseContentType = headersObject.accept; + + // in case HEAD request is not supported by the URL + try { + const metadataResponse = await $.http.head(url, { + headers: headersObject, + }); + + if (!expectedResponseContentType) { + expectedResponseContentType = metadataResponse.headers['content-type']; + } + + throwIfFileSizeExceedsLimit(metadataResponse.headers['content-length']); + // eslint-disable-next-line no-empty + } catch {} + + const requestData = { + url, + method, + data, + headers: headersObject, + }; + + if (!isPossiblyTextBased(expectedResponseContentType)) { + requestData.responseType = 'arraybuffer'; + } + + const response = await $.http.request(requestData); + + throwIfFileSizeExceedsLimit(response.headers['content-length']); + + let responseData = response.data; + + if (!isPossiblyTextBased(expectedResponseContentType)) { + responseData = Buffer.from(responseData).toString('base64'); + } + + $.setActionItem({ raw: { data: responseData } }); + }, +}); diff --git a/packages/backend/src/apps/http-request/actions/index.js b/packages/backend/src/apps/http-request/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0396f241ce033e8c36bb6a19a9dbc6e5307d69df --- /dev/null +++ b/packages/backend/src/apps/http-request/actions/index.js @@ -0,0 +1,3 @@ +import customRequest from './custom-request/index.js'; + +export default [customRequest]; diff --git a/packages/backend/src/apps/http-request/assets/favicon.svg b/packages/backend/src/apps/http-request/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..87c7dae5ef2e6f47c586c6d80758299484783fa9 --- /dev/null +++ b/packages/backend/src/apps/http-request/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/http-request/index.js b/packages/backend/src/apps/http-request/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7854c08acff7c2bcbd42b627de8be7d940fd62ae --- /dev/null +++ b/packages/backend/src/apps/http-request/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'HTTP Request', + key: 'http-request', + iconUrl: '{BASE_URL}/apps/http-request/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/http-request/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '000000', + actions, +}); diff --git a/packages/backend/src/apps/hubspot/actions/create-contact/index.js b/packages/backend/src/apps/hubspot/actions/create-contact/index.js new file mode 100644 index 0000000000000000000000000000000000000000..790854a169f5a53775a2b73c2f2fbb704870b7c1 --- /dev/null +++ b/packages/backend/src/apps/hubspot/actions/create-contact/index.js @@ -0,0 +1,83 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create contact', + key: 'createContact', + description: `Create contact on user's account.`, + arguments: [ + { + label: 'Company name', + key: 'company', + type: 'string', + required: false, + variables: true, + }, + { + label: 'Email', + key: 'email', + type: 'string', + required: false, + variables: true, + }, + { + label: 'First name', + key: 'firstName', + type: 'string', + required: false, + variables: true, + }, + { + label: 'Last name', + key: 'lastName', + type: 'string', + required: false, + description: 'Last name', + variables: true, + }, + { + label: 'Phone', + key: 'phone', + type: 'string', + required: false, + variables: true, + }, + { + label: 'Website URL', + key: 'website', + type: 'string', + required: false, + variables: true, + }, + { + label: 'Owner ID', + key: 'hubspotOwnerId', + type: 'string', + required: false, + variables: true, + }, + ], + + async run($) { + const company = $.step.parameters.company; + const email = $.step.parameters.email; + const firstName = $.step.parameters.firstName; + const lastName = $.step.parameters.lastName; + const phone = $.step.parameters.phone; + const website = $.step.parameters.website; + const hubspotOwnerId = $.step.parameters.hubspotOwnerId; + + const response = await $.http.post(`crm/v3/objects/contacts`, { + properties: { + company, + email, + firstname: firstName, + lastname: lastName, + phone, + website, + hubspot_owner_id: hubspotOwnerId, + }, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/hubspot/actions/index.js b/packages/backend/src/apps/hubspot/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9dbfbd0694efeea082fdd8386215012ee47e3c70 --- /dev/null +++ b/packages/backend/src/apps/hubspot/actions/index.js @@ -0,0 +1,3 @@ +import createContact from './create-contact/index.js'; + +export default [createContact]; diff --git a/packages/backend/src/apps/hubspot/assets/favicon.svg b/packages/backend/src/apps/hubspot/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..c21891fb37ddcfa5b0dbf15e0e3aa62d1b289fcc --- /dev/null +++ b/packages/backend/src/apps/hubspot/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/backend/src/apps/hubspot/auth/generate-auth-url.js b/packages/backend/src/apps/hubspot/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..3b7bf2e89ce947f93f21f04fe9fac001c701296b --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/generate-auth-url.js @@ -0,0 +1,19 @@ +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const callbackUrl = oauthRedirectUrlField.value; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: callbackUrl, + scope: scopes.join(' '), + }); + + const url = `https://app.hubspot.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ url }); +} diff --git a/packages/backend/src/apps/hubspot/auth/index.js b/packages/backend/src/apps/hubspot/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..47f75638ae0fa14d32a6cd15c17d4d0242a1fbc4 --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/hubspot/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in HubSpot OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/hubspot/auth/is-still-verified.js b/packages/backend/src/apps/hubspot/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..7927d274417361298ffee9f38878e46e310effce --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import getAccessTokenInfo from '../common/get-access-token-info.js'; + +const isStillVerified = async ($) => { + await getAccessTokenInfo($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/hubspot/auth/refresh-token.js b/packages/backend/src/apps/hubspot/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..9afea4e1f1e0779c2280b88fce5d834fe3450346 --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/refresh-token.js @@ -0,0 +1,27 @@ +import { URLSearchParams } from 'url'; + +const refreshToken = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + redirect_uri: callbackUrl, + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post('/oauth/v1/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/hubspot/auth/verify-credentials.js b/packages/backend/src/apps/hubspot/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..15e44b4ea9391c993d177fae6825bdd7d11fa1f7 --- /dev/null +++ b/packages/backend/src/apps/hubspot/auth/verify-credentials.js @@ -0,0 +1,51 @@ +import { URLSearchParams } from 'url'; +import getAccessTokenInfo from '../common/get-access-token-info.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const callbackUrl = oauthRedirectUrlField.value; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + redirect_uri: callbackUrl, + code: $.auth.data.code, + }); + + const { data: verifiedCredentials } = await $.http.post( + '/oauth/v1/token', + params.toString() + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + } = verifiedCredentials; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + }); + + const accessTokenInfo = await getAccessTokenInfo($); + + await $.auth.set({ + screenName: accessTokenInfo.user, + hubDomain: accessTokenInfo.hub_domain, + scopes: accessTokenInfo.scopes, + scopeToScopeGroupPks: accessTokenInfo.scope_to_scope_group_pks, + trialScopes: accessTokenInfo.trial_scopes, + trialScopeToScoreGroupPks: accessTokenInfo.trial_scope_to_scope_group_pks, + hubId: accessTokenInfo.hub_id, + appId: accessTokenInfo.app_id, + userId: accessTokenInfo.user_id, + expiresIn: accessTokenInfo.expires_in, + tokenType: accessTokenInfo.token_type, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/hubspot/common/add-auth-header.js b/packages/backend/src/apps/hubspot/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..38e690943f2fcca9530b7af9cf532b637162ccb6 --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/add-auth-header.js @@ -0,0 +1,13 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; + + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/hubspot/common/get-access-token-info.js b/packages/backend/src/apps/hubspot/common/get-access-token-info.js new file mode 100644 index 0000000000000000000000000000000000000000..fec4f64064128e3f1e1edbdb4a8a21317454fbe9 --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/get-access-token-info.js @@ -0,0 +1,9 @@ +const getAccessTokenInfo = async ($) => { + const response = await $.http.get( + `/oauth/v1/access-tokens/${$.auth.data.accessToken}` + ); + + return response.data; +}; + +export default getAccessTokenInfo; diff --git a/packages/backend/src/apps/hubspot/common/scopes.js b/packages/backend/src/apps/hubspot/common/scopes.js new file mode 100644 index 0000000000000000000000000000000000000000..38cb30a3ec76cedc29387d1ca741245626a20f9a --- /dev/null +++ b/packages/backend/src/apps/hubspot/common/scopes.js @@ -0,0 +1,3 @@ +const scopes = ['crm.objects.contacts.read', 'crm.objects.contacts.write']; + +export default scopes; diff --git a/packages/backend/src/apps/hubspot/index.js b/packages/backend/src/apps/hubspot/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c34c6d74e7b9ef871723071a1db5d70291a525ad --- /dev/null +++ b/packages/backend/src/apps/hubspot/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import actions from './actions/index.js'; +import auth from './auth/index.js'; + +export default defineApp({ + name: 'HubSpot', + key: 'hubspot', + iconUrl: '{BASE_URL}/apps/hubspot/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/hubspot/connection', + supportsConnections: true, + baseUrl: 'https://www.hubspot.com', + apiBaseUrl: 'https://api.hubapi.com', + primaryColor: 'F95C35', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-client/fields.js b/packages/backend/src/apps/invoice-ninja/actions/create-client/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..419e45bc939ee333723cd62157e55cfb3fc8b11d --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-client/fields.js @@ -0,0 +1,639 @@ +export const fields = [ + { + label: 'Client Name', + key: 'clientName', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Contact First Name', + key: 'contactFirstName', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Contact Last Name', + key: 'contactLastName', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Contact Email', + key: 'contactEmail', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Contact Phone', + key: 'contactPhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Language Code', + key: 'languageCode', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { value: 1, label: 'English - United States' }, + { value: 2, label: 'Italian' }, + { value: 3, label: 'German' }, + { value: 4, label: 'French' }, + { value: 5, label: 'Portuguese - Brazilian' }, + { value: 6, label: 'Dutch' }, + { value: 7, label: 'Spanish' }, + { value: 8, label: 'Norwegian' }, + { value: 9, label: 'Danish' }, + { value: 10, label: 'Japanese' }, + { value: 11, label: 'Swedish' }, + { value: 12, label: 'Spanish - Spain' }, + { value: 13, label: 'French - Canada' }, + { value: 14, label: 'Lithuanian' }, + { value: 15, label: 'Polish' }, + { value: 16, label: 'Czech' }, + { value: 17, label: 'Croatian' }, + { value: 18, label: 'Albanian' }, + { value: 19, label: 'Greek' }, + { value: 20, label: 'English - United Kingdom' }, + { value: 21, label: 'Portuguese - Portugal' }, + { value: 22, label: 'Slovenian' }, + { value: 23, label: 'Finnish' }, + { value: 24, label: 'Romanian' }, + { value: 25, label: 'Turkish - Turkey' }, + { value: 26, label: 'Thai' }, + { value: 27, label: 'Macedonian' }, + { value: 28, label: 'Chinese - Taiwan' }, + { value: 29, label: 'Russian (Russia)' }, + { value: 30, label: 'Arabic' }, + { value: 31, label: 'Persian' }, + { value: 32, label: 'Latvian' }, + { value: 33, label: 'Serbian' }, + { value: 34, label: 'Slovak' }, + { value: 35, label: 'Estonian' }, + { value: 36, label: 'Bulgarian' }, + { value: 37, label: 'Hebrew' }, + { value: 38, label: 'Khmer' }, + { value: 39, label: 'Hungarian' }, + { value: 40, label: 'French - Swiss' }, + ], + }, + { + label: 'Currency Code', + key: 'currencyCode', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { value: 1, label: 'US Dollar' }, + { value: 2, label: 'British Pound' }, + { value: 3, label: 'Euro' }, + { value: 4, label: 'South African Rand' }, + { value: 5, label: 'Danish Krone' }, + { value: 6, label: 'Israeli Shekel' }, + { value: 7, label: 'Swedish Krona' }, + { value: 8, label: 'Kenyan Shilling' }, + { value: 9, label: 'Canadian Dollar' }, + { value: 10, label: 'Philippine Peso' }, + { value: 11, label: 'Indian Rupee' }, + { value: 12, label: 'Australian Dollar' }, + { value: 13, label: 'Singapore Dollar' }, + { value: 14, label: 'Norske Kroner' }, + { value: 15, label: 'New Zealand Dollar' }, + { value: 16, label: 'Vietnamese Dong' }, + { value: 17, label: 'Swiss Franc' }, + { value: 18, label: 'Guatemalan Quetzal' }, + { value: 19, label: 'Malaysian Ringgit' }, + { value: 20, label: 'Brazilian Real' }, + { value: 21, label: 'Thai Baht' }, + { value: 22, label: 'Nigerian Naira' }, + { value: 23, label: 'Argentine Peso' }, + { value: 24, label: 'Bangladeshi Taka' }, + { value: 25, label: 'United Arab Emirates Dirham' }, + { value: 26, label: 'Hong Kong Dollar' }, + { value: 27, label: 'Indonesian Rupiah' }, + { value: 28, label: 'Mexican Peso' }, + { value: 29, label: 'Egyptian Pound' }, + { value: 30, label: 'Colombian Peso' }, + { value: 31, label: 'West African Franc' }, + { value: 32, label: 'Chinese Renminbi' }, + { value: 33, label: 'Rwandan Franc' }, + { value: 34, label: 'Tanzanian Shilling' }, + { value: 35, label: 'Netherlands Antillean Guilder' }, + { value: 36, label: 'Trinidad and Tobago Dollar' }, + { value: 37, label: 'East Caribbean Dollar' }, + { value: 38, label: 'Ghanaian Cedi' }, + { value: 39, label: 'Bulgarian Lev' }, + { value: 40, label: 'Aruban Florin' }, + { value: 41, label: 'Turkish Lira' }, + { value: 42, label: 'Romanian New Leu' }, + { value: 43, label: 'Croatian Kuna' }, + { value: 44, label: 'Saudi Riyal' }, + { value: 45, label: 'Japanese Yen' }, + { value: 46, label: 'Maldivian Rufiyaa' }, + { value: 47, label: 'Costa Rican ColΓ³n' }, + { value: 48, label: 'Pakistani Rupee' }, + { value: 49, label: 'Polish Zloty' }, + { value: 50, label: 'Sri Lankan Rupee' }, + { value: 51, label: 'Czech Koruna' }, + { value: 52, label: 'Uruguayan Peso' }, + { value: 53, label: 'Namibian Dollar' }, + { value: 54, label: 'Tunisian Dinar' }, + { value: 55, label: 'Russian Ruble' }, + { value: 56, label: 'Mozambican Metical' }, + { value: 57, label: 'Omani Rial' }, + { value: 58, label: 'Ukrainian Hryvnia' }, + { value: 59, label: 'Macanese Pataca' }, + { value: 60, label: 'Taiwan New Dollar' }, + { value: 61, label: 'Dominican Peso' }, + { value: 62, label: 'Chilean Peso' }, + { value: 63, label: 'Icelandic KrΓ³na' }, + { value: 64, label: 'Papua New Guinean Kina' }, + { value: 65, label: 'Jordanian Dinar' }, + { value: 66, label: 'Myanmar Kyat' }, + { value: 67, label: 'Peruvian Sol' }, + { value: 68, label: 'Botswana Pula' }, + { value: 69, label: 'Hungarian Forint' }, + { value: 70, label: 'Ugandan Shilling' }, + { value: 71, label: 'Barbadian Dollar' }, + { value: 72, label: 'Brunei Dollar' }, + { value: 73, label: 'Georgian Lari' }, + { value: 74, label: 'Qatari Riyal' }, + { value: 75, label: 'Honduran Lempira' }, + { value: 76, label: 'Surinamese Dollar' }, + { value: 77, label: 'Bahraini Dinar' }, + { value: 78, label: 'Venezuelan Bolivars' }, + { value: 79, label: 'South Korean Won' }, + { value: 80, label: 'Moroccan Dirham' }, + { value: 81, label: 'Jamaican Dollar' }, + { value: 82, label: 'Angolan Kwanza' }, + { value: 83, label: 'Haitian Gourde' }, + { value: 84, label: 'Zambian Kwacha' }, + { value: 85, label: 'Nepalese Rupee' }, + { value: 86, label: 'CFP Franc' }, + { value: 87, label: 'Mauritian Rupee' }, + { value: 88, label: 'Cape Verdean Escudo' }, + { value: 89, label: 'Kuwaiti Dinar' }, + { value: 90, label: 'Algerian Dinar' }, + { value: 91, label: 'Macedonian Denar' }, + { value: 92, label: 'Fijian Dollar' }, + { value: 93, label: 'Bolivian Boliviano' }, + { value: 94, label: 'Albanian Lek' }, + { value: 95, label: 'Serbian Dinar' }, + { value: 96, label: 'Lebanese Pound' }, + { value: 97, label: 'Armenian Dram' }, + { value: 98, label: 'Azerbaijan Manat' }, + { value: 99, label: 'Bosnia and Herzegovina Convertible Mark' }, + { value: 100, label: 'Belarusian Ruble' }, + { value: 101, label: 'Gibraltar Pound' }, + { value: 102, label: 'Moldovan Leu' }, + { value: 103, label: 'Kazakhstani Tenge' }, + { value: 104, label: 'Ethiopian Birr' }, + { value: 105, label: 'Gambia Dalasi' }, + { value: 106, label: 'Paraguayan Guarani' }, + { value: 107, label: 'Malawi Kwacha' }, + { value: 108, label: 'Zimbabwean Dollar' }, + { value: 109, label: 'Cambodian Riel' }, + { value: 110, label: 'Vanuatu Vatu' }, + { value: 111, label: 'Cuban Peso' }, + { value: 112, label: 'Cayman Island Dollar' }, + { value: 113, label: 'Swazi lilangeni' }, + { value: 114, label: 'BZ Dollar' }, + { value: 115, label: 'Libyan Dinar' }, + { value: 116, label: 'Silver Troy Ounce' }, + { value: 117, label: 'Gold Troy Ounce' }, + { value: 118, label: 'Nicaraguan CΓ³rdoba' }, + ], + }, + { + label: 'Id Number', + key: 'idNumber', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Vat Number', + key: 'vatNumber', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Street Address', + key: 'streetAddress', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Apt/Suite', + key: 'aptSuite', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'City', + key: 'city', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'State/Province', + key: 'stateProvince', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Postal Code', + key: 'postalCode', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Country Code', + key: 'countryCode', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { value: 4, label: 'Afghanistan' }, + { value: 8, label: 'Albania' }, + { value: 12, label: 'Algeria' }, + { value: 16, label: 'American Samoa' }, + { value: 20, label: 'Andorra' }, + { value: 24, label: 'Angola' }, + { value: 660, label: 'Anguilla' }, + { value: 10, label: 'Antarctica' }, + { value: 28, label: 'Antigua and Barbuda' }, + { value: 32, label: 'Argentina' }, + { value: 51, label: 'Armenia' }, + { value: 533, label: 'Aruba' }, + { value: 36, label: 'Australia' }, + { value: 40, label: 'Austria' }, + { value: 31, label: 'Azerbaijan' }, + { value: 44, label: 'Bahamas' }, + { value: 48, label: 'Bahrain' }, + { value: 50, label: 'Bangladesh' }, + { value: 52, label: 'Barbados' }, + { value: 112, label: 'Belarus' }, + { value: 56, label: 'Belgium' }, + { value: 84, label: 'Belize' }, + { value: 204, label: 'Benin' }, + { value: 60, label: 'Bermuda' }, + { value: 64, label: 'Bhutan' }, + { value: 68, label: 'Bolivia, Plurinational State of' }, + { value: 535, label: 'Bonaire, Sint Eustatius and Saba' }, + { value: 70, label: 'Bosnia and Herzegovina' }, + { value: 72, label: 'Botswana' }, + { value: 74, label: 'Bouvet Island' }, + { value: 76, label: 'Brazil' }, + { value: 86, label: 'British Indian Ocean Territory' }, + { value: 96, label: 'Brunei Darussalam' }, + { value: 100, label: 'Bulgaria' }, + { value: 854, label: 'Burkina Faso' }, + { value: 108, label: 'Burundi' }, + { value: 116, label: 'Cambodia' }, + { value: 120, label: 'Cameroon' }, + { value: 124, label: 'Canada' }, + { value: 132, label: 'Cape Verde' }, + { value: 136, label: 'Cayman Islands' }, + { value: 140, label: 'Central African Republic' }, + { value: 148, label: 'Chad' }, + { value: 152, label: 'Chile' }, + { value: 156, label: 'China' }, + { value: 162, label: 'Christmas Island' }, + { value: 166, label: 'Cocos (Keeling) Islands' }, + { value: 170, label: 'Colombia' }, + { value: 174, label: 'Comoros' }, + { value: 178, label: 'Congo' }, + { value: 180, label: 'Congo, the Democratic Republic of the' }, + { value: 184, label: 'Cook Islands' }, + { value: 188, label: 'Costa Rica' }, + { value: 191, label: 'Croatia' }, + { value: 192, label: 'Cuba' }, + { value: 531, label: 'CuraΓ§ao' }, + { value: 196, label: 'Cyprus' }, + { value: 203, label: 'Czech Republic' }, + { value: 384, label: "CΓ΄te d'Ivoire" }, + { value: 208, label: 'Denmark' }, + { value: 262, label: 'Djibouti' }, + { value: 212, label: 'Dominica' }, + { value: 214, label: 'Dominican Republic' }, + { value: 218, label: 'Ecuador' }, + { value: 818, label: 'Egypt' }, + { value: 222, label: 'El Salvador' }, + { value: 226, label: 'Equatorial Guinea' }, + { value: 232, label: 'Eritrea' }, + { value: 233, label: 'Estonia' }, + { value: 231, label: 'Ethiopia' }, + { value: 238, label: 'Falkland Islands (Malvinas)' }, + { value: 234, label: 'Faroe Islands' }, + { value: 242, label: 'Fiji' }, + { value: 246, label: 'Finland' }, + { value: 250, label: 'France' }, + { value: 254, label: 'French Guiana' }, + { value: 258, label: 'French Polynesia' }, + { value: 260, label: 'French Southern Territories' }, + { value: 266, label: 'Gabon' }, + { value: 270, label: 'Gambia' }, + { value: 268, label: 'Georgia' }, + { value: 276, label: 'Germany' }, + { value: 288, label: 'Ghana' }, + { value: 292, label: 'Gibraltar' }, + { value: 300, label: 'Greece' }, + { value: 304, label: 'Greenland' }, + { value: 308, label: 'Grenada' }, + { value: 312, label: 'Guadeloupe' }, + { value: 316, label: 'Guam' }, + { value: 320, label: 'Guatemala' }, + { value: 831, label: 'Guernsey' }, + { value: 324, label: 'Guinea' }, + { value: 624, label: 'Guinea-Bissau' }, + { value: 328, label: 'Guyana' }, + { value: 332, label: 'Haiti' }, + { value: 334, label: 'Heard Island and McDonald Islands' }, + { value: 336, label: 'Holy See (Vatican City State)' }, + { value: 340, label: 'Honduras' }, + { value: 344, label: 'Hong Kong' }, + { value: 348, label: 'Hungary' }, + { value: 352, label: 'Iceland' }, + { value: 356, label: 'India' }, + { value: 360, label: 'Indonesia' }, + { value: 364, label: 'Iran, Islamic Republic of' }, + { value: 368, label: 'Iraq' }, + { value: 372, label: 'Ireland' }, + { value: 833, label: 'Isle of Man' }, + { value: 376, label: 'Israel' }, + { value: 380, label: 'Italy' }, + { value: 388, label: 'Jamaica' }, + { value: 392, label: 'Japan' }, + { value: 832, label: 'Jersey' }, + { value: 400, label: 'Jordan' }, + { value: 398, label: 'Kazakhstan' }, + { value: 404, label: 'Kenya' }, + { value: 296, label: 'Kiribati' }, + { value: 408, label: "Korea, Democratic People's Republic of" }, + { value: 410, label: 'Korea, Republic of' }, + { value: 414, label: 'Kuwait' }, + { value: 417, label: 'Kyrgyzstan' }, + { value: 418, label: "Lao People's Democratic Republic" }, + { value: 428, label: 'Latvia' }, + { value: 422, label: 'Lebanon' }, + { value: 426, label: 'Lesotho' }, + { value: 430, label: 'Liberia' }, + { value: 434, label: 'Libya' }, + { value: 438, label: 'Liechtenstein' }, + { value: 440, label: 'Lithuania' }, + { value: 442, label: 'Luxembourg' }, + { value: 446, label: 'Macao' }, + { value: 807, label: 'Macedonia, the former Yugoslav Republic of' }, + { value: 450, label: 'Madagascar' }, + { value: 454, label: 'Malawi' }, + { value: 458, label: 'Malaysia' }, + { value: 462, label: 'Maldives' }, + { value: 466, label: 'Mali' }, + { value: 470, label: 'Malta' }, + { value: 584, label: 'Marshall Islands' }, + { value: 474, label: 'Martinique' }, + { value: 478, label: 'Mauritania' }, + { value: 480, label: 'Mauritius' }, + { value: 175, label: 'Mayotte' }, + { value: 484, label: 'Mexico' }, + { value: 583, label: 'Micronesia, Federated States of' }, + { value: 498, label: 'Moldova, Republic of' }, + { value: 492, label: 'Monaco' }, + { value: 496, label: 'Mongolia' }, + { value: 499, label: 'Montenegro' }, + { value: 500, label: 'Montserrat' }, + { value: 504, label: 'Morocco' }, + { value: 508, label: 'Mozambique' }, + { value: 104, label: 'Myanmar' }, + { value: 516, label: 'Namibia' }, + { value: 520, label: 'Nauru' }, + { value: 524, label: 'Nepal' }, + { value: 528, label: 'Netherlands' }, + { value: 540, label: 'New Caledonia' }, + { value: 554, label: 'New Zealand' }, + { value: 558, label: 'Nicaragua' }, + { value: 562, label: 'Niger' }, + { value: 566, label: 'Nigeria' }, + { value: 570, label: 'Niue' }, + { value: 574, label: 'Norfolk Island' }, + { value: 580, label: 'Northern Mariana Islands' }, + { value: 578, label: 'Norway' }, + { value: 512, label: 'Oman' }, + { value: 586, label: 'Pakistan' }, + { value: 585, label: 'Palau' }, + { value: 275, label: 'Palestine' }, + { value: 591, label: 'Panama' }, + { value: 598, label: 'Papua New Guinea' }, + { value: 600, label: 'Paraguay' }, + { value: 604, label: 'Peru' }, + { value: 608, label: 'Philippines' }, + { value: 612, label: 'Pitcairn' }, + { value: 616, label: 'Poland' }, + { value: 620, label: 'Portugal' }, + { value: 630, label: 'Puerto Rico' }, + { value: 634, label: 'Qatar' }, + { value: 642, label: 'Romania' }, + { value: 643, label: 'Russian Federation' }, + { value: 646, label: 'Rwanda' }, + { value: 638, label: 'RΓ©union' }, + { value: 652, label: 'Saint BarthΓ©lemy' }, + { value: 654, label: 'Saint Helena, Ascension and Tristan da Cunha' }, + { value: 659, label: 'Saint Kitts and Nevis' }, + { value: 662, label: 'Saint Lucia' }, + { value: 663, label: 'Saint Martin (French part)' }, + { value: 666, label: 'Saint Pierre and Miquelon' }, + { value: 670, label: 'Saint Vincent and the Grenadines' }, + { value: 882, label: 'Samoa' }, + { value: 674, label: 'San Marino' }, + { value: 678, label: 'Sao Tome and Principe' }, + { value: 682, label: 'Saudi Arabia' }, + { value: 686, label: 'Senegal' }, + { value: 688, label: 'Serbia' }, + { value: 690, label: 'Seychelles' }, + { value: 694, label: 'Sierra Leone' }, + { value: 702, label: 'Singapore' }, + { value: 534, label: 'Sint Maarten (Dutch part)' }, + { value: 703, label: 'Slovakia' }, + { value: 705, label: 'Slovenia' }, + { value: 90, label: 'Solomon Islands' }, + { value: 706, label: 'Somalia' }, + { value: 710, label: 'South Africa' }, + { value: 239, label: 'South Georgia and the South Sandwich Islands' }, + { value: 728, label: 'South Sudan' }, + { value: 724, label: 'Spain' }, + { value: 144, label: 'Sri Lanka' }, + { value: 729, label: 'Sudan' }, + { value: 740, label: 'Suriname' }, + { value: 744, label: 'Svalbard and Jan Mayen' }, + { value: 748, label: 'Swaziland' }, + { value: 752, label: 'Sweden' }, + { value: 756, label: 'Switzerland' }, + { value: 760, label: 'Syrian Arab Republic' }, + { value: 158, label: 'Taiwan, Province of China' }, + { value: 762, label: 'Tajikistan' }, + { value: 834, label: 'Tanzania, United Republic of' }, + { value: 764, label: 'Thailand' }, + { value: 626, label: 'Timor-Leste' }, + { value: 768, label: 'Togo' }, + { value: 772, label: 'Tokelau' }, + { value: 776, label: 'Tonga' }, + { value: 780, label: 'Trinidad and Tobago' }, + { value: 788, label: 'Tunisia' }, + { value: 792, label: 'Turkey' }, + { value: 795, label: 'Turkmenistan' }, + { value: 796, label: 'Turks and Caicos Islands' }, + { value: 798, label: 'Tuvalu' }, + { value: 800, label: 'Uganda' }, + { value: 804, label: 'Ukraine' }, + { value: 784, label: 'United Arab Emirates' }, + { value: 826, label: 'United Kingdom' }, + { value: 840, label: 'United States' }, + { value: 581, label: 'United States Minor Outlying Islands' }, + { value: 858, label: 'Uruguay' }, + { value: 860, label: 'Uzbekistan' }, + { value: 548, label: 'Vanuatu' }, + { value: 862, label: 'Venezuela, Bolivarian Republic of' }, + { value: 704, label: 'Viet Nam' }, + { value: 92, label: 'Virgin Islands, British' }, + { value: 850, label: 'Virgin Islands, U.S.' }, + { value: 876, label: 'Wallis and Futuna' }, + { value: 732, label: 'Western Sahara' }, + { value: 887, label: 'Yemen' }, + { value: 894, label: 'Zambia' }, + { value: 716, label: 'Zimbabwe' }, + { value: 248, label: 'Γ…land Islands' }, + ], + }, + { + label: 'Shipping Street Address', + key: 'shippingStreetAddress', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Shipping Apt/Suite', + key: 'shippingAptSuite', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Shipping City', + key: 'shippingCity', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Shipping State/Province', + key: 'shippingStateProvince', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Shipping Postal Code', + key: 'shippingPostalCode', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Shipping Country Code', + key: 'shippingCountryCode', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Private Notes', + key: 'privateNotes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Public Notes', + key: 'publicNotes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Website', + key: 'website', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 1', + key: 'customValue1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 2', + key: 'customValue2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 3', + key: 'customValue3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 4', + key: 'customValue4', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-client/index.js b/packages/backend/src/apps/invoice-ninja/actions/create-client/index.js new file mode 100644 index 0000000000000000000000000000000000000000..47edb573eff8e395226fcddc95d08ae91645510a --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-client/index.js @@ -0,0 +1,84 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create client', + key: 'createClient', + description: 'Creates a new client.', + arguments: fields, + + async run($) { + const { + clientName, + contactFirstName, + contactLastName, + contactEmail, + contactPhone, + languageCode, + currencyCode, + idNumber, + vatNumber, + streetAddress, + aptSuite, + city, + stateProvince, + postalCode, + countryCode, + shippingStreetAddress, + shippingAptSuite, + shippingCity, + shippingStateProvince, + shippingPostalCode, + shippingCountryCode, + privateNotes, + publicNotes, + website, + customValue1, + customValue2, + customValue3, + customValue4, + } = $.step.parameters; + + const bodyFields = { + name: clientName, + contacts: { + first_name: contactFirstName, + last_name: contactLastName, + email: contactEmail, + phone: contactPhone, + }, + settings: { + language_id: languageCode, + currency_id: currencyCode, + }, + id_number: idNumber, + vat_number: vatNumber, + address1: streetAddress, + address2: aptSuite, + city: city, + state: stateProvince, + postal_code: postalCode, + country_id: countryCode, + shipping_address1: shippingStreetAddress, + shipping_address2: shippingAptSuite, + shipping_city: shippingCity, + shipping_state: shippingStateProvince, + shipping_postal_code: shippingPostalCode, + shipping_country_id: shippingCountryCode, + private_notes: privateNotes, + public_notes: publicNotes, + website: website, + custom_value1: customValue1, + custom_value2: customValue2, + custom_value3: customValue3, + custom_value4: customValue4, + }; + + const body = filterProvidedFields(bodyFields); + + const response = await $.http.post('/v1/clients', body); + + $.setActionItem({ raw: response.data.data }); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-invoice/fields.js b/packages/backend/src/apps/invoice-ninja/actions/create-invoice/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..f2556ea24c1e67e45201798fd58cc395d69d767a --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-invoice/fields.js @@ -0,0 +1,407 @@ +export const fields = [ + { + label: 'Client ID', + key: 'clientId', + type: 'dropdown', + required: true, + description: 'The ID of the client, not the name or email address.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listClients', + }, + ], + }, + }, + { + label: 'Send Email', + key: 'sendEmail', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'False', value: 'false' }, + { label: 'True', value: 'true' }, + ], + }, + { + label: 'Mark Sent', + key: 'markSent', + type: 'dropdown', + required: false, + description: 'Setting this to true creates the invoice as sent.', + variables: true, + options: [ + { label: 'False', value: 'false' }, + { label: 'True', value: 'true' }, + ], + }, + { + label: 'Paid', + key: 'paid', + type: 'dropdown', + required: false, + description: 'Setting this to true creates the invoice as paid.', + variables: true, + options: [ + { label: 'False', value: 'false' }, + { label: 'True', value: 'true' }, + ], + }, + { + label: 'Amount Paid', + key: 'amountPaid', + type: 'string', + required: false, + description: + 'If this value is greater than zero a payment will be created along with the invoice.', + variables: true, + }, + { + label: 'Number', + key: 'number', + type: 'string', + required: false, + description: + 'The invoice number - is a unique alpha numeric number per invoice per company', + variables: true, + }, + { + label: 'Discount', + key: 'discount', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'PO Number', + key: 'poNumber', + type: 'string', + required: false, + description: 'The purchase order associated with this invoice', + variables: true, + }, + { + label: 'Date', + key: 'date', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Due Date', + key: 'dueDate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 1', + key: 'taxRate1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 1', + key: 'taxName1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 2', + key: 'taxRate2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 2', + key: 'taxName2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 3', + key: 'taxRate3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 3', + key: 'taxName3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Field 1', + key: 'customField1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Field 2', + key: 'customField2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Field 3', + key: 'customField3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Field 4', + key: 'customField4', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Surcharge 1', + key: 'customSurcharge1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Surcharge 2', + key: 'customSurcharge2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Surcharge 3', + key: 'customSurcharge3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Surcharge 4', + key: 'customSurcharge4', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Is Amount Discount', + key: 'isAmountDiscount', + type: 'dropdown', + required: false, + description: + 'By default the discount is applied as a percentage, enabling this applies the discount as a fixed amount.', + variables: true, + options: [ + { label: 'False', value: 'false' }, + { label: 'True', value: 'true' }, + ], + }, + { + label: 'Partial/Deposit', + key: 'partialDeposit', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Partial Due Date', + key: 'partialDueDate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Cost', + key: 'lineItemCost', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Quatity', + key: 'lineItemQuantity', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Product', + key: 'lineItemProduct', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Discount', + key: 'lineItemDiscount', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Description', + key: 'lineItemDescription', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Rate 1', + key: 'lineItemTaxRate1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Name 1', + key: 'lineItemTaxName1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Rate 2', + key: 'lineItemTaxRate2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Name 2', + key: 'lineItemTaxName2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Rate 3', + key: 'lineItemTaxRate3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Tax Name 3', + key: 'lineItemTaxName3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Custom Field 1', + key: 'lineItemCustomField1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Custom Field 2', + key: 'lineItemCustomField2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Custom Field 3', + key: 'lineItemCustomField3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Custom Field 4', + key: 'lineItemCustomField4', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Line Item Product Cost', + key: 'lineItemProductCost', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Public Notes', + key: 'publicNotes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Private Notes', + key: 'privateNotes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Terms', + key: 'terms', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Footer', + key: 'footer', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-invoice/index.js b/packages/backend/src/apps/invoice-ninja/actions/create-invoice/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7915ebfcdebc822d5c9d73a28402c435eeaa72cd --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-invoice/index.js @@ -0,0 +1,127 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create invoice', + key: 'createInvoice', + description: 'Creates a new invoice.', + arguments: fields, + + async run($) { + const { + clientId, + sendEmail, + markSent, + paid, + amountPaid, + number, + discount, + poNumber, + date, + dueDate, + taxRate1, + taxName1, + taxRate2, + taxName2, + taxRate3, + taxName3, + customField1, + customField2, + customField3, + customField4, + customSurcharge1, + customSurcharge2, + customSurcharge3, + customSurcharge4, + isAmountDiscount, + partialDeposit, + partialDueDate, + lineItemCost, + lineItemQuantity, + lineItemProduct, + lineItemDiscount, + lineItemDescription, + lineItemTaxRate1, + lineItemTaxName1, + lineItemTaxRate2, + lineItemTaxName2, + lineItemTaxRate3, + lineItemTaxName3, + lineItemCustomField1, + lineItemCustomField2, + lineItemCustomField3, + lineItemCustomField4, + lineItemProductCost, + publicNotes, + privateNotes, + terms, + footer, + } = $.step.parameters; + + const paramFields = { + send_email: sendEmail, + mark_sent: markSent, + paid: paid, + amount_paid: amountPaid, + }; + + const params = filterProvidedFields(paramFields); + + const bodyFields = { + client_id: clientId, + number: number, + discount: discount, + po_number: poNumber, + date: date, + due_date: dueDate, + tax_rate1: taxRate1, + tax_name1: taxName1, + tax_rate2: taxRate2, + tax_name2: taxName2, + tax_rate3: taxRate3, + tax_name3: taxName3, + custom_value1: customField1, + custom_value2: customField2, + custom_value3: customField3, + custom_value4: customField4, + custom_surcharge1: customSurcharge1, + custom_surcharge2: customSurcharge2, + custom_surcharge3: customSurcharge3, + custom_surcharge4: customSurcharge4, + is_amount_discount: Boolean(isAmountDiscount), + partial: partialDeposit, + partial_due_date: partialDueDate, + line_items: [ + { + cost: lineItemCost, + quantity: lineItemQuantity, + product_key: lineItemProduct, + discount: lineItemDiscount, + notes: lineItemDescription, + tax_rate1: lineItemTaxRate1, + tax_name1: lineItemTaxName1, + tax_rate2: lineItemTaxRate2, + tax_name2: lineItemTaxName2, + tax_rate3: lineItemTaxRate3, + tax_name3: lineItemTaxName3, + custom_value1: lineItemCustomField1, + custom_value2: lineItemCustomField2, + custom_value3: lineItemCustomField3, + custom_value4: lineItemCustomField4, + product_cost: lineItemProductCost, + }, + ], + public_notes: publicNotes, + private_notes: privateNotes, + terms: terms, + footer: footer, + }; + + const body = filterProvidedFields(bodyFields); + + const response = await $.http.post('/v1/invoices', body, { params }); + + $.setActionItem({ raw: response.data.data }); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-payment/fields.js b/packages/backend/src/apps/invoice-ninja/actions/create-payment/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..608b89fd82daaed30518fb9ebdfbe70b54689762 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-payment/fields.js @@ -0,0 +1,111 @@ +export const fields = [ + { + label: 'Client ID', + key: 'clientId', + type: 'dropdown', + required: true, + description: 'The ID of the client, not the name or email address.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listClients', + }, + ], + }, + }, + { + label: 'Payment Date', + key: 'paymentDate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Invoice', + key: 'invoiceId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listInvoices', + }, + ], + }, + }, + { + label: 'Invoice Amount', + key: 'invoiceAmount', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Payment Type', + key: 'paymentType', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'Bank Transfer', value: '1' }, + { label: 'Cash', value: '2' }, + { label: 'Debit', value: '3' }, + { label: 'ACH', value: '4' }, + { label: 'Visa Card', value: '5' }, + { label: 'MasterCard', value: '6' }, + { label: 'American Express', value: '7' }, + { label: 'Discover Card', value: '8' }, + { label: 'Diners Card', value: '9' }, + { label: 'EuroCard', value: '10' }, + { label: 'Nova', value: '11' }, + { label: 'Credit Card Other', value: '12' }, + { label: 'PayPal', value: '13' }, + { label: 'Google Wallet', value: '14' }, + { label: 'Check', value: '15' }, + { label: 'Carte Blanche', value: '16' }, + { label: 'UnionPay', value: '17' }, + { label: 'JCB', value: '18' }, + { label: 'Laser', value: '19' }, + { label: 'Maestro', value: '20' }, + { label: 'Solo', value: '21' }, + { label: 'Switch', value: '22' }, + { label: 'iZettle', value: '23' }, + { label: 'Swish', value: '24' }, + { label: 'Venmo', value: '25' }, + { label: 'Money Order', value: '26' }, + { label: 'Alipay', value: '27' }, + { label: 'Sofort', value: '28' }, + { label: 'SEPA', value: '29' }, + { label: 'GoCardless', value: '30' }, + { label: 'Bitcoin', value: '31' }, + ], + }, + { + label: 'Transfer Reference', + key: 'transferReference', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Private Notes', + key: 'privateNotes', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-payment/index.js b/packages/backend/src/apps/invoice-ninja/actions/create-payment/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4d4ea080faa2289c80d1bd5acf5a4548dec08953 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-payment/index.js @@ -0,0 +1,42 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create payment', + key: 'createPayment', + description: 'Creates a new payment.', + arguments: fields, + + async run($) { + const { + clientId, + paymentDate, + invoiceId, + invoiceAmount, + paymentType, + transferReference, + privateNotes, + } = $.step.parameters; + + const bodyFields = { + client_id: clientId, + date: paymentDate, + invoices: [ + { + invoice_id: invoiceId, + amount: invoiceAmount, + }, + ], + type_id: paymentType, + transaction_reference: transferReference, + private_notes: privateNotes, + }; + + const body = filterProvidedFields(bodyFields); + + const response = await $.http.post('/v1/payments', body); + + $.setActionItem({ raw: response.data.data }); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-product/fields.js b/packages/backend/src/apps/invoice-ninja/actions/create-product/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..87b5fecc02c2e5b23fa18de4a38144479befd979 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-product/fields.js @@ -0,0 +1,114 @@ +export const fields = [ + { + label: 'Product Key', + key: 'productKey', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Notes', + key: 'notes', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Price', + key: 'price', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Quantity', + key: 'quantity', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 1', + key: 'taxRate1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 1', + key: 'taxName1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 2', + key: 'taxRate2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 2', + key: 'taxName2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Rate 3', + key: 'taxRate3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Tax Name 3', + key: 'taxName3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 1', + key: 'customValue1', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 2', + key: 'customValue2', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 3', + key: 'customValue3', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Custom Value 4', + key: 'customValue4', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/invoice-ninja/actions/create-product/index.js b/packages/backend/src/apps/invoice-ninja/actions/create-product/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5fa6dd6a9fb1058bdef20b82ca8432dfc5ab7da6 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/create-product/index.js @@ -0,0 +1,52 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create product', + key: 'createProduct', + description: 'Creates a new product.', + arguments: fields, + + async run($) { + const { + productKey, + notes, + price, + quantity, + taxRate1, + taxName1, + taxRate2, + taxName2, + taxRate3, + taxName3, + customValue1, + customValue2, + customValue3, + customValue4, + } = $.step.parameters; + + const bodyFields = { + product_key: productKey, + notes: notes, + price: price, + quantity: quantity, + tax_rate1: taxRate1, + tax_name1: taxName1, + tax_rate2: taxRate2, + tax_name2: taxName2, + tax_rate3: taxRate3, + tax_name3: taxName3, + custom_value1: customValue1, + custom_value2: customValue2, + custom_value3: customValue3, + custom_value4: customValue4, + }; + + const body = filterProvidedFields(bodyFields); + + const response = await $.http.post('/v1/products', body); + + $.setActionItem({ raw: response.data.data }); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/actions/index.js b/packages/backend/src/apps/invoice-ninja/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..36d38214aa98df2413ba69bd8a66fd222da8bcaf --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/actions/index.js @@ -0,0 +1,6 @@ +import createClient from './create-client/index.js'; +import createInvoice from './create-invoice/index.js'; +import createPayment from './create-payment/index.js'; +import createProduct from './create-product/index.js'; + +export default [createClient, createInvoice, createPayment, createProduct]; diff --git a/packages/backend/src/apps/invoice-ninja/assets/favicon.svg b/packages/backend/src/apps/invoice-ninja/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..59d3f11fbcfb9db3b64086f7927f4da569f52d78 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/assets/favicon.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/backend/src/apps/invoice-ninja/auth/index.js b/packages/backend/src/apps/invoice-ninja/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4e9a78caef3e29ddecc6727a4d593231f0656195 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/auth/index.js @@ -0,0 +1,33 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'apiToken', + label: 'API Token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Tokens can be created in the v5 app on Settings > Account Management', + clickToCopy: false, + }, + { + key: 'instanceUrl', + label: 'Invoice Ninja instance URL (optional)', + type: 'string', + required: false, + readOnly: false, + value: null, + placeholder: null, + description: "Leave this field blank if you're using hosted platform.", + clickToCopy: true, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/invoice-ninja/auth/is-still-verified.js b/packages/backend/src/apps/invoice-ninja/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..270d415ba799b443cb98782cb6af6ffd13b0f23c --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/invoice-ninja/auth/verify-credentials.js b/packages/backend/src/apps/invoice-ninja/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..371876cb9b69aa4fb3c5fef7e2ad01df94be2686 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/auth/verify-credentials.js @@ -0,0 +1,13 @@ +const verifyCredentials = async ($) => { + const { data } = await $.http.get('/v1/ping'); + + const screenName = [data.user_name, data.company_name] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/invoice-ninja/common/add-auth-header.js b/packages/backend/src/apps/invoice-ninja/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..a9bb7341add6bb02f1a10f692d8b2698630f4805 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/common/add-auth-header.js @@ -0,0 +1,18 @@ +const addAuthHeader = ($, requestConfig) => { + const { instanceUrl } = $.auth.data; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } + + requestConfig.headers['X-API-TOKEN'] = $.auth.data.apiToken; + + requestConfig.headers['X-Requested-With'] = 'XMLHttpRequest'; + + requestConfig.headers['Content-Type'] = + requestConfig.headers['Content-Type'] || 'application/json'; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/invoice-ninja/common/filter-provided-fields.js b/packages/backend/src/apps/invoice-ninja/common/filter-provided-fields.js new file mode 100644 index 0000000000000000000000000000000000000000..ceff6e22f575aeb1bd6642458b6d7ee85f39d4cb --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/common/filter-provided-fields.js @@ -0,0 +1,18 @@ +import isObject from 'lodash/isObject.js'; + +export function filterProvidedFields(body) { + return Object.keys(body).reduce((result, key) => { + const value = body[key]; + + if (isObject(value)) { + const filteredNestedObj = filterProvidedFields(value); + if (Object.keys(filteredNestedObj).length > 0) { + result[key] = filteredNestedObj; + } + } else if (body[key]) { + result[key] = value; + } + + return result; + }, {}); +} diff --git a/packages/backend/src/apps/invoice-ninja/common/set-base-url.js b/packages/backend/src/apps/invoice-ninja/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..fc3252afc154234c90c2f4df36c4f178be0aa599 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/common/set-base-url.js @@ -0,0 +1,11 @@ +const setBaseUrl = ($, requestConfig) => { + const instanceUrl = $.auth.data.instanceUrl; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/invoice-ninja/dynamic-data/index.js b/packages/backend/src/apps/invoice-ninja/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b3ef2269a4b120e456729f45d5a77760c18f590d --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listClients from './list-clients/index.js'; +import listInvoices from './list-invoices/index.js'; + +export default [listClients, listInvoices]; diff --git a/packages/backend/src/apps/invoice-ninja/dynamic-data/list-clients/index.js b/packages/backend/src/apps/invoice-ninja/dynamic-data/list-clients/index.js new file mode 100644 index 0000000000000000000000000000000000000000..512ebda196a7ac38603438797d710598cf5d96eb --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/dynamic-data/list-clients/index.js @@ -0,0 +1,31 @@ +export default { + name: 'List clients', + key: 'listClients', + + async run($) { + const clients = { + data: [], + }; + + const params = { + sort: 'created_at|desc', + }; + + const { + data: { data }, + } = await $.http.get('/v1/clients', { params }); + + if (!data?.length) { + return; + } + + for (const client of data) { + clients.data.push({ + value: client.id, + name: client.name, + }); + } + + return clients; + }, +}; diff --git a/packages/backend/src/apps/invoice-ninja/dynamic-data/list-invoices/index.js b/packages/backend/src/apps/invoice-ninja/dynamic-data/list-invoices/index.js new file mode 100644 index 0000000000000000000000000000000000000000..15b6d06271c63fc997396b41be3ca9bb891cf7e4 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/dynamic-data/list-invoices/index.js @@ -0,0 +1,31 @@ +export default { + name: 'List invoices', + key: 'listInvoices', + + async run($) { + const invoices = { + data: [], + }; + + const params = { + sort: 'created_at|desc', + }; + + const { + data: { data }, + } = await $.http.get('/v1/invoices', { params }); + + if (!data?.length) { + return; + } + + for (const invoice of data) { + invoices.data.push({ + value: invoice.id, + name: invoice.number, + }); + } + + return invoices; + }, +}; diff --git a/packages/backend/src/apps/invoice-ninja/index.js b/packages/backend/src/apps/invoice-ninja/index.js new file mode 100644 index 0000000000000000000000000000000000000000..688ffc018f1c964bdcc24843cca735faf0e4f0ad --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/index.js @@ -0,0 +1,23 @@ +import defineApp from '../../helpers/define-app.js'; +import setBaseUrl from './common/set-base-url.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Invoice Ninja', + key: 'invoice-ninja', + baseUrl: 'https://invoiceninja.com', + apiBaseUrl: 'https://invoicing.co/api', + iconUrl: '{BASE_URL}/apps/invoice-ninja/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/invoice-ninja/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/index.js b/packages/backend/src/apps/invoice-ninja/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8293af88f52b67b4233e9ca39bef656b01adf271 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/index.js @@ -0,0 +1,15 @@ +import newClients from './new-clients/index.js'; +import newCredits from './new-credits/index.js'; +import newInvoices from './new-invoices/index.js'; +import newPayments from './new-payments/index.js'; +import newProjects from './new-projects/index.js'; +import newQuotes from './new-quotes/index.js'; + +export default [ + newClients, + newCredits, + newInvoices, + newPayments, + newProjects, + newQuotes, +]; diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-clients/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-clients/index.js new file mode 100644 index 0000000000000000000000000000000000000000..608541d56e7422210054fa240cf98df5c77b0e2d --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-clients/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New clients', + key: 'newClients', + type: 'webhook', + description: 'Triggers when a new client is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_CLIENT_EVENT_ID = '1'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_CLIENT_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-credits/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-credits/index.js new file mode 100644 index 0000000000000000000000000000000000000000..13af3438ada4c683eb1de416b5a75069c47c2ade --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-credits/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New credits', + key: 'newCredits', + type: 'webhook', + description: 'Triggers when a new credit is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_CREDIT_EVENT_ID = '27'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_CREDIT_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-invoices/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-invoices/index.js new file mode 100644 index 0000000000000000000000000000000000000000..92e130f9c578ebf8d9c4bd450dbfe9d2a240ab73 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-invoices/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New invoices', + key: 'newInvoices', + type: 'webhook', + description: 'Triggers when a new invoice is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_INVOICE_EVENT_ID = '2'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_INVOICE_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-payments/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-payments/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c50586739b1b7a304e449022eee9f05397e07c1d --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-payments/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New payments', + key: 'newPayments', + type: 'webhook', + description: 'Triggers when a new payment is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_PAYMENT_EVENT_ID = '4'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_PAYMENT_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-projects/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-projects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5f6b64d8f47e6a62d2e52fe94dc0be0e3026cb45 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-projects/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New projects', + key: 'newProjects', + type: 'webhook', + description: 'Triggers when a new project is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_PROJECT_EVENT_ID = '25'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_PROJECT_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/invoice-ninja/triggers/new-quotes/index.js b/packages/backend/src/apps/invoice-ninja/triggers/new-quotes/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d2398d4509606de4c62fea1fa462d434842af732 --- /dev/null +++ b/packages/backend/src/apps/invoice-ninja/triggers/new-quotes/index.js @@ -0,0 +1,54 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New quotes', + key: 'newQuotes', + type: 'webhook', + description: 'Triggers when a new quote is added.', + arguments: [], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const CREATE_QUOTE_EVENT_ID = '3'; + + const payload = { + target_url: $.webhookUrl, + event_id: CREATE_QUOTE_EVENT_ID, + format: 'JSON', + rest_method: 'post', + }; + + const response = await $.http.post('/v1/webhooks', payload); + + await $.flow.setRemoteWebhookId(response.data.data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v1/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/mattermost/actions/index.js b/packages/backend/src/apps/mattermost/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..93ee03b1e50e5d1deb51941da92f11aa501f5667 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/index.js @@ -0,0 +1,3 @@ +import sendMessageToChannel from './send-a-message-to-channel/index.js'; + +export default [sendMessageToChannel]; diff --git a/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.js b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..30cdc4beb5b1fe8f3c6fc705a066be21af74e182 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/index.js @@ -0,0 +1,42 @@ +import defineAction from '../../../../helpers/define-action.js'; +import postMessage from './post-message.js'; + +export default defineAction({ + name: 'Send a message to channel', + key: 'sendMessageToChannel', + description: 'Sends a message to a channel you specify.', + arguments: [ + { + label: 'Channel', + key: 'channel', + type: 'dropdown', + required: true, + description: 'Pick a channel to send the message to.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listChannels', + }, + ], + }, + }, + { + label: 'Message text', + key: 'message', + type: 'string', + required: true, + description: 'The content of your new message.', + variables: true, + }, + ], + + async run($) { + const message = await postMessage($); + + return message; + }, +}); diff --git a/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.js b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.js new file mode 100644 index 0000000000000000000000000000000000000000..e4a915de9446632353ac498c107a977d681c5d22 --- /dev/null +++ b/packages/backend/src/apps/mattermost/actions/send-a-message-to-channel/post-message.js @@ -0,0 +1,20 @@ +const postMessage = async ($) => { + const { parameters } = $.step; + const channel_id = parameters.channel; + const message = parameters.message; + + const data = { + channel_id, + message, + }; + + const response = await $.http.post('/api/v4/posts', data); + + const actionData = { + raw: response?.data, + }; + + $.setActionItem(actionData); +}; + +export default postMessage; diff --git a/packages/backend/src/apps/mattermost/assets/favicon.svg b/packages/backend/src/apps/mattermost/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..1d5bf91f6abbc2c9b9f9ab56c7038570d0f538d2 --- /dev/null +++ b/packages/backend/src/apps/mattermost/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/mattermost/auth/generate-auth-url.js b/packages/backend/src/apps/mattermost/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..ba9b1ac608fb47fc0682a89e803ab831865cbacd --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/generate-auth-url.js @@ -0,0 +1,17 @@ +import { URL, URLSearchParams } from 'url'; +import getBaseUrl from '../common/get-base-url.js'; + +export default async function generateAuthUrl($) { + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: $.auth.data.oAuthRedirectUrl, + response_type: 'code', + }); + + const baseUrl = getBaseUrl($); + const path = `/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url: new URL(path, baseUrl).toString(), + }); +} diff --git a/packages/backend/src/apps/mattermost/auth/index.js b/packages/backend/src/apps/mattermost/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4dd50ae02bd9d77611288cdc2a4f7cdd3b157cd0 --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/index.js @@ -0,0 +1,57 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/mattermost/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Mattermost OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'instanceUrl', + label: 'Mattermost instance URL', + type: 'string', + required: false, + readOnly: false, + value: null, + placeholder: null, + description: 'Your Mattermost instance URL', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client id', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/mattermost/auth/is-still-verified.js b/packages/backend/src/apps/mattermost/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/mattermost/auth/verify-credentials.js b/packages/backend/src/apps/mattermost/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..e5c8396f66d7199d0dc4efd92c874f9239485367 --- /dev/null +++ b/packages/backend/src/apps/mattermost/auth/verify-credentials.js @@ -0,0 +1,43 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }; + const headers = { + 'Content-Type': 'application/x-www-form-urlencoded', // This is not documented yet required + }; + const response = await $.http.post('/oauth/access_token', null, { + params, + headers, + }); + + const { + data: { access_token, refresh_token, scope, token_type }, + } = response; + + $.auth.data.accessToken = response.data.access_token; + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: access_token, + refreshToken: refresh_token, + scope: scope, + tokenType: token_type, + userId: currentUser.id, + screenName: currentUser.username, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/mattermost/common/add-auth-header.js b/packages/backend/src/apps/mattermost/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..b44932c5a2a1ef78f0820eed63860e03bceeb084 --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers = requestConfig.headers || {}; + requestConfig.headers.Authorization = `Bearer ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.js b/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.js new file mode 100644 index 0000000000000000000000000000000000000000..2e232874696bb1e10ce19cad7dc744db61285e9f --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/add-x-requested-with-header.js @@ -0,0 +1,9 @@ +const addXRequestedWithHeader = ($, requestConfig) => { + // This is not documented yet required + // ref. https://forum.mattermost.com/t/solved-invalid-or-expired-session-please-login-again/6772 + requestConfig.headers = requestConfig.headers || {}; + requestConfig.headers['X-Requested-With'] = `XMLHttpRequest`; + return requestConfig; +}; + +export default addXRequestedWithHeader; diff --git a/packages/backend/src/apps/mattermost/common/get-base-url.js b/packages/backend/src/apps/mattermost/common/get-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..8ba13c4de5ff2771ebbc9c2a1808ac519c6de48f --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/get-base-url.js @@ -0,0 +1,5 @@ +const getBaseUrl = ($) => { + return $.auth.data.instanceUrl; +}; + +export default getBaseUrl; diff --git a/packages/backend/src/apps/mattermost/common/get-current-user.js b/packages/backend/src/apps/mattermost/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..84286c280cc2ca41f4c512a7ce09ec7373b3b17c --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/get-current-user.js @@ -0,0 +1,7 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/api/v4/users/me'); + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/mattermost/common/set-base-url.js b/packages/backend/src/apps/mattermost/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..af904f7d2c889e64c448fb52c7f28f9cc1f1bb8e --- /dev/null +++ b/packages/backend/src/apps/mattermost/common/set-base-url.js @@ -0,0 +1,7 @@ +const setBaseUrl = ($, requestConfig) => { + requestConfig.baseURL = $.auth.data.instanceUrl; + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/mattermost/dynamic-data/index.js b/packages/backend/src/apps/mattermost/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4305d61156bcf3fa1e459e770d4235bbed78c938 --- /dev/null +++ b/packages/backend/src/apps/mattermost/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listChannels from './list-channels/index.js'; + +export default [listChannels]; diff --git a/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.js b/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ab904c9e608102ae65aef791e7deaa94c272c6b6 --- /dev/null +++ b/packages/backend/src/apps/mattermost/dynamic-data/list-channels/index.js @@ -0,0 +1,22 @@ +export default { + name: 'List channels', + key: 'listChannels', + + async run($) { + const channels = { + data: [], + error: null, + }; + + const response = await $.http.get('/api/v4/users/me/channels'); // this endpoint will return only channels user joined, there is no endpoint to list all channels available for user + + for (const channel of response.data) { + channels.data.push({ + value: channel.id, + name: channel.display_name || channel.id, // it's possible for channel to not have any name thus falling back to using id + }); + } + + return channels; + }, +}; diff --git a/packages/backend/src/apps/mattermost/index.js b/packages/backend/src/apps/mattermost/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7719ca9ce6eaf129e863a81f10da740a858ea329 --- /dev/null +++ b/packages/backend/src/apps/mattermost/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import addXRequestedWithHeader from './common/add-x-requested-with-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Mattermost', + key: 'mattermost', + iconUrl: '{BASE_URL}/apps/mattermost/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/mattermost/connection', + baseUrl: 'https://mattermost.com', + apiBaseUrl: '', // there is no cloud version of this app, user always need to provide address of own instance when creating connection + primaryColor: '4a154b', + supportsConnections: true, + beforeRequest: [setBaseUrl, addXRequestedWithHeader, addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/miro/actions/copy-board/index.js b/packages/backend/src/apps/miro/actions/copy-board/index.js new file mode 100644 index 0000000000000000000000000000000000000000..201a4255a9526584e0008ddfec6abecd455feeea --- /dev/null +++ b/packages/backend/src/apps/miro/actions/copy-board/index.js @@ -0,0 +1,116 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Copy board', + key: 'copyBoard', + description: 'Creates a copy of an existing board.', + arguments: [ + { + label: 'Original board', + key: 'originalBoard', + type: 'dropdown', + required: true, + description: 'The board that you want to copy.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBoards', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: 'Title for the board.', + variables: true, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: 'Description of the board.', + variables: true, + }, + { + label: 'Team Access', + key: 'teamAccess', + type: 'dropdown', + required: false, + description: + 'Team access to the board. Can be private, view, comment or edit. Default: private.', + variables: true, + options: [ + { + label: 'Private - nobody in the team can find and access the board', + value: 'private', + }, + { + label: 'View - any team member can find and view the board', + value: 'view', + }, + { + label: 'Comment - any team member can find and comment the board', + value: 'comment', + }, + { + label: 'Edit - any team member can find and edit the board', + value: 'edit', + }, + ], + }, + { + label: 'Access Via Link', + key: 'accessViaLink', + type: 'dropdown', + required: false, + description: + 'Access to the board by link. Can be private, view, comment. Default: private.', + variables: true, + options: [ + { + label: 'Private - only you have access to the board', + value: 'private', + }, + { + label: 'View - can view, no sign-in required', + value: 'view', + }, + { + label: 'Comment - can comment, no sign-in required', + value: 'comment', + }, + ], + }, + ], + + async run($) { + const params = { + copy_from: $.step.parameters.originalBoard, + }; + + const body = { + name: $.step.parameters.title, + description: $.step.parameters.description, + policy: { + sharingPolicy: { + access: $.step.parameters.accessViaLink || 'private', + teamAccess: $.step.parameters.teamAccess || 'private', + }, + }, + }; + + const { data } = await $.http.put('/v2/boards', body, { params }); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/miro/actions/create-board/index.js b/packages/backend/src/apps/miro/actions/create-board/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6633c6d3fe7c441fed5817e2f985491e714dd99c --- /dev/null +++ b/packages/backend/src/apps/miro/actions/create-board/index.js @@ -0,0 +1,94 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create board', + key: 'createBoard', + description: 'Creates a new board.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: 'Title for the board.', + variables: true, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: 'Description of the board.', + variables: true, + }, + { + label: 'Team Access', + key: 'teamAccess', + type: 'dropdown', + required: false, + description: + 'Team access to the board. Can be private, view, comment or edit. Default: private.', + variables: true, + options: [ + { + label: 'Private - nobody in the team can find and access the board', + value: 'private', + }, + { + label: 'View - any team member can find and view the board', + value: 'view', + }, + { + label: 'Comment - any team member can find and comment the board', + value: 'comment', + }, + { + label: 'Edit - any team member can find and edit the board', + value: 'edit', + }, + ], + }, + { + label: 'Access Via Link', + key: 'accessViaLink', + type: 'dropdown', + required: false, + description: + 'Access to the board by link. Can be private, view, comment. Default: private.', + variables: true, + options: [ + { + label: 'Private - only you have access to the board', + value: 'private', + }, + { + label: 'View - can view, no sign-in required', + value: 'view', + }, + { + label: 'Comment - can comment, no sign-in required', + value: 'comment', + }, + ], + }, + ], + + async run($) { + const body = { + name: $.step.parameters.title, + description: $.step.parameters.description, + policy: { + sharingPolicy: { + access: $.step.parameters.accessViaLink || 'private', + teamAccess: $.step.parameters.teamAccess || 'private', + }, + }, + }; + + const { data } = await $.http.post('/v2/boards', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/miro/actions/create-card-widget/index.js b/packages/backend/src/apps/miro/actions/create-card-widget/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b319694cbbc7a267c7b3237c5aa4d58ed3fa8a6f --- /dev/null +++ b/packages/backend/src/apps/miro/actions/create-card-widget/index.js @@ -0,0 +1,154 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create card widget', + key: 'createCardWidget', + description: 'Creates a new card widget on an existing board.', + arguments: [ + { + label: 'Board', + key: 'boardId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBoards', + }, + ], + }, + }, + { + label: 'Frame', + key: 'frameId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.boardId'], + description: + 'You need to create a frame prior to this step. Switch frame to grid mode to avoid cards overlap.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFrames', + }, + { + name: 'parameters.boardId', + value: '{parameters.boardId}', + }, + ], + }, + }, + { + label: 'Card Title', + key: 'cardTitle', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Card Title Link', + key: 'cardTitleLink', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Card Description', + key: 'cardDescription', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Card Due Date', + key: 'cardDueDate', + type: 'string', + required: false, + description: + 'format: date-time. Example value: 2023-10-12 22:00:55+00:00', + variables: true, + }, + { + label: 'Card Border Color', + key: 'cardBorderColor', + type: 'dropdown', + required: false, + description: 'In hex format. Default is blue (#2399F3).', + variables: true, + options: [ + { label: 'white', value: '#FFFFFF' }, + { label: 'yellow', value: '#FEF445' }, + { label: 'orange', value: '#FAC710' }, + { label: 'red', value: '#F24726' }, + { label: 'bright red', value: '#DA0063' }, + { label: 'light gray', value: '#E6E6E6' }, + { label: 'gray', value: '#808080' }, + { label: 'black', value: '#1A1A1A' }, + { label: 'light green', value: '#CEE741' }, + { label: 'green', value: '#8FD14F' }, + { label: 'dark green', value: '#0CA789' }, + { label: 'light blue', value: '#12CDD4' }, + { label: 'blue', value: '#2D9BF0' }, + { label: 'dark blue', value: '#414BB2' }, + { label: 'purple', value: '#9510AC' }, + { label: 'dark purple', value: '#652CB3' }, + ], + }, + ], + + async run($) { + const { + boardId, + frameId, + cardTitle, + cardTitleLink, + cardDescription, + cardDueDate, + cardBorderColor, + } = $.step.parameters; + + let title; + if (cardTitleLink) { + title = `${cardTitle}`; + } else { + title = cardTitle; + } + + const body = { + data: { + title: title, + description: cardDescription, + }, + style: {}, + parent: { + id: frameId, + }, + }; + + if (cardBorderColor) { + body.style.cardTheme = cardBorderColor; + } + + if (cardDueDate) { + body.data.dueDate = cardDueDate; + } + + const response = await $.http.post(`/v2/boards/${boardId}/cards`, body); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/miro/actions/index.js b/packages/backend/src/apps/miro/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c200553ead373e84478204c371d747edf25e5d47 --- /dev/null +++ b/packages/backend/src/apps/miro/actions/index.js @@ -0,0 +1,5 @@ +import copyBoard from './copy-board/index.js'; +import createBoard from './create-board/index.js'; +import createCardWidget from './create-card-widget/index.js'; + +export default [copyBoard, createBoard, createCardWidget]; diff --git a/packages/backend/src/apps/miro/assets/favicon.svg b/packages/backend/src/apps/miro/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..b87ea9a1c8d7accee9bb6163db6af662c4f07a06 --- /dev/null +++ b/packages/backend/src/apps/miro/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/miro/auth/generate-auth-url.js b/packages/backend/src/apps/miro/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..cb70ad36cc2f8bf13de2cd6ad24b254b6eda02a7 --- /dev/null +++ b/packages/backend/src/apps/miro/auth/generate-auth-url.js @@ -0,0 +1,19 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + response_type: 'code', + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + }); + + const url = `https://miro.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/miro/auth/index.js b/packages/backend/src/apps/miro/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e971a702f6596374fa49e98497ecfc103e91cacc --- /dev/null +++ b/packages/backend/src/apps/miro/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/miro/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Miro, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/miro/auth/is-still-verified.js b/packages/backend/src/apps/miro/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6d792b125dc06c5812c8375c0c31e511dc71619a --- /dev/null +++ b/packages/backend/src/apps/miro/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/miro/auth/refresh-token.js b/packages/backend/src/apps/miro/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..62be5d2ac0df757fc73b599ed910b9a461518e2d --- /dev/null +++ b/packages/backend/src/apps/miro/auth/refresh-token.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'node:url'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post('/v1/oauth/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + scope: data.scope, + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/miro/auth/verify-credentials.js b/packages/backend/src/apps/miro/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..d6d862c224dc1b6317edb9f7cc1010f1bc33597c --- /dev/null +++ b/packages/backend/src/apps/miro/auth/verify-credentials.js @@ -0,0 +1,39 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = { + grant_type: 'authorization_code', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + redirect_uri: redirectUri, + }; + + const { data } = await $.http.post(`/v1/oauth/token`, null, { + params, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + userId: data.user_id, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + teamId: data.team_id, + scope: data.scope, + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + screenName: currentUser.name, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/miro/common/add-auth-header.js b/packages/backend/src/apps/miro/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/miro/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/miro/common/get-current-user.js b/packages/backend/src/apps/miro/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2c09552fec0d470728b7937e18984006d9c5371e --- /dev/null +++ b/packages/backend/src/apps/miro/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data } = await $.http.get( + `https://api.miro.com/v1/oauth-token?access_token=${$.auth.data.accessToken}` + ); + return data.user; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/miro/dynamic-data/index.js b/packages/backend/src/apps/miro/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fd8026f8d16fbafa511ef89105d5a7695fed1d76 --- /dev/null +++ b/packages/backend/src/apps/miro/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listBoards from './list-boards/index.js'; +import listFrames from './list-frames/index.js'; + +export default [listBoards, listFrames]; diff --git a/packages/backend/src/apps/miro/dynamic-data/list-boards/index.js b/packages/backend/src/apps/miro/dynamic-data/list-boards/index.js new file mode 100644 index 0000000000000000000000000000000000000000..849ebc5d280f3aa04c8db7edfb359b6d8ab220d3 --- /dev/null +++ b/packages/backend/src/apps/miro/dynamic-data/list-boards/index.js @@ -0,0 +1,30 @@ +export default { + name: 'List boards', + key: 'listBoards', + + async run($) { + const boards = { + data: [], + }; + + let next; + do { + const { + data: { data, links }, + } = await $.http.get('/v2/boards'); + + next = links?.next; + + if (data.length) { + for (const board of data) { + boards.data.push({ + value: board.id, + name: board.name, + }); + } + } + } while (next); + + return boards; + }, +}; diff --git a/packages/backend/src/apps/miro/dynamic-data/list-frames/index.js b/packages/backend/src/apps/miro/dynamic-data/list-frames/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cbafd3af4d9dacfa9637e69e5e383f557b2387c --- /dev/null +++ b/packages/backend/src/apps/miro/dynamic-data/list-frames/index.js @@ -0,0 +1,38 @@ +export default { + name: 'List frames', + key: 'listFrames', + + async run($) { + const frames = { + data: [], + }; + + const boardId = $.step.parameters.boardId; + + if (!boardId) { + return { data: [] }; + } + + let next; + do { + const { + data: { data, links }, + } = await $.http.get(`/v2/boards/${boardId}/items`); + + next = links?.next; + + const allFrames = data.filter((item) => item.type === 'frame'); + + if (allFrames.length) { + for (const frame of allFrames) { + frames.data.push({ + value: frame.id, + name: frame.data.title, + }); + } + } + } while (next); + + return frames; + }, +}; diff --git a/packages/backend/src/apps/miro/index.js b/packages/backend/src/apps/miro/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be6a0210f8f36932ee114fb6da6cf9ac44bba99e --- /dev/null +++ b/packages/backend/src/apps/miro/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Miro', + key: 'miro', + baseUrl: 'https://miro.com', + apiBaseUrl: 'https://api.miro.com', + iconUrl: '{BASE_URL}/apps/miro/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/miro/connection', + primaryColor: 'F2CA02', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/notion/actions/create-database-item/index.js b/packages/backend/src/apps/notion/actions/create-database-item/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c9c4a4e7ca229fb96e4a36c7f23f7eac3df853e4 --- /dev/null +++ b/packages/backend/src/apps/notion/actions/create-database-item/index.js @@ -0,0 +1,93 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create database item', + key: 'createDatabaseItem', + description: 'Creates an item in a database.', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + { + label: 'Name', + key: 'name', + type: 'string', + required: false, + description: + 'This field has a 2000 character limit. Any characters beyond 2000 will not be included.', + variables: true, + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: false, + description: + 'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.', + variables: true, + }, + ], + + async run($) { + const name = $.step.parameters.name; + const truncatedName = name.slice(0, 2000); + const content = $.step.parameters.content; + const truncatedContent = content.slice(0, 2000); + + const body = { + parent: { + database_id: $.step.parameters.databaseId, + }, + properties: {}, + children: [], + }; + + if (name) { + body.properties.Name = { + title: [ + { + text: { + content: truncatedName, + }, + }, + ], + }; + } + + if (content) { + body.children = [ + { + object: 'block', + paragraph: { + rich_text: [ + { + text: { + content: truncatedContent, + }, + }, + ], + }, + }, + ]; + } + + const { data } = await $.http.post('/v1/pages', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/notion/actions/create-page/index.js b/packages/backend/src/apps/notion/actions/create-page/index.js new file mode 100644 index 0000000000000000000000000000000000000000..375463021c262bc36e17917ad311de017d0491fd --- /dev/null +++ b/packages/backend/src/apps/notion/actions/create-page/index.js @@ -0,0 +1,98 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create page', + key: 'createPage', + description: 'Creates a page inside a parent page', + arguments: [ + { + label: 'Parent page', + key: 'parentPageId', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listParentPages', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: false, + description: + 'This field has a 2000 character limit. Any characters beyond 2000 will not be included.', + variables: true, + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: false, + description: + 'The text to add to the page body. The max length for this field is 2000 characters. Any characters beyond 2000 will not be included.', + variables: true, + }, + ], + + async run($) { + const parentPageId = $.step.parameters.parentPageId; + const title = $.step.parameters.title; + const truncatedTitle = title.slice(0, 2000); + const content = $.step.parameters.content; + const truncatedContent = content.slice(0, 2000); + + const body = { + parent: { + page_id: parentPageId, + }, + properties: {}, + children: [], + }; + + if (title) { + body.properties.title = { + type: 'title', + title: [ + { + text: { + content: truncatedTitle, + }, + }, + ], + }; + } + + if (content) { + body.children = [ + { + object: 'block', + type: 'paragraph', + paragraph: { + rich_text: [ + { + type: 'text', + text: { + content: truncatedContent, + }, + }, + ], + }, + }, + ]; + } + + const { data } = await $.http.post('/v1/pages', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/notion/actions/find-database-item/index.js b/packages/backend/src/apps/notion/actions/find-database-item/index.js new file mode 100644 index 0000000000000000000000000000000000000000..44f9fea1a756b491a99343e8ed1ee18ffedc5558 --- /dev/null +++ b/packages/backend/src/apps/notion/actions/find-database-item/index.js @@ -0,0 +1,64 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Find database item', + key: 'findDatabaseItem', + description: 'Searches for an item in a database by property.', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + { + label: 'Name', + key: 'name', + type: 'string', + required: false, + description: + 'This field has a 2000 character limit. Any characters beyond 2000 will not be included.', + variables: true, + }, + ], + + async run($) { + const databaseId = $.step.parameters.databaseId; + const name = $.step.parameters.name; + const truncatedName = name.slice(0, 2000); + + const body = { + filter: { + property: 'Name', + rich_text: { + equals: truncatedName, + }, + }, + sorts: [ + { + timestamp: 'last_edited_time', + direction: 'descending', + }, + ], + }; + + const { data } = await $.http.post( + `/v1/databases/${databaseId}/query`, + body + ); + + $.setActionItem({ + raw: data.results[0], + }); + }, +}); diff --git a/packages/backend/src/apps/notion/actions/index.js b/packages/backend/src/apps/notion/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0b1629e104bb2e870de98452b4eccb976b9a825e --- /dev/null +++ b/packages/backend/src/apps/notion/actions/index.js @@ -0,0 +1,5 @@ +import createDatabaseItem from './create-database-item/index.js'; +import createPage from './create-page/index.js'; +import findDatabaseItem from './find-database-item/index.js'; + +export default [createDatabaseItem, createPage, findDatabaseItem]; diff --git a/packages/backend/src/apps/notion/assets/favicon.svg b/packages/backend/src/apps/notion/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..ebcbe81614fd5b66206f43c798c84ceb71f3d4e0 --- /dev/null +++ b/packages/backend/src/apps/notion/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/backend/src/apps/notion/auth/generate-auth-url.js b/packages/backend/src/apps/notion/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..5547ec12aa9b8dea1bd7bebc79b2b615e24777d2 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URL, URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + response_type: 'code', + owner: 'user', + }); + + const url = new URL( + `/v1/oauth/authorize?${searchParams}`, + $.app.apiBaseUrl + ).toString(); + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/notion/auth/index.js b/packages/backend/src/apps/notion/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5dba7507b3f57f08e33b55b5fa46e5fe82f16e69 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/index.js @@ -0,0 +1,49 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/notion/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Notion OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/notion#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/notion#client-id', + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/notion#client-secret', + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/notion/auth/is-still-verified.js b/packages/backend/src/apps/notion/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/notion/auth/verify-credentials.js b/packages/backend/src/apps/notion/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..5bed76f4d9d3fd2ec4fb04eb25021b2931c418f7 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/verify-credentials.js @@ -0,0 +1,52 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const response = await $.http.post( + `${$.app.apiBaseUrl}/v1/oauth/token`, + { + redirect_uri: redirectUri, + code: $.auth.data.code, + grant_type: 'authorization_code', + }, + { + headers: { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + }, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + const data = response.data; + + $.auth.data.accessToken = data.access_token; + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: data.access_token, + botId: data.bot_id, + duplicatedTemplateId: data.duplicated_template_id, + owner: data.owner, + tokenType: data.token_type, + workspaceIcon: data.workspace_icon, + workspaceId: data.workspace_id, + workspaceName: data.workspace_name, + screenName: data.workspace_name, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.name} @ ${data.workspace_name}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/notion/common/add-auth-header.js b/packages/backend/src/apps/notion/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..38e690943f2fcca9530b7af9cf532b637162ccb6 --- /dev/null +++ b/packages/backend/src/apps/notion/common/add-auth-header.js @@ -0,0 +1,13 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; + + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/notion/common/add-notion-version-header.js b/packages/backend/src/apps/notion/common/add-notion-version-header.js new file mode 100644 index 0000000000000000000000000000000000000000..08b2f9031c581eced9e05de0ec69851eb0bf9510 --- /dev/null +++ b/packages/backend/src/apps/notion/common/add-notion-version-header.js @@ -0,0 +1,7 @@ +const addNotionVersionHeader = ($, requestConfig) => { + requestConfig.headers['Notion-Version'] = '2022-06-28'; + + return requestConfig; +}; + +export default addNotionVersionHeader; diff --git a/packages/backend/src/apps/notion/common/get-current-user.js b/packages/backend/src/apps/notion/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..8147d1ffaaae04072873909688314059e615650d --- /dev/null +++ b/packages/backend/src/apps/notion/common/get-current-user.js @@ -0,0 +1,9 @@ +const getCurrentUser = async ($) => { + const userId = $.auth.data.owner.user.id; + const response = await $.http.get(`/v1/users/${userId}`); + + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/notion/dynamic-data/index.js b/packages/backend/src/apps/notion/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3811a7c3c4f7d3cd1ef385fe2e634b58d58e7a36 --- /dev/null +++ b/packages/backend/src/apps/notion/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listDatabases from './list-databases/index.js'; +import listParentPages from './list-parent-pages/index.js'; + +export default [listDatabases, listParentPages]; diff --git a/packages/backend/src/apps/notion/dynamic-data/list-databases/index.js b/packages/backend/src/apps/notion/dynamic-data/list-databases/index.js new file mode 100644 index 0000000000000000000000000000000000000000..da8f27d8cd4e57e37f756d8e5e8637d897491d54 --- /dev/null +++ b/packages/backend/src/apps/notion/dynamic-data/list-databases/index.js @@ -0,0 +1,32 @@ +export default { + name: 'List databases', + key: 'listDatabases', + + async run($) { + const databases = { + data: [], + error: null, + }; + const payload = { + filter: { + value: 'database', + property: 'object', + }, + }; + + do { + const response = await $.http.post('/v1/search', payload); + + payload.start_cursor = response.data.next_cursor; + + for (const database of response.data.results) { + databases.data.push({ + value: database.id, + name: database.title[0].plain_text, + }); + } + } while (payload.start_cursor); + + return databases; + }, +}; diff --git a/packages/backend/src/apps/notion/dynamic-data/list-parent-pages/index.js b/packages/backend/src/apps/notion/dynamic-data/list-parent-pages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9212e9b4d8b0daa04f7a814058a16c7d0f1e3282 --- /dev/null +++ b/packages/backend/src/apps/notion/dynamic-data/list-parent-pages/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List parent pages', + key: 'listParentPages', + + async run($) { + const parentPages = { + data: [], + error: null, + }; + const payload = { + filter: { + value: 'page', + property: 'object', + }, + }; + + do { + const response = await $.http.post('/v1/search', payload); + + payload.start_cursor = response.data.next_cursor; + + const topLevelPages = response.data.results.filter( + (page) => page.parent.workspace + ); + + for (const pages of topLevelPages) { + parentPages.data.push({ + value: pages.id, + name: pages.properties.title.title[0].plain_text, + }); + } + } while (payload.start_cursor); + + return parentPages; + }, +}; diff --git a/packages/backend/src/apps/notion/index.js b/packages/backend/src/apps/notion/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ea69d373750118e13837eb3fee7763f0d6dd9c02 --- /dev/null +++ b/packages/backend/src/apps/notion/index.js @@ -0,0 +1,23 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import addNotionVersionHeader from './common/add-notion-version-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Notion', + key: 'notion', + baseUrl: 'https://notion.com', + apiBaseUrl: 'https://api.notion.com', + iconUrl: '{BASE_URL}/apps/notion/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/notion/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [addAuthHeader, addNotionVersionHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/notion/triggers/index.js b/packages/backend/src/apps/notion/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..85d3380909826efae01163c94f498f894eb905c4 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/index.js @@ -0,0 +1,4 @@ +import newDatabaseItems from './new-database-items/index.js'; +import updatedDatabaseItems from './updated-database-items/index.js'; + +export default [newDatabaseItems, updatedDatabaseItems]; diff --git a/packages/backend/src/apps/notion/triggers/new-database-items/index.js b/packages/backend/src/apps/notion/triggers/new-database-items/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8ed8ccd5a9c8e89855b16211312f2a19c90a4183 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/new-database-items/index.js @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newDatabaseItems from './new-database-items.js'; + +export default defineTrigger({ + name: 'New database items', + key: 'newDatabaseItems', + pollInterval: 15, + description: 'Triggers when a new database item is created', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + ], + + async run($) { + await newDatabaseItems($); + }, +}); diff --git a/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.js b/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.js new file mode 100644 index 0000000000000000000000000000000000000000..1d134f58da0e1520450a0e88b7c9eea3274dbe09 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.js @@ -0,0 +1,29 @@ +const newDatabaseItems = async ($) => { + const payload = { + sorts: [ + { + timestamp: 'created_time', + direction: 'descending', + }, + ], + }; + + const databaseId = $.step.parameters.databaseId; + const path = `/v1/databases/${databaseId}/query`; + do { + const response = await $.http.post(path, payload); + + payload.start_cursor = response.data.next_cursor; + + for (const databaseItem of response.data.results) { + $.pushTriggerItem({ + raw: databaseItem, + meta: { + internalId: databaseItem.id, + }, + }); + } + } while (payload.start_cursor); +}; + +export default newDatabaseItems; diff --git a/packages/backend/src/apps/notion/triggers/updated-database-items/index.js b/packages/backend/src/apps/notion/triggers/updated-database-items/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a08ced6f023f7d3e0cb9f149d3822cb42d8577cc --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/updated-database-items/index.js @@ -0,0 +1,33 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import updatedDatabaseItems from './updated-database-items.js'; + +export default defineTrigger({ + name: 'Updated database items', + key: 'updatedDatabaseItems', + pollInterval: 15, + description: + 'Triggers when there is an update to an item in a chosen database', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + ], + + async run($) { + await updatedDatabaseItems($); + }, +}); diff --git a/packages/backend/src/apps/notion/triggers/updated-database-items/updated-database-items.js b/packages/backend/src/apps/notion/triggers/updated-database-items/updated-database-items.js new file mode 100644 index 0000000000000000000000000000000000000000..282aaf9f96e0b8eca787494be153c39b6d9481f6 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/updated-database-items/updated-database-items.js @@ -0,0 +1,29 @@ +const updatedDatabaseItems = async ($) => { + const payload = { + sorts: [ + { + timestamp: 'last_edited_time', + direction: 'descending', + }, + ], + }; + + const databaseId = $.step.parameters.databaseId; + const path = `/v1/databases/${databaseId}/query`; + do { + const response = await $.http.post(path, payload); + + payload.start_cursor = response.data.next_cursor; + + for (const databaseItem of response.data.results) { + $.pushTriggerItem({ + raw: databaseItem, + meta: { + internalId: `${databaseItem.id}-${databaseItem.last_edited_time}`, + }, + }); + } + } while (payload.start_cursor); +}; + +export default updatedDatabaseItems; diff --git a/packages/backend/src/apps/ntfy/actions/index.js b/packages/backend/src/apps/ntfy/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..92d67c2c5c3cb44f1613841137575e141ce4f79a --- /dev/null +++ b/packages/backend/src/apps/ntfy/actions/index.js @@ -0,0 +1,3 @@ +import sendMessage from './send-message/index.js'; + +export default [sendMessage]; diff --git a/packages/backend/src/apps/ntfy/actions/send-message/index.js b/packages/backend/src/apps/ntfy/actions/send-message/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a25db4acbdda02b18e9b9ca2e15689d4ef8eb2aa --- /dev/null +++ b/packages/backend/src/apps/ntfy/actions/send-message/index.js @@ -0,0 +1,96 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send message', + key: 'sendMessage', + description: 'Sends a message to a topic you specify.', + arguments: [ + { + label: 'Topic', + key: 'topic', + type: 'string', + required: true, + description: 'Target topic name.', + variables: true, + }, + { + label: 'Message body', + key: 'message', + type: 'string', + required: true, + description: + 'Message body to be sent, set to triggered if empty or not passed.', + variables: true, + }, + { + label: 'Title', + key: 'title', + type: 'string', + required: false, + description: 'Message title.', + variables: true, + }, + { + label: 'Email', + key: 'email', + type: 'string', + required: false, + description: 'E-mail address for e-mail notifications.', + variables: true, + }, + { + label: 'Click URL', + key: 'click', + type: 'string', + required: false, + description: 'Website opened when notification is clicked.', + variables: true, + }, + { + label: 'Attach file by URL', + key: 'attach', + type: 'string', + required: false, + description: 'URL of an attachment.', + variables: true, + }, + { + label: 'Filename', + key: 'filename', + type: 'string', + required: false, + description: 'File name of the attachment.', + variables: true, + }, + { + label: 'Delay', + key: 'delay', + type: 'string', + required: false, + description: + 'Timestamp or duration for delayed delivery. For example, 30min or 9am.', + variables: true, + }, + ], + + async run($) { + const { topic, message, title, email, click, attach, filename, delay } = + $.step.parameters; + const payload = { + topic, + message, + title, + email, + click, + attach, + filename, + delay, + }; + + const response = await $.http.post('/', payload); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/ntfy/assets/favicon.svg b/packages/backend/src/apps/ntfy/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..9e5b5136fd9740a5ed0c6e40b7c305d6d62c3183 --- /dev/null +++ b/packages/backend/src/apps/ntfy/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/ntfy/auth/index.js b/packages/backend/src/apps/ntfy/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f8eaed35170aca24f15f1646d679e3e0bfdce777 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/index.js @@ -0,0 +1,43 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'serverUrl', + label: 'Server URL', + type: 'string', + required: true, + readOnly: false, + value: 'https://ntfy.sh', + placeholder: null, + description: 'ntfy server to use.', + clickToCopy: false, + }, + { + key: 'username', + label: 'Username', + type: 'string', + required: false, + readOnly: false, + placeholder: null, + clickToCopy: false, + description: + 'You may need to provide your username if your installation requires authentication.', + }, + { + key: 'password', + label: 'Password', + type: 'string', + required: false, + readOnly: false, + placeholder: null, + clickToCopy: false, + description: + 'You may need to provide your password if your installation requires authentication.', + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/ntfy/auth/is-still-verified.js b/packages/backend/src/apps/ntfy/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/ntfy/auth/verify-credentials.js b/packages/backend/src/apps/ntfy/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..4a1aed229314ebf952054d6e054068b7116c3623 --- /dev/null +++ b/packages/backend/src/apps/ntfy/auth/verify-credentials.js @@ -0,0 +1,14 @@ +const verifyCredentials = async ($) => { + await $.http.post('/', { topic: 'automatisch' }); + let screenName = $.auth.data.serverUrl; + + if ($.auth.data.username) { + screenName = `${$.auth.data.username} @ ${screenName}`; + } + + await $.auth.set({ + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/ntfy/common/add-auth-header.js b/packages/backend/src/apps/ntfy/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..0c533682c8759c27f6ac2e3203b10ef1ea05087c --- /dev/null +++ b/packages/backend/src/apps/ntfy/common/add-auth-header.js @@ -0,0 +1,16 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data.serverUrl) { + requestConfig.baseURL = $.auth.data.serverUrl; + } + + if ($.auth.data?.username && $.auth.data?.password) { + requestConfig.auth = { + username: $.auth.data.username, + password: $.auth.data.password, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/ntfy/index.js b/packages/backend/src/apps/ntfy/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9ad1642b9f3c0044050b8cb0d086c51934a9210a --- /dev/null +++ b/packages/backend/src/apps/ntfy/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Ntfy', + key: 'ntfy', + iconUrl: '{BASE_URL}/apps/ntfy/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/ntfy/connection', + supportsConnections: true, + baseUrl: 'https://ntfy.sh', + apiBaseUrl: 'https://ntfy.sh', + primaryColor: '56bda8', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/odoo/actions/create-lead/index.js b/packages/backend/src/apps/odoo/actions/create-lead/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1e0861d3419428a57125402d446dc090b8795a08 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/create-lead/index.js @@ -0,0 +1,98 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { authenticate, asyncMethodCall } from '../../common/xmlrpc-client.js'; + +export default defineAction({ + name: 'Create Lead', + key: 'createLead', + description: '', + arguments: [ + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + description: 'Lead name', + variables: true, + }, + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: true, + variables: true, + options: [ + { + label: 'Lead', + value: 'lead', + }, + { + label: 'Opportunity', + value: 'opportunity', + }, + ], + }, + { + label: 'Email', + key: 'email', + type: 'string', + required: false, + description: 'Email of lead contact', + variables: true, + }, + { + label: 'Contact Name', + key: 'contactName', + type: 'string', + required: false, + description: 'Name of lead contact', + variables: true, + }, + { + label: 'Phone Number', + key: 'phoneNumber', + type: 'string', + required: false, + description: 'Phone number of lead contact', + variables: true, + }, + { + label: 'Mobile Number', + key: 'mobileNumber', + type: 'string', + required: false, + description: 'Mobile number of lead contact', + variables: true, + }, + ], + + async run($) { + const uid = await authenticate($); + const id = await asyncMethodCall($, { + method: 'execute_kw', + params: [ + $.auth.data.databaseName, + uid, + $.auth.data.apiKey, + 'crm.lead', + 'create', + [ + { + name: $.step.parameters.name, + type: $.step.parameters.type, + email_from: $.step.parameters.email, + contact_name: $.step.parameters.contactName, + phone: $.step.parameters.phoneNumber, + mobile: $.step.parameters.mobileNumber, + }, + ], + ], + path: 'object', + }); + + $.setActionItem({ + raw: { + id: id, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/odoo/actions/index.js b/packages/backend/src/apps/odoo/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5fb0747fad8b7e092c3c83c04cddba5a5b080f40 --- /dev/null +++ b/packages/backend/src/apps/odoo/actions/index.js @@ -0,0 +1,3 @@ +import createLead from './create-lead/index.js'; + +export default [createLead]; diff --git a/packages/backend/src/apps/odoo/assets/favicon.svg b/packages/backend/src/apps/odoo/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..aeb5dd77231be423d39beec6e5f30c60800a553a --- /dev/null +++ b/packages/backend/src/apps/odoo/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/odoo/auth/index.js b/packages/backend/src/apps/odoo/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..553666382444c5c01cc3089e7adbf9479d03e8f7 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/index.js @@ -0,0 +1,88 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'host', + label: 'Host Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Host name of your Odoo Server (e.g. sub.domain.com without the protocol)', + clickToCopy: false, + }, + { + key: 'port', + label: 'Port', + type: 'string', + required: true, + readOnly: false, + value: '443', + placeholder: null, + description: 'Port that the host is running on, defaults to 443 (HTTPS)', + clickToCopy: false, + }, + { + key: 'secure', + label: 'Secure', + type: 'dropdown', + required: true, + readOnly: false, + value: 'true', + description: 'True if the host communicates via secure protocol.', + variables: false, + clickToCopy: false, + options: [ + { + label: 'True', + value: 'true', + }, + { + label: 'False', + value: 'false', + }, + ], + }, + { + key: 'databaseName', + label: 'Database Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Name of your Odoo database', + clickToCopy: false, + }, + { + key: 'email', + label: 'Email Address', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Email Address of the account that will be interacting with the database', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Key for your Odoo account', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/odoo/auth/is-still-verified.js b/packages/backend/src/apps/odoo/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/odoo/auth/verify-credentials.js b/packages/backend/src/apps/odoo/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..30aec786a0aa88914465e13260c2307ce4fd5166 --- /dev/null +++ b/packages/backend/src/apps/odoo/auth/verify-credentials.js @@ -0,0 +1,15 @@ +import { authenticate } from '../common/xmlrpc-client.js'; + +const verifyCredentials = async ($) => { + try { + await authenticate($); + + await $.auth.set({ + screenName: `${$.auth.data.email} @ ${$.auth.data.databaseName} - ${$.auth.data.host}`, + }); + } catch (error) { + throw new Error('Failed while authorizing!'); + } +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/odoo/common/xmlrpc-client.js b/packages/backend/src/apps/odoo/common/xmlrpc-client.js new file mode 100644 index 0000000000000000000000000000000000000000..d45cb9be0c23d31dd6f34b2b5c31452caa5c2c1a --- /dev/null +++ b/packages/backend/src/apps/odoo/common/xmlrpc-client.js @@ -0,0 +1,53 @@ +import { join } from 'node:path'; +import xmlrpc from 'xmlrpc'; + +export const asyncMethodCall = async ($, { method, params, path }) => { + return new Promise((resolve, reject) => { + const client = getClient($, { path }); + + client.methodCall(method, params, (error, response) => { + if (error != null) { + // something went wrong on the server side, display the error returned by Odoo + reject(error); + } + + resolve(response); + }); + }); +}; + +export const getClient = ($, { path = 'common' }) => { + const host = $.auth.data.host; + const port = Number($.auth.data.port); + const secure = $.auth.data.secure === 'true'; + const createClientFunction = secure + ? xmlrpc.createSecureClient + : xmlrpc.createClient; + + return createClientFunction({ + host, + port, + path: join('/xmlrpc/2', path), + }); +}; + +export const authenticate = async ($) => { + const uid = await asyncMethodCall($, { + method: 'authenticate', + params: [ + $.auth.data.databaseName, + $.auth.data.email, + $.auth.data.apiKey, + [], + ], + }); + + if (!Number.isInteger(uid)) { + // failed to authenticate + throw new Error( + 'Failed to connect to the Odoo server. Please, check the credentials!' + ); + } + + return uid; +}; diff --git a/packages/backend/src/apps/odoo/index.js b/packages/backend/src/apps/odoo/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6bfbfb0b1e2c70db4cdeb4a0ab5cb3b1224d5a1f --- /dev/null +++ b/packages/backend/src/apps/odoo/index.js @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Odoo', + key: 'odoo', + iconUrl: '{BASE_URL}/apps/odoo/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/odoo/connection', + supportsConnections: true, + baseUrl: 'https://odoo.com', + apiBaseUrl: '', + primaryColor: '9c5789', + auth, + actions, +}); diff --git a/packages/backend/src/apps/openai/actions/check-moderation/index.js b/packages/backend/src/apps/openai/actions/check-moderation/index.js new file mode 100644 index 0000000000000000000000000000000000000000..331acc7fe063df4956a869138c73ab223e15520d --- /dev/null +++ b/packages/backend/src/apps/openai/actions/check-moderation/index.js @@ -0,0 +1,30 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Check moderation', + key: 'checkModeration', + description: + 'Checks for hate, hate/threatening, self-harm, sexual, sexual/minors, violence, or violence/graphic content in the given text.', + arguments: [ + { + label: 'Input', + key: 'input', + type: 'string', + required: true, + variables: true, + description: 'The text to analyze.', + }, + ], + + async run($) { + const { data } = await $.http.post('/v1/moderations', { + input: $.step.parameters.input, + }); + + const result = data?.results[0]; + + $.setActionItem({ + raw: result, + }); + }, +}); diff --git a/packages/backend/src/apps/openai/actions/index.js b/packages/backend/src/apps/openai/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..45dbad51b9e6b39db16a0a63f79847e057a8804e --- /dev/null +++ b/packages/backend/src/apps/openai/actions/index.js @@ -0,0 +1,5 @@ +import checkModeration from './check-moderation/index.js'; +import sendPrompt from './send-prompt/index.js'; +import sendChatPrompt from './send-chat-prompt/index.js'; + +export default [checkModeration, sendChatPrompt, sendPrompt]; diff --git a/packages/backend/src/apps/openai/actions/send-chat-prompt/index.js b/packages/backend/src/apps/openai/actions/send-chat-prompt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2865a69ab324ec3609ebbbe7d9ff1cd99202b254 --- /dev/null +++ b/packages/backend/src/apps/openai/actions/send-chat-prompt/index.js @@ -0,0 +1,138 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Send chat prompt', + key: 'sendChatPrompt', + description: 'Creates a completion for the provided prompt and parameters.', + arguments: [ + { + label: 'Model', + key: 'model', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listModels', + }, + ], + }, + }, + { + label: 'Messages', + key: 'messages', + type: 'dynamic', + required: true, + description: 'Add or remove messages as needed', + value: [{ role: 'system', body: '' }], + fields: [ + { + label: 'Role', + key: 'role', + type: 'dropdown', + required: true, + options: [ + { + label: 'System', + value: 'system', + }, + { + label: 'User', + value: 'user', + }, + ], + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: + 'The maximum number of tokens to generate in the completion.', + }, + { + label: 'Stop Sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + description: + 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with Top P probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, + }, + ], + + async run($) { + const payload = { + model: $.step.parameters.model, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + stop: $.step.parameters.stopSequence || null, + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + messages: $.step.parameters.messages.map((message) => ({ + role: message.role, + content: message.content, + })), + }; + const { data } = await $.http.post('/v1/chat/completions', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/openai/actions/send-prompt/index.js b/packages/backend/src/apps/openai/actions/send-prompt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..786b81e10f334d1e4b751faa6dcf1b834b37310f --- /dev/null +++ b/packages/backend/src/apps/openai/actions/send-prompt/index.js @@ -0,0 +1,110 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Send prompt', + key: 'sendPrompt', + description: 'Creates a completion for the provided prompt and parameters.', + arguments: [ + { + label: 'Model', + key: 'model', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listModels', + }, + ], + }, + }, + { + label: 'Prompt', + key: 'prompt', + type: 'string', + required: true, + variables: true, + description: 'The text to analyze.', + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: + 'The maximum number of tokens to generate in the completion.', + }, + { + label: 'Stop Sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + description: + 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with Top P probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, + }, + ], + + async run($) { + const payload = { + model: $.step.parameters.model, + prompt: $.step.parameters.prompt, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + stop: $.step.parameters.stopSequence || null, + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + }; + const { data } = await $.http.post('/v1/completions', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/openai/assets/favicon.svg b/packages/backend/src/apps/openai/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..b62b84eb144e7679e9ad93882da71d38730c2ade --- /dev/null +++ b/packages/backend/src/apps/openai/assets/favicon.svg @@ -0,0 +1,6 @@ + + OpenAI + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/openai/auth/index.js b/packages/backend/src/apps/openai/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cd9c891d08c46f7bfe6a4acd9d3d866e58b59612 --- /dev/null +++ b/packages/backend/src/apps/openai/auth/index.js @@ -0,0 +1,34 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'OpenAI API key of your account.', + docUrl: 'https://automatisch.io/docs/openai#api-key', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/openai/auth/is-still-verified.js b/packages/backend/src/apps/openai/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..3e6c90956d1613d176b0ae8569a7ad3d15e75f94 --- /dev/null +++ b/packages/backend/src/apps/openai/auth/is-still-verified.js @@ -0,0 +1,6 @@ +const isStillVerified = async ($) => { + await $.http.get('/v1/models'); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/openai/auth/verify-credentials.js b/packages/backend/src/apps/openai/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..7f43f8842c509d42cab92335287999e4fa0179aa --- /dev/null +++ b/packages/backend/src/apps/openai/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v1/models'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/openai/common/add-auth-header.js b/packages/backend/src/apps/openai/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..f9f5acbacced965668558aa951c023cc94185fc7 --- /dev/null +++ b/packages/backend/src/apps/openai/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiKey}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/openai/dynamic-data/index.js b/packages/backend/src/apps/openai/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6db480461685464fdcd2f19add69ce027ea8fa37 --- /dev/null +++ b/packages/backend/src/apps/openai/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listModels from './list-models/index.js'; + +export default [listModels]; diff --git a/packages/backend/src/apps/openai/dynamic-data/list-models/index.js b/packages/backend/src/apps/openai/dynamic-data/list-models/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a8e8153824e70e0efaffdfb90b4fa09d228d5a1a --- /dev/null +++ b/packages/backend/src/apps/openai/dynamic-data/list-models/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List models', + key: 'listModels', + + async run($) { + const response = await $.http.get('/v1/models'); + + const models = response.data.data.map((model) => { + return { + value: model.id, + name: model.id, + }; + }); + + return { data: models }; + }, +}; diff --git a/packages/backend/src/apps/openai/index.js b/packages/backend/src/apps/openai/index.js new file mode 100644 index 0000000000000000000000000000000000000000..04e288fd1b282750a4ed09d3d7fbd7a46602cc8c --- /dev/null +++ b/packages/backend/src/apps/openai/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'OpenAI', + key: 'openai', + baseUrl: 'https://openai.com', + apiBaseUrl: 'https://api.openai.com', + iconUrl: '{BASE_URL}/apps/openai/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/openai/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-activity/index.js b/packages/backend/src/apps/pipedrive/actions/create-activity/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ca2a42bf48507bd030cd02c90d910db987e3d47c --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-activity/index.js @@ -0,0 +1,198 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create activity', + key: 'createActivity', + description: 'Creates a new activity.', + arguments: [ + { + label: 'Subject', + key: 'subject', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Assigned To', + key: 'userId', + type: 'dropdown', + required: false, + description: + 'If omitted, the activity will be assigned to the user of the connected account.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Person', + key: 'personId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listPersons', + }, + ], + }, + }, + { + label: 'Deal', + key: 'dealId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDeals', + }, + ], + }, + }, + { + label: 'Is done?', + key: 'isDone', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'No', + value: 0, + }, + { + label: 'Yes', + value: 1, + }, + ], + }, + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listActivityTypes', + }, + ], + }, + }, + { + label: 'Due Date', + key: 'dueDate', + type: 'string', + required: false, + description: 'Format must be YYYY-MM-DD', + variables: true, + }, + { + label: 'Due Time', + key: 'dueTime', + type: 'string', + required: false, + description: 'Format must be HH:MM', + variables: true, + }, + { + label: 'Duration', + key: 'duration', + type: 'string', + required: false, + description: 'Format must be HH:MM', + variables: true, + }, + { + label: 'Note', + key: 'note', + type: 'string', + required: false, + description: 'Accepts HTML format', + variables: true, + }, + ], + + async run($) { + const { + subject, + organizationId, + userId, + personId, + dealId, + isDone, + type, + dueTime, + dueDate, + duration, + note, + } = $.step.parameters; + + const fields = { + subject: subject, + org_id: organizationId, + user_id: userId, + person_id: personId, + deal_id: dealId, + done: isDone, + type: type, + due_time: dueTime, + due_date: dueDate, + duration: duration, + note: note, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/activities', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-deal/index.js b/packages/backend/src/apps/pipedrive/actions/create-deal/index.js new file mode 100644 index 0000000000000000000000000000000000000000..55493c54cb76d245d5efb2cfb98fb9040ed79dd9 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-deal/index.js @@ -0,0 +1,222 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create deal', + key: 'createDeal', + description: 'Creates a new deal.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Creation Date', + key: 'addTime', + type: 'string', + required: false, + description: + 'Requires admin access to Pipedrive account. Format: YYYY-MM-DD HH:MM:SS', + variables: true, + }, + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'Open', + value: 'open', + }, + { + label: 'Won', + value: 'won', + }, + { + label: 'Lost', + value: 'lost', + }, + { + label: 'Deleted', + value: 'deleted', + }, + ], + }, + { + label: 'Lost Reason', + key: 'lostReason', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Stage', + key: 'stageId', + type: 'dropdown', + required: false, + value: '1', + description: + 'The ID of the stage this deal will be added to. If omitted, the deal will be placed in the first stage of the default pipeline.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listStages', + }, + ], + }, + }, + { + label: 'Owner', + key: 'userId', + type: 'dropdown', + required: false, + description: + 'Select user who will be marked as the owner of this deal. If omitted, the authorized user will be used.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: 'Organization this deal will be associated with.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Person', + key: 'personId', + type: 'dropdown', + required: false, + description: 'Person this deal will be associated with.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listPersons', + }, + ], + }, + }, + { + label: 'Probability', + key: 'probability', + type: 'string', + required: false, + description: + 'The success probability percentage of the deal. Used/shown only when deal_probability for the pipeline of the deal is enabled.', + variables: true, + }, + { + label: 'Expected Close Date', + key: 'expectedCloseDate', + type: 'string', + required: false, + description: + 'The expected close date of the deal. In ISO 8601 format: YYYY-MM-DD.', + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: false, + description: 'The value of the deal. If omitted, value will be set to 0.', + variables: true, + }, + { + label: 'Currency', + key: 'currency', + type: 'dropdown', + required: false, + description: + 'The currency of the deal. Accepts a 3-character currency code. If omitted, currency will be set to the default currency of the authorized user.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCurrencies', + }, + ], + }, + }, + ], + + async run($) { + const { + title, + addTime, + status, + lostReason, + stageId, + userId, + organizationId, + personId, + probability, + expectedCloseDate, + value, + currency, + } = $.step.parameters; + + const fields = { + title: title, + value: value, + add_time: addTime, + status: status, + lost_reason: lostReason, + stage_id: stageId, + user_id: userId, + org_id: organizationId, + person_id: personId, + probability: probability, + expected_close_date: expectedCloseDate, + currency: currency, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/deals', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-lead/index.js b/packages/backend/src/apps/pipedrive/actions/create-lead/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b17129e143612f7971426de9caf39a123a13873d --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-lead/index.js @@ -0,0 +1,181 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create lead', + key: 'createLead', + description: 'Creates a new lead.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Person', + key: 'personId', + type: 'dropdown', + required: false, + description: + 'Lead must be associated with at least one person or organization.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listPersons', + }, + ], + }, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: + 'Lead must be associated with at least one person or organization.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Owner', + key: 'ownerId', + type: 'dropdown', + required: false, + description: + 'Select user who will be marked as the owner of this lead. If omitted, the authorized user will be used.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Lead Labels', + key: 'labelIds', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Label', + key: 'leadLabelId', + type: 'dropdown', + required: false, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadLabels', + }, + ], + }, + }, + ], + }, + { + label: 'Expected Close Date', + key: 'expectedCloseDate', + type: 'string', + required: false, + description: 'E.g. 2023-10-23', + variables: true, + }, + { + label: 'Lead Value', + key: 'value', + type: 'string', + required: false, + description: 'E.g. 150', + variables: true, + }, + { + label: 'Lead Value Currency', + key: 'currency', + type: 'dropdown', + required: false, + description: 'This field is required if a Lead Value amount is provided.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCurrencies', + }, + ], + }, + }, + ], + + async run($) { + const { + title, + personId, + organizationId, + ownerId, + labelIds, + expectedCloseDate, + value, + currency, + } = $.step.parameters; + + const onlyLabelIds = labelIds + .map((labelId) => labelId.leadLabelId) + .filter(Boolean); + + const labelValue = {}; + + if (value) { + labelValue.amount = Number(value); + } + if (currency) { + labelValue.currency = currency; + } + + const fields = { + title: title, + person_id: Number(personId), + organization_id: Number(organizationId), + owner_id: Number(ownerId), + expected_close_date: expectedCloseDate, + label_ids: onlyLabelIds, + value: labelValue, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/leads', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-note/index.js b/packages/backend/src/apps/pipedrive/actions/create-note/index.js new file mode 100644 index 0000000000000000000000000000000000000000..781d87b5b5f0f5e4c17d0e15b7c92bf14bb72720 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-note/index.js @@ -0,0 +1,198 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create note', + key: 'createNote', + description: 'Creates a new note.', + arguments: [ + { + label: 'Content', + key: 'content', + type: 'string', + required: true, + description: 'Supports some HTML formatting.', + variables: true, + }, + { + label: 'Deal', + key: 'dealId', + type: 'dropdown', + required: false, + description: + 'Note must be associated with at least one deal, person, organization, or lead.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDeals', + }, + ], + }, + }, + { + label: 'Pin note on specified deal?', + key: 'pinnedDeal', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'No', + value: 0, + }, + { + label: 'Yes', + value: 1, + }, + ], + }, + { + label: 'Person', + key: 'personId', + type: 'dropdown', + required: false, + description: + 'Note must be associated with at least one deal, person, organization, or lead.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listPersons', + }, + ], + }, + }, + { + label: 'Pin note on specified person?', + key: 'pinnedPerson', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'No', + value: 0, + }, + { + label: 'Yes', + value: 1, + }, + ], + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: + 'Note must be associated with at least one deal, person, organization, or lead.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Pin note on specified organization?', + key: 'pinnedOrganization', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'No', + value: 0, + }, + { + label: 'Yes', + value: 1, + }, + ], + }, + { + label: 'Lead', + key: 'leadId', + type: 'dropdown', + required: false, + description: + 'Note must be associated with at least one deal, person, organization, or lead.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeads', + }, + ], + }, + }, + { + label: 'Pin note on specified lead?', + key: 'pinnedLead', + type: 'dropdown', + required: false, + description: '', + options: [ + { + label: 'No', + value: 0, + }, + { + label: 'Yes', + value: 1, + }, + ], + }, + ], + + async run($) { + const { + content, + dealId, + pinnedDeal, + personId, + pinnedPerson, + organizationId, + pinnedOrganization, + leadId, + pinnedLead, + } = $.step.parameters; + + const fields = { + content: content, + deal_id: dealId, + pinned_to_deal_flag: pinnedDeal, + person_id: personId, + pinned_to_person_flag: pinnedPerson, + org_id: organizationId, + pinned_to_organization_flag: pinnedOrganization, + lead_id: leadId, + pinned_to_lead_flag: pinnedLead, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/notes', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-organization/index.js b/packages/backend/src/apps/pipedrive/actions/create-organization/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4f8d49505a6fabd93ce75b4a9a3302aa908d4c93 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-organization/index.js @@ -0,0 +1,74 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create organization', + key: 'createOrganization', + description: 'Creates a new organization.', + arguments: [ + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Owner', + key: 'ownerId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Label', + key: 'labelId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizationLabelField', + }, + ], + }, + }, + ], + + async run($) { + const { name, ownerId, labelId } = $.step.parameters; + + const fields = { + name: name, + owner_id: ownerId, + label: labelId, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/organizations', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/create-person/index.js b/packages/backend/src/apps/pipedrive/actions/create-person/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fa96da266a40510877d2a43340f0cf5ac720b373 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/create-person/index.js @@ -0,0 +1,133 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { filterProvidedFields } from '../../common/filter-provided-fields.js'; + +export default defineAction({ + name: 'Create person', + key: 'createPerson', + description: 'Creates a new person.', + arguments: [ + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Owner', + key: 'ownerId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Emails', + key: 'emails', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Email', + key: 'email', + type: 'string', + required: false, + description: '', + variables: true, + }, + ], + }, + { + label: 'Phones', + key: 'phones', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Phone', + key: 'phone', + type: 'string', + required: false, + description: '', + variables: true, + }, + ], + }, + { + label: 'Label', + key: 'labelId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listPersonLabelField', + }, + ], + }, + }, + ], + + async run($) { + const { name, ownerId, organizationId, labelId } = $.step.parameters; + const emails = $.step.parameters.emails; + const emailValues = emails.map((entry) => entry.email); + const phones = $.step.parameters.phones; + const phoneValues = phones.map((entry) => entry.phone); + + const fields = { + name: name, + owner_id: ownerId, + org_id: organizationId, + email: emailValues, + phone: phoneValues, + label: labelId, + }; + + const body = filterProvidedFields(fields); + + const { + data: { data }, + } = await $.http.post('/api/v1/persons', body); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/actions/index.js b/packages/backend/src/apps/pipedrive/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..81460de66307ca1cd526a0c5127e6d6673d563a0 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/actions/index.js @@ -0,0 +1,15 @@ +import createActivity from './create-activity/index.js'; +import createDeal from './create-deal/index.js'; +import createLead from './create-lead/index.js'; +import createNote from './create-note/index.js'; +import createOrganization from './create-organization/index.js'; +import createPerson from './create-person/index.js'; + +export default [ + createActivity, + createDeal, + createLead, + createNote, + createOrganization, + createPerson, +]; diff --git a/packages/backend/src/apps/pipedrive/assets/favicon.svg b/packages/backend/src/apps/pipedrive/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..5efad428c07210934d2a9190a337f981bfc92ff7 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/pipedrive/auth/generate-auth-url.js b/packages/backend/src/apps/pipedrive/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..9079010872768ddd759782aa8f0f77f36698e785 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/auth/generate-auth-url.js @@ -0,0 +1,18 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + }); + + const url = `https://oauth.pipedrive.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/pipedrive/auth/index.js b/packages/backend/src/apps/pipedrive/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..732ed018c16866bd301d861331823b79a9ce4a18 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/pipedrive/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Pipedrive, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/pipedrive/auth/is-still-verified.js b/packages/backend/src/apps/pipedrive/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6d792b125dc06c5812c8375c0c31e511dc71619a --- /dev/null +++ b/packages/backend/src/apps/pipedrive/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/pipedrive/auth/refresh-token.js b/packages/backend/src/apps/pipedrive/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..c0d84f6a2fe4e216bbc796446eff040089175ad8 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/auth/refresh-token.js @@ -0,0 +1,35 @@ +import { URLSearchParams } from 'node:url'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const response = await $.http.post( + 'https://oauth.pipedrive.com/oauth/token', + params.toString(), + { + headers, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + await $.auth.set({ + accessToken: response.data.access_token, + refreshToken: response.data.refresh_token, + tokenType: response.data.token_type, + expiresIn: response.data.expires_in, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/pipedrive/auth/verify-credentials.js b/packages/backend/src/apps/pipedrive/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..2cb1b508d7d55a584b4ec1bfddac211b7d0cc04b --- /dev/null +++ b/packages/backend/src/apps/pipedrive/auth/verify-credentials.js @@ -0,0 +1,58 @@ +import { URLSearchParams } from 'url'; +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code, + redirect_uri: redirectUri, + }); + + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const response = await $.http.post( + `https://oauth.pipedrive.com/oauth/token`, + params.toString(), + { headers } + ); + + const { + access_token: accessToken, + api_domain: apiDomain, + expires_in: expiresIn, + refresh_token: refreshToken, + scope: scope, + token_type: tokenType, + } = response.data; + + await $.auth.set({ + accessToken, + apiDomain, + expiresIn, + refreshToken, + scope, + tokenType, + }); + + const user = await getCurrentUser($); + + const screenName = [user.name, user.company_domain] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + userId: user.id, + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/pipedrive/common/add-auth-header.js b/packages/backend/src/apps/pipedrive/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..679ef605b5ef24bc6b90eb0a33e4d18f3134ad20 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/common/add-auth-header.js @@ -0,0 +1,12 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; + + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/pipedrive/common/filter-provided-fields.js b/packages/backend/src/apps/pipedrive/common/filter-provided-fields.js new file mode 100644 index 0000000000000000000000000000000000000000..1da42951acded348aa3a25d812ae182aa5ba9988 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/common/filter-provided-fields.js @@ -0,0 +1,16 @@ +import isObject from 'lodash/isObject.js'; + +export function filterProvidedFields(body) { + return Object.keys(body).reduce((result, key) => { + const value = body[key]; + if (isObject(value)) { + const filteredNestedObj = filterProvidedFields(value); + if (Object.keys(filteredNestedObj).length > 0) { + result[key] = filteredNestedObj; + } + } else if (body[key]) { + result[key] = value; + } + return result; + }, {}); +} diff --git a/packages/backend/src/apps/pipedrive/common/get-current-user.js b/packages/backend/src/apps/pipedrive/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..469245b604320216f749d61815df034200769b9d --- /dev/null +++ b/packages/backend/src/apps/pipedrive/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get(`${$.auth.data.apiDomain}/api/v1/users/me`); + const currentUser = response.data.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/pipedrive/common/set-base-url.js b/packages/backend/src/apps/pipedrive/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..3e81406872c590261f34d68e44a28269a4f23330 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/common/set-base-url.js @@ -0,0 +1,10 @@ +const setBaseUrl = ($, requestConfig) => { + const { apiDomain } = $.auth.data; + + if (apiDomain) { + requestConfig.baseURL = apiDomain; + } + + return requestConfig; +}; +export default setBaseUrl; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..42af32e6e2a45ae10d49be8704cccc2f6d0c537e --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/index.js @@ -0,0 +1,25 @@ +import listActivityTypes from './list-activity-types/index.js'; +import listCurrencies from './list-currencies/index.js'; +import listDeals from './list-deals/index.js'; +import listLeadLabels from './list-lead-labels/index.js'; +import listLeads from './list-leads/index.js'; +import listOrganizationLabelField from './list-organization-label-field/index.js'; +import listOrganizations from './list-organizations/index.js'; +import listPersonLabelField from './list-person-label-field/index.js'; +import listPersons from './list-persons/index.js'; +import listStages from './list-stages/index.js'; +import listUsers from './list-users/index.js'; + +export default [ + listActivityTypes, + listCurrencies, + listDeals, + listLeadLabels, + listLeads, + listOrganizationLabelField, + listOrganizations, + listPersonLabelField, + listPersons, + listStages, + listUsers, +]; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-activity-types/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-activity-types/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ad54e9138c1302946921e1dc989022b2d19768ec --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-activity-types/index.js @@ -0,0 +1,25 @@ +export default { + name: 'List activity types', + key: 'listActivityTypes', + + async run($) { + const activityTypes = { + data: [], + }; + + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/activityTypes` + ); + + if (data.data?.length) { + for (const activityType of data.data) { + activityTypes.data.push({ + value: activityType.key_string, + name: activityType.name, + }); + } + } + + return activityTypes; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-currencies/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-currencies/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9a00e7dd9470ec2ebcf5b0e48602a0d24fd118ae --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-currencies/index.js @@ -0,0 +1,25 @@ +export default { + name: 'List currencies', + key: 'listCurrencies', + + async run($) { + const currencies = { + data: [], + }; + + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/currencies` + ); + + if (data.data?.length) { + for (const currency of data.data) { + currencies.data.push({ + value: currency.code, + name: currency.name, + }); + } + } + + return currencies; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-deals/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-deals/index.js new file mode 100644 index 0000000000000000000000000000000000000000..901ec22866300d2de844bcfca334c4c19ef8184e --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-deals/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List deals', + key: 'listDeals', + + async run($) { + const deals = { + data: [], + }; + + const params = { + sort: 'add_time DESC', + }; + + const { data } = await $.http.get(`${$.auth.data.apiDomain}/api/v1/deals`, { + params, + }); + + if (data.data?.length) { + for (const deal of data.data) { + deals.data.push({ + value: deal.id, + name: deal.title, + }); + } + } + + return deals; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-lead-labels/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-lead-labels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b75c3c7a1b1cd2f7119a1dfc621c9eede7a6974f --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-lead-labels/index.js @@ -0,0 +1,26 @@ +export default { + name: 'List lead labels', + key: 'listLeadLabels', + + async run($) { + const leadLabels = { + data: [], + }; + + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/leadLabels` + ); + + if (data.data?.length) { + for (const leadLabel of data.data) { + const name = `${leadLabel.name} (${leadLabel.color})`; + leadLabels.data.push({ + value: leadLabel.id, + name, + }); + } + } + + return leadLabels; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-leads/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-leads/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e92bda658a6afb47d0adb7c74603887a34c42a6b --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-leads/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List leads', + key: 'listLeads', + + async run($) { + const leads = { + data: [], + }; + + const params = { + sort: 'add_time DESC', + }; + + const { data } = await $.http.get(`${$.auth.data.apiDomain}/api/v1/leads`, { + params, + }); + + if (data.data?.length) { + for (const lead of data.data) { + leads.data.push({ + value: lead.id, + name: lead.title, + }); + } + } + + return leads; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-organization-label-field/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-organization-label-field/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1cd84ef6957c8f961a4b8ce9c8abe4eded307e3c --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-organization-label-field/index.js @@ -0,0 +1,28 @@ +export default { + name: 'List organization label field', + key: 'listOrganizationLabelField', + + async run($) { + const labelFields = { + data: [], + }; + + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/organizationFields` + ); + + const labelField = data.data.filter((field) => field.key === 'label'); + const labelOptions = labelField[0].options; + + if (labelOptions?.length) { + for (const label of labelOptions) { + labelFields.data.push({ + value: label.id, + name: label.label, + }); + } + } + + return labelFields; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-organizations/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-organizations/index.js new file mode 100644 index 0000000000000000000000000000000000000000..597d95ceb93c24a72bc5df599c08da6551ac9d5f --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-organizations/index.js @@ -0,0 +1,25 @@ +export default { + name: 'List organizations', + key: 'listOrganizations', + + async run($) { + const organizations = { + data: [], + }; + + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/organizations` + ); + + if (data.data?.length) { + for (const organization of data.data) { + organizations.data.push({ + value: organization.id, + name: organization.name, + }); + } + } + + return organizations; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-person-label-field/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-person-label-field/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ce1dfffb6af0c9d0b3ec900ffd7de57d691cdaa6 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-person-label-field/index.js @@ -0,0 +1,38 @@ +export default { + name: 'List person label field', + key: 'listPersonLabelField', + + async run($) { + const personFields = { + data: [], + }; + + const params = { + start: 0, + limit: 100, + }; + + do { + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/personFields`, + { params } + ); + params.start = data.additional_data?.pagination?.next_start; + + const labelField = data.data?.filter( + (personField) => personField.key === 'label' + ); + const labelOptions = labelField[0].options; + + if (labelOptions?.length) { + for (const label of labelOptions) { + personFields.data.push({ + value: label.id, + name: label.label, + }); + } + } + } while (params.start); + return personFields; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-persons/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-persons/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dadd841c638c1ee6b487138ccf71f4690b618588 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-persons/index.js @@ -0,0 +1,33 @@ +export default { + name: 'List persons', + key: 'listPersons', + + async run($) { + const persons = { + data: [], + }; + + const params = { + start: 0, + limit: 100, + }; + + do { + const { data } = await $.http.get( + `${$.auth.data.apiDomain}/api/v1/persons`, + { params } + ); + params.start = data.additional_data?.pagination?.next_start; + + if (data.data?.length) { + for (const person of data.data) { + persons.data.push({ + value: person.id, + name: person.name, + }); + } + } + } while (params.start); + return persons; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-stages/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-stages/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8a0f4893ea6945d3b1ded4c8d8da92a9911b2f1b --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-stages/index.js @@ -0,0 +1,23 @@ +export default { + name: 'List stages', + key: 'listStages', + + async run($) { + const stages = { + data: [], + }; + + const { data } = await $.http.get('/api/v1/stages'); + + if (data.data?.length) { + for (const stage of data.data) { + stages.data.push({ + value: stage.id, + name: stage.name, + }); + } + } + + return stages; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/dynamic-data/list-users/index.js b/packages/backend/src/apps/pipedrive/dynamic-data/list-users/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b07433bf6ecc19a38a81299c7f94d8d718e72cb7 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/dynamic-data/list-users/index.js @@ -0,0 +1,23 @@ +export default { + name: 'List users', + key: 'listUsers', + + async run($) { + const users = { + data: [], + }; + + const { data } = await $.http.get(`${$.auth.data.apiDomain}/api/v1/users`); + + if (data.data?.length) { + for (const user of data.data) { + users.data.push({ + value: user.id, + name: user.name, + }); + } + } + + return users; + }, +}; diff --git a/packages/backend/src/apps/pipedrive/index.js b/packages/backend/src/apps/pipedrive/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9d1b168957016adc4f24e4646a74511b5658dd71 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/index.js @@ -0,0 +1,23 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Pipedrive', + key: 'pipedrive', + baseUrl: '', + apiBaseUrl: '', + iconUrl: '{BASE_URL}/apps/pipedrive/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/pipedrive/connection', + primaryColor: 'FFFFFF', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/pipedrive/triggers/index.js b/packages/backend/src/apps/pipedrive/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e2e3b9f28ad3485a71f535b57e6ea475974c6c8d --- /dev/null +++ b/packages/backend/src/apps/pipedrive/triggers/index.js @@ -0,0 +1,6 @@ +import newActivities from './new-activities/index.js'; +import newDeals from './new-deals/index.js'; +import newLeads from './new-leads/index.js'; +import newNotes from './new-notes/index.js'; + +export default [newActivities, newDeals, newLeads, newNotes]; diff --git a/packages/backend/src/apps/pipedrive/triggers/new-activities/index.js b/packages/backend/src/apps/pipedrive/triggers/new-activities/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b8edd6e5fe70c096df4e2382a762f072f9b93a6e --- /dev/null +++ b/packages/backend/src/apps/pipedrive/triggers/new-activities/index.js @@ -0,0 +1,38 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New activities', + key: 'newActivities', + pollInterval: 15, + description: 'Triggers when a new activity is created.', + arguments: [], + + async run($) { + const params = { + start: 0, + limit: 100, + sort: 'add_time DESC', + }; + + do { + const { data } = await $.http.get('/api/v1/activities', { + params, + }); + + if (!data?.data?.length) { + return; + } + + params.start = data.additional_data?.pagination?.next_start; + + for (const activity of data.data) { + $.pushTriggerItem({ + raw: activity, + meta: { + internalId: activity.id.toString(), + }, + }); + } + } while (params.start); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/triggers/new-deals/index.js b/packages/backend/src/apps/pipedrive/triggers/new-deals/index.js new file mode 100644 index 0000000000000000000000000000000000000000..33abde67e87642e5ca5dc04c0037254c9edd38d7 --- /dev/null +++ b/packages/backend/src/apps/pipedrive/triggers/new-deals/index.js @@ -0,0 +1,38 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New deals', + key: 'newDeals', + pollInterval: 15, + description: 'Triggers when a new deal is created.', + arguments: [], + + async run($) { + const params = { + start: 0, + limit: 100, + sort: 'add_time DESC', + }; + + do { + const { data } = await $.http.get('/api/v1/deals', { + params, + }); + + if (!data?.data?.length) { + return; + } + + params.start = data.additional_data?.pagination?.next_start; + + for (const deal of data.data) { + $.pushTriggerItem({ + raw: deal, + meta: { + internalId: deal.id.toString(), + }, + }); + } + } while (params.start); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/triggers/new-leads/index.js b/packages/backend/src/apps/pipedrive/triggers/new-leads/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b29e3fb511020e1b92fd05f24242ce256e82e0ac --- /dev/null +++ b/packages/backend/src/apps/pipedrive/triggers/new-leads/index.js @@ -0,0 +1,38 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New leads', + key: 'newLeads', + pollInterval: 15, + description: 'Triggers when a new lead is created.', + arguments: [], + + async run($) { + const params = { + start: 0, + limit: 100, + sort: 'add_time DESC', + }; + + do { + const { data } = await $.http.get('/api/v1/leads', { + params, + }); + + if (!data?.data?.length) { + return; + } + + params.start = data.additional_data?.pagination?.next_start; + + for (const lead of data.data) { + $.pushTriggerItem({ + raw: lead, + meta: { + internalId: lead.id.toString(), + }, + }); + } + } while (params.start); + }, +}); diff --git a/packages/backend/src/apps/pipedrive/triggers/new-notes/index.js b/packages/backend/src/apps/pipedrive/triggers/new-notes/index.js new file mode 100644 index 0000000000000000000000000000000000000000..183883138d28eef9695838740214a70405627efc --- /dev/null +++ b/packages/backend/src/apps/pipedrive/triggers/new-notes/index.js @@ -0,0 +1,38 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New notes', + key: 'newNotes', + pollInterval: 15, + description: 'Triggers when a new note is created.', + arguments: [], + + async run($) { + const params = { + start: 0, + limit: 100, + sort: 'add_time DESC', + }; + + do { + const { data } = await $.http.get('/api/v1/notes', { + params, + }); + + if (!data?.data?.length) { + return; + } + + params.start = data.additional_data?.pagination?.next_start; + + for (const note of data.data) { + $.pushTriggerItem({ + raw: note, + meta: { + internalId: note.id.toString(), + }, + }); + } + } while (params.start); + }, +}); diff --git a/packages/backend/src/apps/placetel/assets/favicon.svg b/packages/backend/src/apps/placetel/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6df467ad37a4711786f72c28c9dac7fbab8ef8c0 --- /dev/null +++ b/packages/backend/src/apps/placetel/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/backend/src/apps/placetel/auth/index.js b/packages/backend/src/apps/placetel/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..85181dbe96b3251f7ba50fb6b64d226918f5ac0b --- /dev/null +++ b/packages/backend/src/apps/placetel/auth/index.js @@ -0,0 +1,21 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'apiToken', + label: 'API Token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Placetel API Token of your account.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/placetel/auth/is-still-verified.js b/packages/backend/src/apps/placetel/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/placetel/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/placetel/auth/verify-credentials.js b/packages/backend/src/apps/placetel/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..0475497c62f69b15a74ecf66ba91e17b13da258e --- /dev/null +++ b/packages/backend/src/apps/placetel/auth/verify-credentials.js @@ -0,0 +1,9 @@ +const verifyCredentials = async ($) => { + const { data } = await $.http.get('/v2/me'); + + await $.auth.set({ + screenName: `${data.name} @ ${data.company}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/placetel/common/add-auth-header.js b/packages/backend/src/apps/placetel/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..92ac33138a90dfc75148e7d7e2204b25897952df --- /dev/null +++ b/packages/backend/src/apps/placetel/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiToken) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/placetel/dynamic-data/index.js b/packages/backend/src/apps/placetel/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1c3e14e46998a27a7a8a05fe7825635fd24eec90 --- /dev/null +++ b/packages/backend/src/apps/placetel/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listNumbers from './list-numbers/index.js'; + +export default [listNumbers]; diff --git a/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.js b/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..eeccca33b8d67d2664932a8762f63d825b19671e --- /dev/null +++ b/packages/backend/src/apps/placetel/dynamic-data/list-numbers/index.js @@ -0,0 +1,27 @@ +export default { + name: 'List numbers', + key: 'listNumbers', + + async run($) { + const numbers = { + data: [], + }; + + const { data } = await $.http.get('/v2/numbers'); + + if (!data) { + return { data: [] }; + } + + if (data.length) { + for (const number of data) { + numbers.data.push({ + value: number.number, + name: number.number, + }); + } + } + + return numbers; + }, +}; diff --git a/packages/backend/src/apps/placetel/index.js b/packages/backend/src/apps/placetel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..79668d1b16aeb500461c745138882467a34ba71e --- /dev/null +++ b/packages/backend/src/apps/placetel/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Placetel', + key: 'placetel', + iconUrl: '{BASE_URL}/apps/placetel/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/placetel/connection', + supportsConnections: true, + baseUrl: 'https://placetel.de', + apiBaseUrl: 'https://api.placetel.de', + primaryColor: '069dd9', + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/placetel/triggers/hungup-call/index.js b/packages/backend/src/apps/placetel/triggers/hungup-call/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f0f0fba5444092f961dae3a29da7bf6f0f3372c8 --- /dev/null +++ b/packages/backend/src/apps/placetel/triggers/hungup-call/index.js @@ -0,0 +1,145 @@ +import Crypto from 'crypto'; + +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getRawBody from 'raw-body'; + +export default defineTrigger({ + name: 'Hungup Call', + key: 'hungupCall', + type: 'webhook', + description: 'Triggers when a call is hungup.', + arguments: [ + { + label: 'Types', + key: 'types', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: true, + description: + 'Filter events by type. If the types are not specified, all types will be notified.', + variables: true, + options: [ + { label: 'All', value: 'all' }, + { label: 'Voicemail', value: 'voicemail' }, + { label: 'Missed', value: 'missed' }, + { label: 'Blocked', value: 'blocked' }, + { label: 'Accepted', value: 'accepted' }, + { label: 'Busy', value: 'busy' }, + { label: 'Cancelled', value: 'cancelled' }, + { label: 'Unavailable', value: 'unavailable' }, + { label: 'Congestion', value: 'congestion' }, + ], + }, + ], + }, + { + label: 'Numbers', + key: 'numbers', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Number', + key: 'number', + type: 'dropdown', + required: true, + description: + 'Filter events by number. If the numbers are not specified, all numbers will be notified.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listNumbers', + }, + ], + }, + }, + ], + }, + ], + + async run($) { + const stringBody = await getRawBody($.request, { + length: $.request.headers['content-length'], + encoding: true, + }); + + const jsonRequestBody = JSON.parse(stringBody); + + let types = $.step.parameters.types.map((type) => type.type); + + if (types.length === 0) { + types = ['all']; + } + + if (types.includes(jsonRequestBody.type) || types.includes('all')) { + const dataItem = { + raw: jsonRequestBody, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + } + }, + + async testRun($) { + const types = $.step.parameters.types.map((type) => type.type); + + const sampleEventData = { + type: types[0] || 'missed', + duration: 0, + from: '01662223344', + to: '02229997766', + call_id: + '9c81d4776d3977d920a558cbd4f0950b168e32bd4b5cc141a85b6ed3aa530107', + event: 'HungUp', + direction: 'in', + }; + + const dataItem = { + raw: sampleEventData, + meta: { + internalId: sampleEventData.call_id, + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async registerHook($) { + const numbers = $.step.parameters.numbers + .map((number) => number.number) + .filter(Boolean); + + const subscriptionPayload = { + service: 'string', + url: $.webhookUrl, + incoming: false, + outgoing: false, + hungup: true, + accepted: false, + phone: false, + numbers, + }; + + const { data } = await $.http.put('/v2/subscriptions', subscriptionPayload); + + await $.flow.setRemoteWebhookId(data.id); + }, + + async unregisterHook($) { + await $.http.delete(`/v2/subscriptions/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/apps/placetel/triggers/index.js b/packages/backend/src/apps/placetel/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8bb892fb687ee313905fb336ba4ee9bff0d265e5 --- /dev/null +++ b/packages/backend/src/apps/placetel/triggers/index.js @@ -0,0 +1,3 @@ +import hungupCall from './hungup-call/index.js'; + +export default [hungupCall]; diff --git a/packages/backend/src/apps/postgresql/actions/delete/index.js b/packages/backend/src/apps/postgresql/actions/delete/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1f78160ba50b9019f6e1eddcba3c8fd27649720a --- /dev/null +++ b/packages/backend/src/apps/postgresql/actions/delete/index.js @@ -0,0 +1,109 @@ +import defineAction from '../../../../helpers/define-action.js'; +import getClient from '../../common/postgres-client.js'; +import setParams from '../../common/set-run-time-parameters.js'; +import whereClauseOperators from '../../common/where-clause-operators.js'; + +export default defineAction({ + name: 'Delete', + key: 'delete', + description: 'Delete rows found based on the given where clause entries.', + arguments: [ + { + label: 'Schema name', + key: 'schema', + type: 'string', + value: 'public', + required: true, + variables: true, + }, + { + label: 'Table name', + key: 'table', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Where clause entries', + key: 'whereClauseEntries', + type: 'dynamic', + required: true, + fields: [ + { + label: 'Column name', + key: 'columnName', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Operator', + key: 'operator', + type: 'dropdown', + required: true, + variables: true, + options: whereClauseOperators, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Run-time parameters', + key: 'params', + type: 'dynamic', + required: false, + description: 'Change run-time configuration parameters with SET command', + fields: [ + { + label: 'Parameter name', + key: 'parameter', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + const client = getClient($); + await setParams(client, $.step.parameters.params); + + const whereClauseEntries = $.step.parameters.whereClauseEntries; + + const response = await client($.step.parameters.table) + .withSchema($.step.parameters.schema) + .returning('*') + .where((builder) => { + for (const whereClauseEntry of whereClauseEntries) { + const { columnName, operator, value } = whereClauseEntry; + + if (columnName) { + builder.where(columnName, operator, value); + } + } + }) + .del(); + + client.destroy(); + + $.setActionItem({ + raw: { + rows: response, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/postgresql/actions/index.js b/packages/backend/src/apps/postgresql/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b3cbbb15035934943c578ba02a1925e89785e8c5 --- /dev/null +++ b/packages/backend/src/apps/postgresql/actions/index.js @@ -0,0 +1,6 @@ +import insertAction from './insert/index.js'; +import updateAction from './update/index.js'; +import deleteAction from './delete/index.js'; +import SQLQuery from './sql-query/index.js'; + +export default [insertAction, updateAction, deleteAction, SQLQuery]; diff --git a/packages/backend/src/apps/postgresql/actions/insert/index.js b/packages/backend/src/apps/postgresql/actions/insert/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ecf0efb7a5571ed7d62a5601e608b26d2a9f4809 --- /dev/null +++ b/packages/backend/src/apps/postgresql/actions/insert/index.js @@ -0,0 +1,95 @@ +import defineAction from '../../../../helpers/define-action.js'; +import getClient from '../../common/postgres-client.js'; +import setParams from '../../common/set-run-time-parameters.js'; + +export default defineAction({ + name: 'Insert', + key: 'insert', + description: 'Create a new row in a table in specified schema.', + arguments: [ + { + label: 'Schema name', + key: 'schema', + type: 'string', + value: 'public', + required: true, + variables: true, + }, + { + label: 'Table name', + key: 'table', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Column - value entries', + key: 'columnValueEntries', + type: 'dynamic', + required: true, + description: 'Table columns with values', + fields: [ + { + label: 'Column name', + key: 'columnName', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Run-time parameters', + key: 'params', + type: 'dynamic', + required: true, + description: 'Change run-time configuration parameters with SET command', + fields: [ + { + label: 'Parameter name', + key: 'parameter', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + const client = getClient($); + await setParams(client, $.step.parameters.params); + + const fields = $.step.parameters.columnValueEntries; + const data = fields.reduce( + (result, { columnName, value }) => ({ + ...result, + [columnName]: value, + }), + {} + ); + + const response = await client($.step.parameters.table) + .withSchema($.step.parameters.schema) + .returning('*') + .insert(data); + + client.destroy(); + + $.setActionItem({ raw: response[0] }); + }, +}); diff --git a/packages/backend/src/apps/postgresql/actions/sql-query/index.js b/packages/backend/src/apps/postgresql/actions/sql-query/index.js new file mode 100644 index 0000000000000000000000000000000000000000..221259d5771aae996b1e6a8c6ec358b97595ba60 --- /dev/null +++ b/packages/backend/src/apps/postgresql/actions/sql-query/index.js @@ -0,0 +1,57 @@ +import defineAction from '../../../../helpers/define-action.js'; +import getClient from '../../common/postgres-client.js'; +import setParams from '../../common/set-run-time-parameters.js'; + +export default defineAction({ + name: 'SQL query', + key: 'SQLQuery', + description: 'Executes the given SQL statement.', + arguments: [ + { + label: 'SQL statement', + key: 'queryStatement', + type: 'string', + value: 'public', + required: true, + variables: true, + }, + { + label: 'Run-time parameters', + key: 'params', + type: 'dynamic', + required: false, + description: 'Change run-time configuration parameters with SET command', + fields: [ + { + label: 'Parameter name', + key: 'parameter', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + const client = getClient($); + await setParams(client, $.step.parameters.params); + + const queryStatemnt = $.step.parameters.queryStatement; + const { rows } = await client.raw(queryStatemnt); + client.destroy(); + + $.setActionItem({ + raw: { + rows, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/postgresql/actions/update/index.js b/packages/backend/src/apps/postgresql/actions/update/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5f9cc36109791cae25e97ca27d2cfee20cb98002 --- /dev/null +++ b/packages/backend/src/apps/postgresql/actions/update/index.js @@ -0,0 +1,141 @@ +import defineAction from '../../../../helpers/define-action.js'; +import getClient from '../../common/postgres-client.js'; +import setParams from '../../common/set-run-time-parameters.js'; +import whereClauseOperators from '../../common/where-clause-operators.js'; + +export default defineAction({ + name: 'Update', + key: 'update', + description: 'Update rows found based on the given where clause entries.', + arguments: [ + { + label: 'Schema name', + key: 'schema', + type: 'string', + value: 'public', + required: true, + variables: true, + }, + { + label: 'Table name', + key: 'table', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Where clause entries', + key: 'whereClauseEntries', + type: 'dynamic', + required: true, + fields: [ + { + label: 'Column name', + key: 'columnName', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Operator', + key: 'operator', + type: 'dropdown', + required: true, + variables: true, + options: whereClauseOperators, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Column - value entries', + key: 'columnValueEntries', + type: 'dynamic', + required: true, + description: 'Table columns with values', + fields: [ + { + label: 'Column name', + key: 'columnName', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Run-time parameters', + key: 'params', + type: 'dynamic', + required: false, + description: 'Change run-time configuration parameters with SET command', + fields: [ + { + label: 'Parameter name', + key: 'parameter', + type: 'string', + required: true, + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + variables: true, + }, + ], + }, + ], + + async run($) { + const client = getClient($); + await setParams(client, $.step.parameters.params); + + const whereClauseEntries = $.step.parameters.whereClauseEntries; + + const fields = $.step.parameters.columnValueEntries; + const data = fields.reduce( + (result, { columnName, value }) => ({ + ...result, + [columnName]: value, + }), + {} + ); + + const response = await client($.step.parameters.table) + .withSchema($.step.parameters.schema) + .returning('*') + .where((builder) => { + for (const whereClauseEntry of whereClauseEntries) { + const { columnName, operator, value } = whereClauseEntry; + + if (columnName) { + builder.where(columnName, operator, value); + } + } + }) + .update(data); + + client.destroy(); + + $.setActionItem({ + raw: { + rows: response, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/postgresql/assets/favicon.svg b/packages/backend/src/apps/postgresql/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0bdb3e3e7c04c2aa393d3e4b75b902aa568879b6 --- /dev/null +++ b/packages/backend/src/apps/postgresql/assets/favicon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/backend/src/apps/postgresql/auth/index.js b/packages/backend/src/apps/postgresql/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e839ac23558e4e498523f5f9d24b554f1d3f4197 --- /dev/null +++ b/packages/backend/src/apps/postgresql/auth/index.js @@ -0,0 +1,98 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'version', + label: 'PostgreSQL version', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'The version of PostgreSQL database that user want to connect with.', + clickToCopy: false, + }, + { + key: 'host', + label: 'Host', + type: 'string', + required: true, + readOnly: false, + value: '127.0.0.1', + placeholder: null, + description: 'The host of the PostgreSQL database.', + clickToCopy: false, + }, + { + key: 'port', + label: 'Port', + type: 'string', + required: true, + readOnly: false, + value: '5432', + placeholder: null, + description: 'The port of the PostgreSQL database.', + clickToCopy: false, + }, + { + key: 'enableSsl', + label: 'Enable SSL', + type: 'dropdown', + required: true, + readOnly: false, + value: 'false', + description: 'The port of the PostgreSQL database.', + variables: false, + clickToCopy: false, + options: [ + { + label: 'True', + value: 'true', + }, + { + label: 'False', + value: 'false', + }, + ], + }, + { + key: 'database', + label: 'Database name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The database name of the PostgreSQL database.', + clickToCopy: false, + }, + { + key: 'user', + label: 'Database username', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The user who has access on postgres database.', + clickToCopy: false, + }, + { + key: 'password', + label: 'Password', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The password of the PostgreSQL database user.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/postgresql/auth/is-still-verified.js b/packages/backend/src/apps/postgresql/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..270d415ba799b443cb98782cb6af6ffd13b0f23c --- /dev/null +++ b/packages/backend/src/apps/postgresql/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/postgresql/auth/verify-credentials.js b/packages/backend/src/apps/postgresql/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..2249e6005c9ada1864ce4bae48dc730b2648b75b --- /dev/null +++ b/packages/backend/src/apps/postgresql/auth/verify-credentials.js @@ -0,0 +1,25 @@ +import logger from '../../../helpers/logger.js'; +import getClient from '../common/postgres-client.js'; + +const verifyCredentials = async ($) => { + const client = getClient($); + const checkConnection = await client.raw('SELECT 1'); + client.destroy(); + + logger.debug(checkConnection); + + await $.auth.set({ + screenName: `${$.auth.data.user}@${$.auth.data.host}:${$.auth.data.port}/${$.auth.data.database}`, + client: 'pg', + version: $.auth.data.version, + host: $.auth.data.host, + port: Number($.auth.data.port), + enableSsl: + $.auth.data.enableSsl === 'true' || $.auth.data.enableSsl === true, + user: $.auth.data.user, + password: $.auth.data.password, + database: $.auth.data.database, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/postgresql/common/postgres-client.js b/packages/backend/src/apps/postgresql/common/postgres-client.js new file mode 100644 index 0000000000000000000000000000000000000000..e91e0455975312548b59c623649c94507ed9a55b --- /dev/null +++ b/packages/backend/src/apps/postgresql/common/postgres-client.js @@ -0,0 +1,20 @@ +import knex from 'knex'; + +const getClient = ($) => { + const client = knex({ + client: 'pg', + version: $.auth.data.version, + connection: { + host: $.auth.data.host, + port: Number($.auth.data.port), + ssl: $.auth.data.enableSsl === 'true' || $.auth.data.enableSsl === true, + user: $.auth.data.user, + password: $.auth.data.password, + database: $.auth.data.database, + }, + }); + + return client; +}; + +export default getClient; diff --git a/packages/backend/src/apps/postgresql/common/set-run-time-parameters.js b/packages/backend/src/apps/postgresql/common/set-run-time-parameters.js new file mode 100644 index 0000000000000000000000000000000000000000..bf9a1b79845ee724cc91992fc41fbfdc26b12360 --- /dev/null +++ b/packages/backend/src/apps/postgresql/common/set-run-time-parameters.js @@ -0,0 +1,14 @@ +const setParams = async (client, params) => { + for (const { parameter, value } of params) { + if (parameter) { + const bindings = { + parameter, + value, + }; + + await client.raw('SET :parameter: = :value:', bindings); + } + } +}; + +export default setParams; diff --git a/packages/backend/src/apps/postgresql/common/where-clause-operators.js b/packages/backend/src/apps/postgresql/common/where-clause-operators.js new file mode 100644 index 0000000000000000000000000000000000000000..5242a54c43e9c0dbe883f39dc377cc060e229fb8 --- /dev/null +++ b/packages/backend/src/apps/postgresql/common/where-clause-operators.js @@ -0,0 +1,60 @@ +const whereClauseOperators = [ + { + value: "=", + label: "=" + }, + { + value: ">", + label: ">" + }, + { + value: "<", + label: "<" + }, + { + value: ">=", + label: ">=" + }, + { + value: "<=", + label: "<=" + }, + { + value: "<>", + label: "<>" + }, + { + value: "!=", + label: "!=" + }, + { + value: "AND", + label: "AND" + }, + { + value: "OR", + label: "OR" + }, + { + value: "IN", + label: "IN" + }, + { + value: "BETWEEN", + label: "BETWEEN" + }, + { + value: "LIKE", + label: "LIKE" + }, + { + value: "IS NULL", + label: "IS NULL" + }, + { + value: "NOT", + label: "NOT" + } +]; + +export default whereClauseOperators; diff --git a/packages/backend/src/apps/postgresql/index.js b/packages/backend/src/apps/postgresql/index.js new file mode 100644 index 0000000000000000000000000000000000000000..63ea6dc5a5770af59aec2511294fa1193f52197d --- /dev/null +++ b/packages/backend/src/apps/postgresql/index.js @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'PostgreSQL', + key: 'postgresql', + iconUrl: '{BASE_URL}/apps/postgresql/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/postgresql/connection', + supportsConnections: true, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '336791', + auth, + actions, +}); diff --git a/packages/backend/src/apps/pushover/actions/index.js b/packages/backend/src/apps/pushover/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ba1bd4b552a7b1ed323368907d327dba566e0444 --- /dev/null +++ b/packages/backend/src/apps/pushover/actions/index.js @@ -0,0 +1,3 @@ +import sendAPushoverNotification from './send-a-pushover-notification/index.js'; + +export default [sendAPushoverNotification]; diff --git a/packages/backend/src/apps/pushover/actions/send-a-pushover-notification/index.js b/packages/backend/src/apps/pushover/actions/send-a-pushover-notification/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5923298272ae3e574e0ee23a48608ff2a2d84cb3 --- /dev/null +++ b/packages/backend/src/apps/pushover/actions/send-a-pushover-notification/index.js @@ -0,0 +1,133 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send a Pushover Notification', + key: 'sendPushoverNotification', + description: + 'Generates a Pushover notification on the devices you have subscribed to.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: false, + description: 'An optional title displayed with the message.', + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string', + required: true, + description: 'The main message text of your notification.', + variables: true, + }, + { + label: 'Priority', + key: 'priority', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'Lowest (no notification, just in-app message)', value: -2 }, + { label: 'Low (no sound or vibration)', value: -1 }, + { label: 'Normal', value: 0 }, + { label: 'High (bypass quiet hours, highlight)', value: 1 }, + { + label: 'Emergency (repeat every 30 seconds until acknowledged)', + value: 2, + }, + ], + }, + { + label: 'Sound', + key: 'sound', + type: 'dropdown', + required: false, + description: 'Optional sound to override your default.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSounds', + }, + ], + }, + }, + { + label: 'URL', + key: 'url', + type: 'string', + required: false, + description: 'URL to display with message.', + variables: true, + }, + { + label: 'URL Title', + key: 'urlTitle', + type: 'string', + required: false, + description: + 'Title of URL to display, otherwise URL itself will be displayed.', + variables: true, + }, + { + label: 'Devices', + key: 'devices', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Device', + key: 'device', + type: 'dropdown', + required: false, + description: + 'Restrict sending to just these devices on your account.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDevices', + }, + ], + }, + }, + ], + }, + ], + + async run($) { + const { title, message, priority, sound, url, urlTitle } = + $.step.parameters; + + const devices = $.step.parameters.devices; + const allDevices = devices.map((device) => device.device).join(','); + + const payload = { + token: $.auth.data.apiToken, + user: $.auth.data.userKey, + title, + message, + priority, + sound, + url, + url_title: urlTitle, + device: allDevices, + }; + + const { data } = await $.http.post('/1/messages.json', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/pushover/assets/favicon.svg b/packages/backend/src/apps/pushover/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..28492a9ec5f50a4d889deb5318c33d5c2c11abe3 --- /dev/null +++ b/packages/backend/src/apps/pushover/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/backend/src/apps/pushover/auth/index.js b/packages/backend/src/apps/pushover/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..06c90ab1ce432e510d972338d7b09daaf902aa71 --- /dev/null +++ b/packages/backend/src/apps/pushover/auth/index.js @@ -0,0 +1,44 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'userKey', + label: 'User Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'apiToken', + label: 'API Token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/pushover/auth/is-still-verified.js b/packages/backend/src/apps/pushover/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/pushover/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/pushover/auth/verify-credentials.js b/packages/backend/src/apps/pushover/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..27b5ec29ea93beeac8d94462e3b7ef99f5da0b01 --- /dev/null +++ b/packages/backend/src/apps/pushover/auth/verify-credentials.js @@ -0,0 +1,24 @@ +import HttpError from '../../../errors/http.js'; + +const verifyCredentials = async ($) => { + try { + await $.http.post(`/1/users/validate.json`, { + token: $.auth.data.apiToken, + user: $.auth.data.userKey, + }); + } catch (error) { + const noDeviceError = 'user is valid but has no active devices'; + const hasNoDeviceError = + error.response?.data?.errors?.includes(noDeviceError); + + if (!hasNoDeviceError) { + throw new HttpError(error); + } + } + + await $.auth.set({ + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/pushover/dynamic-data/index.js b/packages/backend/src/apps/pushover/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1faccbbb7bdc8f1c0f5232fa92e9d426589577c8 --- /dev/null +++ b/packages/backend/src/apps/pushover/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listDevices from './list-devices/index.js'; +import listSounds from './list-sounds/index.js'; + +export default [listDevices, listSounds]; diff --git a/packages/backend/src/apps/pushover/dynamic-data/list-devices/index.js b/packages/backend/src/apps/pushover/dynamic-data/list-devices/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3b3f74c550bd1105b259923e7c5ff0572dd459d2 --- /dev/null +++ b/packages/backend/src/apps/pushover/dynamic-data/list-devices/index.js @@ -0,0 +1,28 @@ +export default { + name: 'List devices', + key: 'listDevices', + + async run($) { + const devices = { + data: [], + }; + + const { data } = await $.http.post(`/1/users/validate.json`, { + token: $.auth.data.apiToken, + user: $.auth.data.userKey, + }); + + if (!data?.devices?.length) { + return; + } + + for (const device of data.devices) { + devices.data.push({ + value: device, + name: device, + }); + } + + return devices; + }, +}; diff --git a/packages/backend/src/apps/pushover/dynamic-data/list-sounds/index.js b/packages/backend/src/apps/pushover/dynamic-data/list-sounds/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f089479fe3c67055184f2bbe809194b14d02d390 --- /dev/null +++ b/packages/backend/src/apps/pushover/dynamic-data/list-sounds/index.js @@ -0,0 +1,26 @@ +export default { + name: 'List sounds', + key: 'listSounds', + + async run($) { + const sounds = { + data: [], + }; + + const params = { + token: $.auth.data.apiToken, + }; + + const { data } = await $.http.get(`/1/sounds.json`, { params }); + const soundEntries = Object.entries(data.sounds); + + for (const [key, value] of soundEntries) { + sounds.data.push({ + value: key, + name: value, + }); + } + + return sounds; + }, +}; diff --git a/packages/backend/src/apps/pushover/index.js b/packages/backend/src/apps/pushover/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e87f451a2d069e4d422fdfec69fb0033f85ddea9 --- /dev/null +++ b/packages/backend/src/apps/pushover/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Pushover', + key: 'pushover', + baseUrl: 'https://pushover.net', + apiBaseUrl: 'https://api.pushover.net', + iconUrl: '{BASE_URL}/apps/pushover/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/pushover/connection', + primaryColor: '249DF1', + supportsConnections: true, + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/reddit/actions/create-link-post/index.js b/packages/backend/src/apps/reddit/actions/create-link-post/index.js new file mode 100644 index 0000000000000000000000000000000000000000..92770237807b8b3f76fe6160264b2bd0c2415d34 --- /dev/null +++ b/packages/backend/src/apps/reddit/actions/create-link-post/index.js @@ -0,0 +1,53 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { URLSearchParams } from 'url'; + +export default defineAction({ + name: 'Create link post', + key: 'createLinkPost', + description: 'Create a new link post within a subreddit.', + arguments: [ + { + label: 'Title', + key: 'title', + type: 'string', + required: true, + description: + 'Heading for the recent post. Limited to 300 characters or less.', + variables: true, + }, + { + label: 'Subreddit', + key: 'subreddit', + type: 'string', + required: true, + description: 'The subreddit for posting. Note: Exclude /r/.', + variables: true, + }, + { + label: 'Url', + key: 'url', + type: 'string', + required: true, + description: '', + variables: true, + }, + ], + + async run($) { + const { title, subreddit, url } = $.step.parameters; + + const params = new URLSearchParams({ + kind: 'link', + api_type: 'json', + title: title, + sr: subreddit, + url: url, + }); + + const { data } = await $.http.post('/api/submit', params.toString()); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/reddit/actions/index.js b/packages/backend/src/apps/reddit/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3570d5289307ac2680011b6234bfaeb424055fc7 --- /dev/null +++ b/packages/backend/src/apps/reddit/actions/index.js @@ -0,0 +1,3 @@ +import createLinkPost from './create-link-post/index.js'; + +export default [createLinkPost]; diff --git a/packages/backend/src/apps/reddit/assets/favicon.svg b/packages/backend/src/apps/reddit/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e41ae322d45d447a59d9c82d87138baf08f9213c --- /dev/null +++ b/packages/backend/src/apps/reddit/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/reddit/auth/generate-auth-url.js b/packages/backend/src/apps/reddit/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..ccbcda5a3d1884770f4c6a578bd77bb61e66ad2e --- /dev/null +++ b/packages/backend/src/apps/reddit/auth/generate-auth-url.js @@ -0,0 +1,25 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const state = Math.random().toString(); + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + response_type: 'code', + redirect_uri: redirectUri, + duration: 'permanent', + scope: authScope.join(' '), + state, + }); + + const url = `https://www.reddit.com/api/v1/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + originalState: state, + }); +} diff --git a/packages/backend/src/apps/reddit/auth/index.js b/packages/backend/src/apps/reddit/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bd8b8112936747b9d16cc06eb6fa94abbedba9f9 --- /dev/null +++ b/packages/backend/src/apps/reddit/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/reddit/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Reddit, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/reddit/auth/is-still-verified.js b/packages/backend/src/apps/reddit/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..08962895460ec3224f6b3163ca953c3cecf84114 --- /dev/null +++ b/packages/backend/src/apps/reddit/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/reddit/auth/refresh-token.js b/packages/backend/src/apps/reddit/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..c36e5f5061e55bbb6c4b221c23fd32e652469e9a --- /dev/null +++ b/packages/backend/src/apps/reddit/auth/refresh-token.js @@ -0,0 +1,28 @@ +import { URLSearchParams } from 'node:url'; + +const refreshToken = async ($) => { + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + }; + const params = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://www.reddit.com/api/v1/access_token', + params.toString(), + { headers } + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: data.scope, + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/reddit/auth/verify-credentials.js b/packages/backend/src/apps/reddit/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..c39dea60401153ffb66d74ddecedd7c43af84d79 --- /dev/null +++ b/packages/backend/src/apps/reddit/auth/verify-credentials.js @@ -0,0 +1,47 @@ +import getCurrentUser from '../common/get-current-user.js'; +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + if ($.auth.data.originalState !== $.auth.data.state) { + throw new Error(`The 'state' parameter does not match.`); + } + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + }; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code, + redirect_uri: redirectUri, + }); + + const { data } = await $.http.post( + 'https://www.reddit.com/api/v1/access_token', + params.toString(), + { headers } + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + const screenName = currentUser?.name; + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/reddit/common/add-auth-header.js b/packages/backend/src/apps/reddit/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..79f970227dcd76e19729dfeaf34da8f7770a09a8 --- /dev/null +++ b/packages/backend/src/apps/reddit/common/add-auth-header.js @@ -0,0 +1,22 @@ +import appConfig from '../../../config/app.js'; + +const addAuthHeader = ($, requestConfig) => { + const screenName = $.auth.data?.screenName; + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + if (screenName) { + requestConfig.headers[ + 'User-Agent' + ] = `web:automatisch:${appConfig.version} (by /u/${screenName})`; + } else { + requestConfig.headers[ + 'User-Agent' + ] = `web:automatisch:${appConfig.version}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/reddit/common/auth-scope.js b/packages/backend/src/apps/reddit/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..612bf475df6dea5841f9f9986f2fc6bca3f209c2 --- /dev/null +++ b/packages/backend/src/apps/reddit/common/auth-scope.js @@ -0,0 +1,3 @@ +const authScope = ['identity', 'read', 'account', 'submit']; + +export default authScope; diff --git a/packages/backend/src/apps/reddit/common/get-current-user.js b/packages/backend/src/apps/reddit/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..f76851c1b5b723c3c2fa46af3dc1119ba7198993 --- /dev/null +++ b/packages/backend/src/apps/reddit/common/get-current-user.js @@ -0,0 +1,6 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get('/api/v1/me'); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/reddit/index.js b/packages/backend/src/apps/reddit/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e4445bf2e417c0c9b181a42c2c649a25559e5894 --- /dev/null +++ b/packages/backend/src/apps/reddit/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Reddit', + key: 'reddit', + baseUrl: 'https://www.reddit.com', + apiBaseUrl: 'https://oauth.reddit.com', + iconUrl: '{BASE_URL}/apps/reddit/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/reddit/connection', + primaryColor: 'FF4500', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, +}); diff --git a/packages/backend/src/apps/reddit/triggers/index.js b/packages/backend/src/apps/reddit/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c8a159dbbecbcc48860eca52d4cca800932e535d --- /dev/null +++ b/packages/backend/src/apps/reddit/triggers/index.js @@ -0,0 +1,3 @@ +import newPostsMatchingSearch from './new-posts-matching-search/index.js'; + +export default [newPostsMatchingSearch]; diff --git a/packages/backend/src/apps/reddit/triggers/new-posts-matching-search/index.js b/packages/backend/src/apps/reddit/triggers/new-posts-matching-search/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6c586949fda12777b4ad3c9ea777bfdc3caa898b --- /dev/null +++ b/packages/backend/src/apps/reddit/triggers/new-posts-matching-search/index.js @@ -0,0 +1,48 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New posts matching search', + key: 'newPostsMatchingSearch', + pollInterval: 15, + description: 'Triggers when a search string matches a new post.', + arguments: [ + { + label: 'Search Query', + key: 'searchQuery', + type: 'string', + required: true, + description: + 'The term or expression to look for, restricted to 512 characters. If your query contains periods (e.g., automatisch.io), ensure it is enclosed in quotes ("automatisch.io").', + variables: true, + }, + ], + + async run($) { + const { searchQuery } = $.step.parameters; + const params = { + q: searchQuery, + type: 'link', + sort: 'new', + limit: 100, + after: undefined, + }; + + do { + const { data } = await $.http.get('/search', { + params, + }); + params.after = data.data.after; + + if (data.data.children?.length) { + for (const item of data.data.children) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.data.id, + }, + }); + } + } + } while (params.after); + }, +}); diff --git a/packages/backend/src/apps/removebg/actions/index.js b/packages/backend/src/apps/removebg/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f99cc566eeb3660a1902445a9921a518be973374 --- /dev/null +++ b/packages/backend/src/apps/removebg/actions/index.js @@ -0,0 +1,3 @@ +import removeImageBackground from './remove-image-background/index.js'; + +export default [removeImageBackground]; diff --git a/packages/backend/src/apps/removebg/actions/remove-image-background/index.js b/packages/backend/src/apps/removebg/actions/remove-image-background/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c08082231a59d13baf1a3ea6858251c29c088062 --- /dev/null +++ b/packages/backend/src/apps/removebg/actions/remove-image-background/index.js @@ -0,0 +1,83 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Remove image background', + key: 'removeImageBackground', + description: 'Removes the background of an image.', + arguments: [ + { + label: 'Image file', + key: 'imageFileB64', + type: 'string', + required: true, + variables: true, + description: + 'Provide a JPG or PNG file in Base64 format, up to 12 MB (see remove.bg/supported-images)', + }, + { + label: 'Size', + key: 'size', + type: 'dropdown', + required: true, + value: 'auto', + options: [ + { label: 'Auto', value: 'auto' }, + { label: 'Preview (up to 0.25 megapixels)', value: 'preview' }, + { label: 'Full (up to 10 megapixels)', value: 'full' }, + ], + }, + { + label: 'Background color', + key: 'bgColor', + type: 'string', + description: + 'Adds a solid color background. Can be a hex color code (e.g. 81d4fa, fff) or a color name (e.g. green)', + required: false, + }, + { + label: 'Background image URL', + key: 'bgImageUrl', + type: 'string', + description: 'Adds a background image from a URL.', + required: false, + }, + { + label: 'Output image format', + key: 'outputFormat', + type: 'dropdown', + description: 'Note: Use PNG to preserve transparency', + required: true, + value: 'auto', + options: [ + { label: 'Auto', value: 'auto' }, + { label: 'PNG', value: 'png' }, + { label: 'JPG', value: 'jpg' }, + { label: 'ZIP', value: 'zip' }, + ], + }, + ], + async run($) { + const imageFileB64 = $.step.parameters.imageFileB64; + const size = $.step.parameters.size; + const bgColor = $.step.parameters.bgColor; + const bgImageUrl = $.step.parameters.bgImageUrl; + const outputFormat = $.step.parameters.outputFormat; + + const body = JSON.stringify({ + image_file_b64: imageFileB64, + size: size, + bg_color: bgColor, + bg_image_url: bgImageUrl, + format: outputFormat, + }); + + const response = await $.http.post('/removebg', body, { + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/removebg/assets/favicon.svg b/packages/backend/src/apps/removebg/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..80197555613146af25e1105f4044709d0ce6a019 --- /dev/null +++ b/packages/backend/src/apps/removebg/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/removebg/auth/index.js b/packages/backend/src/apps/removebg/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f007aaa6e3ff086d531778f0ff5b5550759bbc0a --- /dev/null +++ b/packages/backend/src/apps/removebg/auth/index.js @@ -0,0 +1,33 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API key of the remove.bg API service.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/removebg/auth/is-still-verified.js b/packages/backend/src/apps/removebg/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/removebg/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/removebg/auth/verify-credentials.js b/packages/backend/src/apps/removebg/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..98441bb81eb3d16d67713948ba18caf61f01b86d --- /dev/null +++ b/packages/backend/src/apps/removebg/auth/verify-credentials.js @@ -0,0 +1,10 @@ +const verifyCredentials = async ($) => { + await $.http.get('/account'); + + await $.auth.set({ + screenName: $.auth.data.screenName, + apiKey: $.auth.data.apiKey, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/removebg/common/add-auth-header.js b/packages/backend/src/apps/removebg/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..c5018c36f3024dbf34a1c307a6b72685cb620708 --- /dev/null +++ b/packages/backend/src/apps/removebg/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers['X-API-Key'] = `${$.auth.data.apiKey}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/removebg/index.js b/packages/backend/src/apps/removebg/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a092418fc98b2279846e9f0ee01e4b880bf103b6 --- /dev/null +++ b/packages/backend/src/apps/removebg/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Remove.bg', + key: 'removebg', + iconUrl: '{BASE_URL}/apps/removebg/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/removebg/connection', + supportsConnections: true, + baseUrl: 'https://www.remove.bg', + apiBaseUrl: 'https://api.remove.bg/v1.0', + primaryColor: '55636c', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/rss/assets/favicon.svg b/packages/backend/src/apps/rss/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..ce961d2469ccf901d51cb31b96346fa9cf491a41 --- /dev/null +++ b/packages/backend/src/apps/rss/assets/favicon.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/rss/index.js b/packages/backend/src/apps/rss/index.js new file mode 100644 index 0000000000000000000000000000000000000000..29040f1c50f68d525c50360ef9ec5012baa83163 --- /dev/null +++ b/packages/backend/src/apps/rss/index.js @@ -0,0 +1,14 @@ +import defineApp from '../../helpers/define-app.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'RSS', + key: 'rss', + iconUrl: '{BASE_URL}/apps/rss/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/rss/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: 'ff8800', + triggers, +}); diff --git a/packages/backend/src/apps/rss/triggers/index.js b/packages/backend/src/apps/rss/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..229deea4d39b1500e2e9f5a7b7c3bdeb97ed07fd --- /dev/null +++ b/packages/backend/src/apps/rss/triggers/index.js @@ -0,0 +1,3 @@ +import newItemsInFeed from './new-items-in-feed/index.js'; + +export default [newItemsInFeed]; diff --git a/packages/backend/src/apps/rss/triggers/new-items-in-feed/index.js b/packages/backend/src/apps/rss/triggers/new-items-in-feed/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f4a6baa16e81891dcfc5cb7c8adf80beb2ea088f --- /dev/null +++ b/packages/backend/src/apps/rss/triggers/new-items-in-feed/index.js @@ -0,0 +1,23 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import newItemsInFeed from './new-items-in-feed.js'; + +export default defineTrigger({ + name: 'New items in feed', + key: 'newItemsInFeed', + description: 'Triggers on new RSS feed item.', + pollInterval: 15, + arguments: [ + { + label: 'Feed URL', + key: 'feedUrl', + type: 'string', + required: true, + description: 'Paste your publicly accessible RSS URL here.', + variables: false, + }, + ], + + async run($) { + await newItemsInFeed($); + }, +}); diff --git a/packages/backend/src/apps/rss/triggers/new-items-in-feed/new-items-in-feed.js b/packages/backend/src/apps/rss/triggers/new-items-in-feed/new-items-in-feed.js new file mode 100644 index 0000000000000000000000000000000000000000..3ce458d216ba0842ab9af9141669714214764a5b --- /dev/null +++ b/packages/backend/src/apps/rss/triggers/new-items-in-feed/new-items-in-feed.js @@ -0,0 +1,44 @@ +import { XMLParser } from 'fast-xml-parser'; +import bcrypt from 'bcrypt'; + +const getInternalId = async (item) => { + if (item.guid) { + return typeof item.guid === 'object' + ? item.guid['#text'].toString() + : item.guid.toString(); + } else if (item.id) { + return typeof item.id === 'object' + ? item.id['#text'].toString() + : item.id.toString(); + } + + return await hashItem(JSON.stringify(item)); +}; + +const hashItem = async (value) => { + return await bcrypt.hash(value, 1); +}; + +const newItemsInFeed = async ($) => { + const { data } = await $.http.get($.step.parameters.feedUrl); + const parser = new XMLParser({ + ignoreAttributes: false, + }); + const parsedData = parser.parse(data); + + // naive implementation to cover atom and rss feeds + const items = parsedData.rss?.channel?.item || parsedData.feed?.entry || []; + + for (const item of items) { + const dataItem = { + raw: item, + meta: { + internalId: await getInternalId(item), + }, + }; + + $.pushTriggerItem(dataItem); + } +}; + +export default newItemsInFeed; diff --git a/packages/backend/src/apps/salesforce/actions/create-attachment/index.js b/packages/backend/src/apps/salesforce/actions/create-attachment/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8a79704cd24f0f931f3c64c1b5211ac542ca08c3 --- /dev/null +++ b/packages/backend/src/apps/salesforce/actions/create-attachment/index.js @@ -0,0 +1,52 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create attachment', + key: 'createAttachment', + description: + 'Creates an attachment of a specified object by given parent ID.', + arguments: [ + { + label: 'Parent ID', + key: 'parentId', + type: 'string', + required: true, + variables: true, + description: + 'ID of the parent object of the attachment. The following objects are supported as parents of attachments: Account, Asset, Campaign, Case, Contact, Contract, Custom objects, EmailMessage, EmailTemplate, Event, Lead, Opportunity, Product2, Solution, Task', + }, + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + variables: true, + description: 'Name of the attached file. Maximum size is 255 characters.', + }, + { + label: 'Body', + key: 'body', + type: 'string', + required: true, + variables: true, + description: 'File data. (Max size is 25MB)', + }, + ], + + async run($) { + const { parentId, name, body } = $.step.parameters; + + const options = { + ParentId: parentId, + Name: name, + Body: body, + }; + + const { data } = await $.http.post( + '/services/data/v56.0/sobjects/Attachment/', + options + ); + + $.setActionItem({ raw: data }); + }, +}); diff --git a/packages/backend/src/apps/salesforce/actions/execute-query/index.js b/packages/backend/src/apps/salesforce/actions/execute-query/index.js new file mode 100644 index 0000000000000000000000000000000000000000..988423214db65f45e94e0b5804c8f0e3e4742093 --- /dev/null +++ b/packages/backend/src/apps/salesforce/actions/execute-query/index.js @@ -0,0 +1,31 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Execute query', + key: 'executeQuery', + description: 'Executes a SOQL query in Salesforce.', + arguments: [ + { + label: 'Query', + key: 'query', + type: 'string', + required: true, + description: + 'Salesforce query string. For example: SELECT Id, Name FROM Account', + variables: true, + }, + ], + + async run($) { + const query = $.step.parameters.query; + + const options = { + params: { + q: query, + }, + }; + + const { data } = await $.http.get('/services/data/v56.0/query', options); + $.setActionItem({ raw: data }); + }, +}); diff --git a/packages/backend/src/apps/salesforce/actions/find-record/index.js b/packages/backend/src/apps/salesforce/actions/find-record/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e696e626f40326c97fe7a2a2ca8eb865ad7b7468 --- /dev/null +++ b/packages/backend/src/apps/salesforce/actions/find-record/index.js @@ -0,0 +1,80 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Find record', + key: 'findRecord', + description: 'Finds a record of a specified object by a field and value.', + arguments: [ + { + label: 'Object', + key: 'object', + type: 'dropdown', + required: true, + variables: true, + description: 'Pick which type of object you want to search for.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listObjects', + }, + ], + }, + }, + { + label: 'Field', + key: 'field', + type: 'dropdown', + description: 'Pick which field to search by', + required: true, + variables: true, + dependsOn: ['parameters.object'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFields', + }, + { + name: 'parameters.object', + value: '{parameters.object}', + }, + ], + }, + }, + { + label: 'Search value', + key: 'searchValue', + type: 'string', + required: true, + variables: true, + }, + ], + + async run($) { + const query = ` + SELECT + FIELDS(ALL) + FROM + ${$.step.parameters.object} + WHERE + ${$.step.parameters.field} = '${$.step.parameters.searchValue}' + LIMIT 1 + `; + + const options = { + params: { + q: query, + }, + }; + + const { data } = await $.http.get('/services/data/v56.0/query', options); + const record = data.records[0]; + + $.setActionItem({ raw: record }); + }, +}); diff --git a/packages/backend/src/apps/salesforce/actions/index.js b/packages/backend/src/apps/salesforce/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b4de19843cd49bcaa92c3efbfef7a88d9796e7a6 --- /dev/null +++ b/packages/backend/src/apps/salesforce/actions/index.js @@ -0,0 +1,5 @@ +import createAttachment from './create-attachment/index.js'; +import executeQuery from './execute-query/index.js'; +import findRecord from './find-record/index.js'; + +export default [findRecord, createAttachment, executeQuery]; diff --git a/packages/backend/src/apps/salesforce/assets/favicon.svg b/packages/backend/src/apps/salesforce/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e82db677c67d19fd553e1703869a66842ac09de5 --- /dev/null +++ b/packages/backend/src/apps/salesforce/assets/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/salesforce/auth/generate-auth-url.js b/packages/backend/src/apps/salesforce/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..cb21c14e124301b0d6666d28aa52bc7bc8ed9a1d --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/generate-auth-url.js @@ -0,0 +1,17 @@ +import qs from 'qs'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = qs.stringify({ + client_id: $.auth.data.consumerKey, + redirect_uri: redirectUri, + response_type: 'code', + }); + + await $.auth.set({ + url: `${$.auth.data.oauth2Url}/authorize?${searchParams}`, + }); +} diff --git a/packages/backend/src/apps/salesforce/auth/index.js b/packages/backend/src/apps/salesforce/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..06d344d1391c861199cf87ea789d686ea67fc1d2 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/index.js @@ -0,0 +1,69 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/salesforce/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Salesforce OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'oauth2Url', + label: 'Salesforce Environment', + type: 'dropdown', + required: true, + readOnly: false, + value: 'https://login.salesforce.com/services/oauth2', + placeholder: null, + description: 'Most people should choose the default, "production".', + clickToCopy: false, + options: [ + { + label: 'production', + value: 'https://login.salesforce.com/services/oauth2', + }, + { + label: 'sandbox', + value: 'https://test.salesforce.com/services/oauth2', + }, + ], + }, + { + key: 'consumerKey', + label: 'Consumer Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'Consumer Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + refreshToken, + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/salesforce/auth/is-still-verified.js b/packages/backend/src/apps/salesforce/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..f59ee3b47dd017e65a838069305a42c9275c0bcf --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/salesforce/auth/refresh-token.js b/packages/backend/src/apps/salesforce/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..07ae487ad45eb3d303fd69c546226a07b2cbee74 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/refresh-token.js @@ -0,0 +1,24 @@ +import qs from 'querystring'; + +const refreshToken = async ($) => { + const searchParams = qs.stringify({ + grant_type: 'refresh_token', + client_id: $.auth.data.consumerKey, + client_secret: $.auth.data.consumerSecret, + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + `${$.auth.data.oauth2Url}/token?${searchParams}` + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + idToken: data.id_token, + instanceUrl: data.instance_url, + signature: data.signature, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/salesforce/auth/verify-credentials.js b/packages/backend/src/apps/salesforce/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..ce65ecad9114511644b27c5469d2e76f2c752492 --- /dev/null +++ b/packages/backend/src/apps/salesforce/auth/verify-credentials.js @@ -0,0 +1,38 @@ +import getCurrentUser from '../common/get-current-user.js'; +import qs from 'qs'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = qs.stringify({ + code: $.auth.data.code, + grant_type: 'authorization_code', + client_id: $.auth.data.consumerKey, + client_secret: $.auth.data.consumerSecret, + redirect_uri: redirectUri, + }); + const { data } = await $.http.post( + `${$.auth.data.oauth2Url}/token?${searchParams}` + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + tokenType: data.token_type, + idToken: data.id_token, + instanceUrl: data.instance_url, + signature: data.signature, + userId: data.id, + screenName: data.instance_url, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.displayName} - ${data.instance_url}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/salesforce/common/add-auth-header.js b/packages/backend/src/apps/salesforce/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..453863d08b83e33dfb99db874543b390d9ac8108 --- /dev/null +++ b/packages/backend/src/apps/salesforce/common/add-auth-header.js @@ -0,0 +1,15 @@ +const addAuthHeader = ($, requestConfig) => { + const { instanceUrl, tokenType, accessToken } = $.auth.data; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } + + if (tokenType && accessToken) { + requestConfig.headers.Authorization = `${tokenType} ${accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/salesforce/common/get-current-user.js b/packages/backend/src/apps/salesforce/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..02d57b542446550fe8890d4a353602cd6cb6b06b --- /dev/null +++ b/packages/backend/src/apps/salesforce/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/services/data/v55.0/chatter/users/me'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/salesforce/dynamic-data/index.js b/packages/backend/src/apps/salesforce/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a5e46e453327be6c3feb26418133564c31a4a414 --- /dev/null +++ b/packages/backend/src/apps/salesforce/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listObjects from './list-objects/index.js'; +import listFields from './list-fields/index.js'; + +export default [listObjects, listFields]; diff --git a/packages/backend/src/apps/salesforce/dynamic-data/list-fields/index.js b/packages/backend/src/apps/salesforce/dynamic-data/list-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c4f20dcbad2ee3178b9e23c79f45ab0078da5665 --- /dev/null +++ b/packages/backend/src/apps/salesforce/dynamic-data/list-fields/index.js @@ -0,0 +1,23 @@ +export default { + name: 'List fields', + key: 'listFields', + + async run($) { + const { object } = $.step.parameters; + + if (!object) return { data: [] }; + + const response = await $.http.get( + `/services/data/v56.0/sobjects/${object}/describe` + ); + + const fields = response.data.fields.map((field) => { + return { + value: field.name, + name: field.label, + }; + }); + + return { data: fields }; + }, +}; diff --git a/packages/backend/src/apps/salesforce/dynamic-data/list-objects/index.js b/packages/backend/src/apps/salesforce/dynamic-data/list-objects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dc670cfd46e6b20f3663d2ec05c95c92b3878263 --- /dev/null +++ b/packages/backend/src/apps/salesforce/dynamic-data/list-objects/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List objects', + key: 'listObjects', + + async run($) { + const response = await $.http.get('/services/data/v56.0/sobjects'); + + const objects = response.data.sobjects.map((object) => { + return { + value: object.name, + name: object.label, + }; + }); + + return { data: objects }; + }, +}; diff --git a/packages/backend/src/apps/salesforce/index.js b/packages/backend/src/apps/salesforce/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bad582364b91038827472c1b2c5f0dc528ddf2fa --- /dev/null +++ b/packages/backend/src/apps/salesforce/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Salesforce', + key: 'salesforce', + iconUrl: '{BASE_URL}/apps/salesforce/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/connections/salesforce', + supportsConnections: true, + baseUrl: 'https://salesforce.com', + apiBaseUrl: '', + primaryColor: '00A1E0', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/salesforce/triggers/index.js b/packages/backend/src/apps/salesforce/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..66fb62c9d1c5a13f42486b29cb92e3191e5ab292 --- /dev/null +++ b/packages/backend/src/apps/salesforce/triggers/index.js @@ -0,0 +1,3 @@ +import updatedFieldInRecords from './updated-field-in-records/index.js'; + +export default [updatedFieldInRecords]; diff --git a/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/index.js b/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9f948a7ba10a9f22591413cf7b98d196a83206d7 --- /dev/null +++ b/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/index.js @@ -0,0 +1,55 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import updatedFieldInRecords from './updated-field-in-records.js'; + +export default defineTrigger({ + name: 'Updated field in records', + key: 'updatedFieldInRecords', + pollInterval: 15, + description: 'Triggers when a field is updated in a record.', + arguments: [ + { + label: 'Object', + key: 'object', + type: 'dropdown', + required: true, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listObjects', + }, + ], + }, + }, + { + label: 'Field', + key: 'field', + type: 'dropdown', + description: 'Track updates by this field', + required: true, + variables: false, + dependsOn: ['parameters.object'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFields', + }, + { + name: 'parameters.object', + value: '{parameters.object}', + }, + ], + }, + }, + ], + + async run($) { + await updatedFieldInRecords($); + }, +}); diff --git a/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/updated-field-in-records.js b/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/updated-field-in-records.js new file mode 100644 index 0000000000000000000000000000000000000000..b451d00d98bbfc83378fa56f0c29bcd7af79c7f5 --- /dev/null +++ b/packages/backend/src/apps/salesforce/triggers/updated-field-in-records/updated-field-in-records.js @@ -0,0 +1,43 @@ +function getQuery(object, limit, offset) { + return ` + SELECT + FIELDS(ALL) + FROM + ${object} + ORDER BY LastModifiedDate DESC + LIMIT ${limit} + OFFSET ${offset} + `; +} + +const updatedFieldInRecord = async ($) => { + const limit = 200; + const field = $.step.parameters.field; + const object = $.step.parameters.object; + + let response; + let offset = 0; + do { + const options = { + params: { + q: getQuery(object, limit, offset), + }, + }; + + response = await $.http.get('/services/data/v56.0/query', options); + const records = response.data.records; + + for (const record of records) { + $.pushTriggerItem({ + raw: record, + meta: { + internalId: `${record.Id}-${record[field]}`, + }, + }); + } + + offset = offset + limit; + } while (response.data.records?.length === limit); +}; + +export default updatedFieldInRecord; diff --git a/packages/backend/src/apps/scheduler/assets/favicon.svg b/packages/backend/src/apps/scheduler/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..359793bdf47e9b8592d7a927ed921eea34846988 --- /dev/null +++ b/packages/backend/src/apps/scheduler/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/scheduler/common/cron-times.js b/packages/backend/src/apps/scheduler/common/cron-times.js new file mode 100644 index 0000000000000000000000000000000000000000..8682a65ce3f261fad8320eab58e4b92ab797451f --- /dev/null +++ b/packages/backend/src/apps/scheduler/common/cron-times.js @@ -0,0 +1,10 @@ +const cronTimes = { + everyHour: '0 * * * *', + everyHourExcludingWeekends: '0 * * * 1-5', + everyDayAt: (hour) => `0 ${hour} * * *`, + everyDayExcludingWeekendsAt: (hour) => `0 ${hour} * * 1-5`, + everyWeekOnAndAt: (weekday, hour) => `0 ${hour} * * ${weekday}`, + everyMonthOnAndAt: (day, hour) => `0 ${hour} ${day} * *`, +}; + +export default cronTimes; diff --git a/packages/backend/src/apps/scheduler/common/get-date-time-object.js b/packages/backend/src/apps/scheduler/common/get-date-time-object.js new file mode 100644 index 0000000000000000000000000000000000000000..f0d46358cc286da4e0531df59a856101a9b9bc90 --- /dev/null +++ b/packages/backend/src/apps/scheduler/common/get-date-time-object.js @@ -0,0 +1,14 @@ +import { DateTime } from 'luxon'; + +export default function getDateTimeObjectRepresentation(dateTime) { + const defaults = dateTime.toObject(); + + return { + ...defaults, + ISO_date_time: dateTime.toISO(), + pretty_date: dateTime.toLocaleString(DateTime.DATE_MED), + pretty_time: dateTime.toLocaleString(DateTime.TIME_WITH_SECONDS), + pretty_day_of_week: dateTime.toFormat('cccc'), + day_of_week: dateTime.weekday, + }; +} diff --git a/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.js b/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.js new file mode 100644 index 0000000000000000000000000000000000000000..0c0a77ab1fa98abfc6bc85456bd20cc5d7b3bbdc --- /dev/null +++ b/packages/backend/src/apps/scheduler/common/get-next-cron-date-time.js @@ -0,0 +1,12 @@ +import { DateTime } from 'luxon'; +import cronParser from 'cron-parser'; + +export default function getNextCronDateTime(cronString) { + const cronDate = cronParser.parseExpression(cronString); + const matchingNextCronDateTime = cronDate.next(); + const matchingNextDateTime = DateTime.fromJSDate( + matchingNextCronDateTime.toDate() + ); + + return matchingNextDateTime; +} diff --git a/packages/backend/src/apps/scheduler/index.js b/packages/backend/src/apps/scheduler/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6319248fefbfe1ab65a0a47914da64c05cb1d3ae --- /dev/null +++ b/packages/backend/src/apps/scheduler/index.js @@ -0,0 +1,15 @@ +import defineApp from '../../helpers/define-app.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Scheduler', + key: 'scheduler', + iconUrl: '{BASE_URL}/apps/scheduler/assets/favicon.svg', + docUrl: 'https://automatisch.io/docs/scheduler', + authDocUrl: '{DOCS_URL}/apps/scheduler/connection', + baseUrl: '', + apiBaseUrl: '', + primaryColor: '0059F7', + supportsConnections: false, + triggers, +}); diff --git a/packages/backend/src/apps/scheduler/triggers/every-day/index.js b/packages/backend/src/apps/scheduler/triggers/every-day/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7e1516452b2497ab66db918bf86bb1be7363b233 --- /dev/null +++ b/packages/backend/src/apps/scheduler/triggers/every-day/index.js @@ -0,0 +1,166 @@ +import { DateTime } from 'luxon'; + +import defineTrigger from '../../../../helpers/define-trigger.js'; +import cronTimes from '../../common/cron-times.js'; +import getNextCronDateTime from '../../common/get-next-cron-date-time.js'; +import getDateTimeObjectRepresentation from '../../common/get-date-time-object.js'; + +export default defineTrigger({ + name: 'Every day', + key: 'everyDay', + description: 'Triggers every day.', + arguments: [ + { + label: 'Trigger on weekends?', + key: 'triggersOnWeekend', + type: 'dropdown', + description: 'Should this flow trigger on Saturday and Sunday?', + required: true, + value: true, + variables: false, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + { + label: 'Time of day', + key: 'hour', + type: 'dropdown', + required: true, + value: null, + variables: false, + options: [ + { + label: '00:00', + value: 0, + }, + { + label: '01:00', + value: 1, + }, + { + label: '02:00', + value: 2, + }, + { + label: '03:00', + value: 3, + }, + { + label: '04:00', + value: 4, + }, + { + label: '05:00', + value: 5, + }, + { + label: '06:00', + value: 6, + }, + { + label: '07:00', + value: 7, + }, + { + label: '08:00', + value: 8, + }, + { + label: '09:00', + value: 9, + }, + { + label: '10:00', + value: 10, + }, + { + label: '11:00', + value: 11, + }, + { + label: '12:00', + value: 12, + }, + { + label: '13:00', + value: 13, + }, + { + label: '14:00', + value: 14, + }, + { + label: '15:00', + value: 15, + }, + { + label: '16:00', + value: 16, + }, + { + label: '17:00', + value: 17, + }, + { + label: '18:00', + value: 18, + }, + { + label: '19:00', + value: 19, + }, + { + label: '20:00', + value: 20, + }, + { + label: '21:00', + value: 21, + }, + { + label: '22:00', + value: 22, + }, + { + label: '23:00', + value: 23, + }, + ], + }, + ], + + getInterval(parameters) { + if (parameters.triggersOnWeekend) { + return cronTimes.everyDayAt(parameters.hour); + } + + return cronTimes.everyDayExcludingWeekendsAt(parameters.hour); + }, + + async run($) { + const nextCronDateTime = getNextCronDateTime( + this.getInterval($.step.parameters) + ); + const dateTime = DateTime.now(); + const dateTimeObjectRepresentation = getDateTimeObjectRepresentation( + $.execution.testRun ? nextCronDateTime : dateTime + ); + + const dataItem = { + raw: dateTimeObjectRepresentation, + meta: { + internalId: dateTime.toMillis().toString(), + }, + }; + + $.pushTriggerItem(dataItem); + }, +}); diff --git a/packages/backend/src/apps/scheduler/triggers/every-hour/index.js b/packages/backend/src/apps/scheduler/triggers/every-hour/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a3dd9178c58b47d0b23e044867b2287808971877 --- /dev/null +++ b/packages/backend/src/apps/scheduler/triggers/every-hour/index.js @@ -0,0 +1,60 @@ +import { DateTime } from 'luxon'; + +import defineTrigger from '../../../../helpers/define-trigger.js'; +import cronTimes from '../../common/cron-times.js'; +import getNextCronDateTime from '../../common/get-next-cron-date-time.js'; +import getDateTimeObjectRepresentation from '../../common/get-date-time-object.js'; + +export default defineTrigger({ + name: 'Every hour', + key: 'everyHour', + description: 'Triggers every hour.', + arguments: [ + { + label: 'Trigger on weekends?', + key: 'triggersOnWeekend', + type: 'dropdown', + description: 'Should this flow trigger on Saturday and Sunday?', + required: true, + value: true, + variables: false, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + ], + + getInterval(parameters) { + if (parameters.triggersOnWeekend) { + return cronTimes.everyHour; + } + + return cronTimes.everyHourExcludingWeekends; + }, + + async run($) { + const nextCronDateTime = getNextCronDateTime( + this.getInterval($.step.parameters) + ); + const dateTime = DateTime.now(); + const dateTimeObjectRepresentation = getDateTimeObjectRepresentation( + $.execution.testRun ? nextCronDateTime : dateTime + ); + + const dataItem = { + raw: dateTimeObjectRepresentation, + meta: { + internalId: dateTime.toMillis().toString(), + }, + }; + + $.pushTriggerItem(dataItem); + }, +}); diff --git a/packages/backend/src/apps/scheduler/triggers/every-month/index.js b/packages/backend/src/apps/scheduler/triggers/every-month/index.js new file mode 100644 index 0000000000000000000000000000000000000000..36a78d678c980cbf7b124126fe45ef6cb27e82e0 --- /dev/null +++ b/packages/backend/src/apps/scheduler/triggers/every-month/index.js @@ -0,0 +1,282 @@ +import { DateTime } from 'luxon'; + +import defineTrigger from '../../../../helpers/define-trigger.js'; +import cronTimes from '../../common/cron-times.js'; +import getNextCronDateTime from '../../common/get-next-cron-date-time.js'; +import getDateTimeObjectRepresentation from '../../common/get-date-time-object.js'; + +export default defineTrigger({ + name: 'Every month', + key: 'everyMonth', + description: 'Triggers every month.', + arguments: [ + { + label: 'Day of the month', + key: 'day', + type: 'dropdown', + required: true, + value: null, + variables: false, + options: [ + { + label: '1', + value: 1, + }, + { + label: '2', + value: 2, + }, + { + label: '3', + value: 3, + }, + { + label: '4', + value: 4, + }, + { + label: '5', + value: 5, + }, + { + label: '6', + value: 6, + }, + { + label: '7', + value: 7, + }, + { + label: '8', + value: 8, + }, + { + label: '9', + value: 9, + }, + { + label: '10', + value: 10, + }, + { + label: '11', + value: 11, + }, + { + label: '12', + value: 12, + }, + { + label: '13', + value: 13, + }, + { + label: '14', + value: 14, + }, + { + label: '15', + value: 15, + }, + { + label: '16', + value: 16, + }, + { + label: '17', + value: 17, + }, + { + label: '18', + value: 18, + }, + { + label: '19', + value: 19, + }, + { + label: '20', + value: 20, + }, + { + label: '21', + value: 21, + }, + { + label: '22', + value: 22, + }, + { + label: '23', + value: 23, + }, + { + label: '24', + value: 24, + }, + { + label: '25', + value: 25, + }, + { + label: '26', + value: 26, + }, + { + label: '27', + value: 27, + }, + { + label: '28', + value: 28, + }, + { + label: '29', + value: 29, + }, + { + label: '30', + value: 30, + }, + { + label: '31', + value: 31, + }, + ], + }, + { + label: 'Time of day', + key: 'hour', + type: 'dropdown', + required: true, + value: null, + variables: false, + options: [ + { + label: '00:00', + value: 0, + }, + { + label: '01:00', + value: 1, + }, + { + label: '02:00', + value: 2, + }, + { + label: '03:00', + value: 3, + }, + { + label: '04:00', + value: 4, + }, + { + label: '05:00', + value: 5, + }, + { + label: '06:00', + value: 6, + }, + { + label: '07:00', + value: 7, + }, + { + label: '08:00', + value: 8, + }, + { + label: '09:00', + value: 9, + }, + { + label: '10:00', + value: 10, + }, + { + label: '11:00', + value: 11, + }, + { + label: '12:00', + value: 12, + }, + { + label: '13:00', + value: 13, + }, + { + label: '14:00', + value: 14, + }, + { + label: '15:00', + value: 15, + }, + { + label: '16:00', + value: 16, + }, + { + label: '17:00', + value: 17, + }, + { + label: '18:00', + value: 18, + }, + { + label: '19:00', + value: 19, + }, + { + label: '20:00', + value: 20, + }, + { + label: '21:00', + value: 21, + }, + { + label: '22:00', + value: 22, + }, + { + label: '23:00', + value: 23, + }, + ], + }, + ], + + getInterval(parameters) { + const interval = cronTimes.everyMonthOnAndAt( + parameters.day, + parameters.hour + ); + + return interval; + }, + + async run($) { + const nextCronDateTime = getNextCronDateTime( + this.getInterval($.step.parameters) + ); + const dateTime = DateTime.now(); + const dateTimeObjectRepresentation = getDateTimeObjectRepresentation( + $.execution.testRun ? nextCronDateTime : dateTime + ); + + const dataItem = { + raw: dateTimeObjectRepresentation, + meta: { + internalId: dateTime.toMillis().toString(), + }, + }; + + $.pushTriggerItem(dataItem); + }, +}); diff --git a/packages/backend/src/apps/scheduler/triggers/every-week/index.js b/packages/backend/src/apps/scheduler/triggers/every-week/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9ee925abbb03116ae66fa575e108e28c90588627 --- /dev/null +++ b/packages/backend/src/apps/scheduler/triggers/every-week/index.js @@ -0,0 +1,186 @@ +import { DateTime } from 'luxon'; + +import defineTrigger from '../../../../helpers/define-trigger.js'; +import cronTimes from '../../common/cron-times.js'; +import getNextCronDateTime from '../../common/get-next-cron-date-time.js'; +import getDateTimeObjectRepresentation from '../../common/get-date-time-object.js'; + +export default defineTrigger({ + name: 'Every week', + key: 'everyWeek', + description: 'Triggers every week.', + arguments: [ + { + label: 'Day of the week', + key: 'weekday', + type: 'dropdown', + required: true, + value: null, + variables: false, + options: [ + { + label: 'Monday', + value: 1, + }, + { + label: 'Tuesday', + value: 2, + }, + { + label: 'Wednesday', + value: 3, + }, + { + label: 'Thursday', + value: 4, + }, + { + label: 'Friday', + value: 5, + }, + { + label: 'Saturday', + value: 6, + }, + { + label: 'Sunday', + value: 0, + }, + ], + }, + { + label: 'Time of day', + key: 'hour', + type: 'dropdown', + required: true, + value: null, + variables: false, + options: [ + { + label: '00:00', + value: 0, + }, + { + label: '01:00', + value: 1, + }, + { + label: '02:00', + value: 2, + }, + { + label: '03:00', + value: 3, + }, + { + label: '04:00', + value: 4, + }, + { + label: '05:00', + value: 5, + }, + { + label: '06:00', + value: 6, + }, + { + label: '07:00', + value: 7, + }, + { + label: '08:00', + value: 8, + }, + { + label: '09:00', + value: 9, + }, + { + label: '10:00', + value: 10, + }, + { + label: '11:00', + value: 11, + }, + { + label: '12:00', + value: 12, + }, + { + label: '13:00', + value: 13, + }, + { + label: '14:00', + value: 14, + }, + { + label: '15:00', + value: 15, + }, + { + label: '16:00', + value: 16, + }, + { + label: '17:00', + value: 17, + }, + { + label: '18:00', + value: 18, + }, + { + label: '19:00', + value: 19, + }, + { + label: '20:00', + value: 20, + }, + { + label: '21:00', + value: 21, + }, + { + label: '22:00', + value: 22, + }, + { + label: '23:00', + value: 23, + }, + ], + }, + ], + + getInterval(parameters) { + const interval = cronTimes.everyWeekOnAndAt( + parameters.weekday, + parameters.hour + ); + + return interval; + }, + + async run($) { + const nextCronDateTime = getNextCronDateTime( + this.getInterval($.step.parameters) + ); + const dateTime = DateTime.now(); + const dateTimeObjectRepresentation = getDateTimeObjectRepresentation( + $.execution.testRun ? nextCronDateTime : dateTime + ); + + const dataItem = { + raw: dateTimeObjectRepresentation, + meta: { + internalId: dateTime.toMillis().toString(), + }, + }; + + $.pushTriggerItem(dataItem); + }, +}); diff --git a/packages/backend/src/apps/scheduler/triggers/index.js b/packages/backend/src/apps/scheduler/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3239a453cd1198749c71f3823b77922cc2110a6d --- /dev/null +++ b/packages/backend/src/apps/scheduler/triggers/index.js @@ -0,0 +1,6 @@ +import everyHour from './every-hour/index.js'; +import everyDay from './every-day/index.js'; +import everyWeek from './every-week/index.js'; +import everyMonth from './every-month/index.js'; + +export default [everyHour, everyDay, everyWeek, everyMonth]; diff --git a/packages/backend/src/apps/self-hosted-llm/actions/index.js b/packages/backend/src/apps/self-hosted-llm/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..42d0ac87236f20785bf436df65152b3b64f56e20 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/actions/index.js @@ -0,0 +1,4 @@ +import sendPrompt from './send-prompt/index.js'; +import sendChatPrompt from './send-chat-prompt/index.js'; + +export default [sendChatPrompt, sendPrompt]; diff --git a/packages/backend/src/apps/self-hosted-llm/actions/send-chat-prompt/index.js b/packages/backend/src/apps/self-hosted-llm/actions/send-chat-prompt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2865a69ab324ec3609ebbbe7d9ff1cd99202b254 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/actions/send-chat-prompt/index.js @@ -0,0 +1,138 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Send chat prompt', + key: 'sendChatPrompt', + description: 'Creates a completion for the provided prompt and parameters.', + arguments: [ + { + label: 'Model', + key: 'model', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listModels', + }, + ], + }, + }, + { + label: 'Messages', + key: 'messages', + type: 'dynamic', + required: true, + description: 'Add or remove messages as needed', + value: [{ role: 'system', body: '' }], + fields: [ + { + label: 'Role', + key: 'role', + type: 'dropdown', + required: true, + options: [ + { + label: 'System', + value: 'system', + }, + { + label: 'User', + value: 'user', + }, + ], + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: + 'The maximum number of tokens to generate in the completion.', + }, + { + label: 'Stop Sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + description: + 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with Top P probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, + }, + ], + + async run($) { + const payload = { + model: $.step.parameters.model, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + stop: $.step.parameters.stopSequence || null, + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + messages: $.step.parameters.messages.map((message) => ({ + role: message.role, + content: message.content, + })), + }; + const { data } = await $.http.post('/v1/chat/completions', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/self-hosted-llm/actions/send-prompt/index.js b/packages/backend/src/apps/self-hosted-llm/actions/send-prompt/index.js new file mode 100644 index 0000000000000000000000000000000000000000..786b81e10f334d1e4b751faa6dcf1b834b37310f --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/actions/send-prompt/index.js @@ -0,0 +1,110 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Send prompt', + key: 'sendPrompt', + description: 'Creates a completion for the provided prompt and parameters.', + arguments: [ + { + label: 'Model', + key: 'model', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listModels', + }, + ], + }, + }, + { + label: 'Prompt', + key: 'prompt', + type: 'string', + required: true, + variables: true, + description: 'The text to analyze.', + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: + 'The maximum number of tokens to generate in the completion.', + }, + { + label: 'Stop Sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + description: + 'Single stop sequence where the API will stop generating further tokens. The returned text will not contain the stop sequence.', + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with Top P probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency in the text so far, decreasing the model's likelihood to repeat the same line verbatim.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far, increasing the model's likelihood to talk about new topics.`, + }, + ], + + async run($) { + const payload = { + model: $.step.parameters.model, + prompt: $.step.parameters.prompt, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + stop: $.step.parameters.stopSequence || null, + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + }; + const { data } = await $.http.post('/v1/completions', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/self-hosted-llm/assets/favicon.svg b/packages/backend/src/apps/self-hosted-llm/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..b62b84eb144e7679e9ad93882da71d38730c2ade --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/assets/favicon.svg @@ -0,0 +1,6 @@ + + OpenAI + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/self-hosted-llm/auth/index.js b/packages/backend/src/apps/self-hosted-llm/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9d480537b94c997ba37453eec1e454fa57fb6109 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/auth/index.js @@ -0,0 +1,44 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiUrl', + label: 'API URL', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + docUrl: 'https://automatisch.io/docs/self-hosted-llm#api-url', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + docUrl: 'https://automatisch.io/docs/self-hosted-llm#api-key', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/self-hosted-llm/auth/is-still-verified.js b/packages/backend/src/apps/self-hosted-llm/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..3e6c90956d1613d176b0ae8569a7ad3d15e75f94 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/auth/is-still-verified.js @@ -0,0 +1,6 @@ +const isStillVerified = async ($) => { + await $.http.get('/v1/models'); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/self-hosted-llm/auth/verify-credentials.js b/packages/backend/src/apps/self-hosted-llm/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..7f43f8842c509d42cab92335287999e4fa0179aa --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v1/models'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/self-hosted-llm/common/add-auth-header.js b/packages/backend/src/apps/self-hosted-llm/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..f9f5acbacced965668558aa951c023cc94185fc7 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiKey}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/self-hosted-llm/common/set-base-url.js b/packages/backend/src/apps/self-hosted-llm/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..4dd124ad704d88758a0b1478001361d1d543505d --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/common/set-base-url.js @@ -0,0 +1,9 @@ +const setBaseUrl = ($, requestConfig) => { + if ($.auth.data.apiUrl) { + requestConfig.baseURL = $.auth.data.apiUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/self-hosted-llm/dynamic-data/index.js b/packages/backend/src/apps/self-hosted-llm/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6db480461685464fdcd2f19add69ce027ea8fa37 --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listModels from './list-models/index.js'; + +export default [listModels]; diff --git a/packages/backend/src/apps/self-hosted-llm/dynamic-data/list-models/index.js b/packages/backend/src/apps/self-hosted-llm/dynamic-data/list-models/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a8e8153824e70e0efaffdfb90b4fa09d228d5a1a --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/dynamic-data/list-models/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List models', + key: 'listModels', + + async run($) { + const response = await $.http.get('/v1/models'); + + const models = response.data.data.map((model) => { + return { + value: model.id, + name: model.id, + }; + }); + + return { data: models }; + }, +}; diff --git a/packages/backend/src/apps/self-hosted-llm/index.js b/packages/backend/src/apps/self-hosted-llm/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8e950a5fc1bdd837ab3e7d01437807973afff2dd --- /dev/null +++ b/packages/backend/src/apps/self-hosted-llm/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Self-hosted LLM', + key: 'self-hosted-llm', + baseUrl: '', + apiBaseUrl: '', + iconUrl: '{BASE_URL}/apps/self-hosted-llm/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/self-hosted-llm/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/signalwire/actions/index.js b/packages/backend/src/apps/signalwire/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..18a261f9d10ebb238d25ebbbb8e946ee4e78d4d8 --- /dev/null +++ b/packages/backend/src/apps/signalwire/actions/index.js @@ -0,0 +1,3 @@ +import sendSms from './send-sms/index.js'; + +export default [sendSms]; diff --git a/packages/backend/src/apps/signalwire/actions/send-sms/index.js b/packages/backend/src/apps/signalwire/actions/send-sms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..152ab8d28b6ff043d8f3fc4f03e754f983cfdd50 --- /dev/null +++ b/packages/backend/src/apps/signalwire/actions/send-sms/index.js @@ -0,0 +1,63 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send an SMS', + key: 'sendSms', + description: 'Sends an SMS', + arguments: [ + { + label: 'From Number', + key: 'fromNumber', + type: 'dropdown', + required: true, + description: + 'The number to send the SMS from. Include only country code. Example: 491234567890', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listIncomingPhoneNumbers', + }, + ], + }, + }, + { + label: 'To Number', + key: 'toNumber', + type: 'string', + required: true, + description: + 'The number to send the SMS to. Include only country code. Example: 491234567890', + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string', + required: true, + description: 'The content of the message.', + variables: true, + }, + ], + + async run($) { + const requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages`; + + const Body = $.step.parameters.message; + const From = $.step.parameters.fromNumber; + const To = '+' + $.step.parameters.toNumber.trim(); + + const response = await $.http.post(requestPath, null, { + params: { + Body, + From, + To, + }, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/signalwire/assets/favicon.svg b/packages/backend/src/apps/signalwire/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..1dda203761aaf207ca0bfd072536601d0ca15e63 --- /dev/null +++ b/packages/backend/src/apps/signalwire/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/signalwire/auth/index.js b/packages/backend/src/apps/signalwire/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..59a32bee9f1a21c9e9ef80ba34bbb76cf64d937c --- /dev/null +++ b/packages/backend/src/apps/signalwire/auth/index.js @@ -0,0 +1,64 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'accountSid', + label: 'Project ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Log into your SignalWire account and find the Project ID', + clickToCopy: false, + }, + { + key: 'authToken', + label: 'API Token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Token in the respective project', + clickToCopy: false, + }, + { + key: 'spaceRegion', + label: 'SignalWire Region', + type: 'dropdown', + required: true, + readOnly: false, + value: '', + placeholder: null, + description: 'Most people should choose the default, "US"', + clickToCopy: false, + options: [ + { + label: 'US', + value: '', + }, + { + label: 'EU', + value: 'eu-', + }, + ], + }, + { + key: 'spaceName', + label: 'Space Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Name of your SignalWire space that contains the project', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/signalwire/auth/is-still-verified.js b/packages/backend/src/apps/signalwire/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..270d415ba799b443cb98782cb6af6ffd13b0f23c --- /dev/null +++ b/packages/backend/src/apps/signalwire/auth/is-still-verified.js @@ -0,0 +1,9 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/signalwire/auth/verify-credentials.js b/packages/backend/src/apps/signalwire/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..4b87b86cce028be63a6afec9148c7059f72b2846 --- /dev/null +++ b/packages/backend/src/apps/signalwire/auth/verify-credentials.js @@ -0,0 +1,11 @@ +const verifyCredentials = async ($) => { + const { data } = await $.http.get( + `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}` + ); + + await $.auth.set({ + screenName: `${data.friendly_name} (${$.auth.data.accountSid})`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/signalwire/common/add-auth-header.js b/packages/backend/src/apps/signalwire/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..7059a8c8f0ce53b53343110d41e42cef697d891b --- /dev/null +++ b/packages/backend/src/apps/signalwire/common/add-auth-header.js @@ -0,0 +1,22 @@ +const addAuthHeader = ($, requestConfig) => { + const authData = $.auth.data || {}; + + requestConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + + if (authData.accountSid && authData.authToken) { + requestConfig.auth = { + username: authData.accountSid, + password: authData.authToken, + }; + } + + if (authData.spaceName) { + const serverUrl = `https://${authData.spaceName}.${authData.spaceRegion}signalwire.com`; + + requestConfig.baseURL = serverUrl; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/signalwire/dynamic-data/index.js b/packages/backend/src/apps/signalwire/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..758d4abea90f5cdbace5382a61d7c72c275b33ab --- /dev/null +++ b/packages/backend/src/apps/signalwire/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listIncomingPhoneNumbers from './list-incoming-phone-numbers/index.js'; + +export default [listIncomingPhoneNumbers]; diff --git a/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.js b/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..72fa66a2398093da148bfa5e1098119f07ad7615 --- /dev/null +++ b/packages/backend/src/apps/signalwire/dynamic-data/list-incoming-phone-numbers/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List incoming phone numbers', + key: 'listIncomingPhoneNumbers', + + async run($) { + let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers`; + + const aggregatedResponse = { + data: [], + }; + + do { + const { data } = await $.http.get(requestPath); + + const smsCapableIncomingPhoneNumbers = data.incoming_phone_numbers + .filter((incomingPhoneNumber) => { + return incomingPhoneNumber.capabilities.sms; + }) + .map((incomingPhoneNumber) => { + const friendlyName = incomingPhoneNumber.friendly_name; + const phoneNumber = incomingPhoneNumber.phone_number; + const name = [friendlyName, phoneNumber].filter(Boolean).join(' - '); + + return { + value: phoneNumber, + name, + }; + }); + aggregatedResponse.data.push(...smsCapableIncomingPhoneNumbers); + + requestPath = data.next_page_uri; + } while (requestPath); + + return aggregatedResponse; + }, +}; diff --git a/packages/backend/src/apps/signalwire/index.js b/packages/backend/src/apps/signalwire/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f8135c4438b14353af839022ccca1465178458ec --- /dev/null +++ b/packages/backend/src/apps/signalwire/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'SignalWire', + key: 'signalwire', + iconUrl: '{BASE_URL}/apps/signalwire/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/signalwire/connection', + supportsConnections: true, + baseUrl: 'https://signalwire.com', + apiBaseUrl: '', + primaryColor: '044cf6', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/signalwire/triggers/index.js b/packages/backend/src/apps/signalwire/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c7219e503d403b4eddf8db665df9b9895566b262 --- /dev/null +++ b/packages/backend/src/apps/signalwire/triggers/index.js @@ -0,0 +1,3 @@ +import receiveSms from './receive-sms/index.js'; + +export default [receiveSms]; diff --git a/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.js b/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.js new file mode 100644 index 0000000000000000000000000000000000000000..ab3a6d4e6c7322dddbba68d9b55772a5c8961775 --- /dev/null +++ b/packages/backend/src/apps/signalwire/triggers/receive-sms/fetch-messages.js @@ -0,0 +1,25 @@ +const fetchMessages = async ($) => { + const toNumber = $.step.parameters.toNumber; + + let response; + let requestPath = `/api/laml/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages?To=${toNumber}`; + + do { + response = await $.http.get(requestPath); + + response.data.messages.forEach((message) => { + const dataItem = { + raw: message, + meta: { + internalId: message.date_sent, + }, + }; + + $.pushTriggerItem(dataItem); + }); + + requestPath = response.data.next_page_uri; + } while (requestPath); +}; + +export default fetchMessages; diff --git a/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js b/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d71ab76203834603f52ced7fa7c15cc0a12cb46b --- /dev/null +++ b/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js @@ -0,0 +1,33 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import fetchMessages from './fetch-messages.js'; + +export default defineTrigger({ + name: 'Receive SMS', + key: 'receiveSms', + pollInterval: 15, + description: 'Triggers when a new SMS is received.', + arguments: [ + { + label: 'To Number', + key: 'toNumber', + type: 'dropdown', + required: true, + description: + 'The number to receive the SMS on. It should be a SignalWire number in your project.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listIncomingPhoneNumbers', + }, + ], + }, + }, + ], + + async run($) { + await fetchMessages($); + }, +}); diff --git a/packages/backend/src/apps/slack/actions/find-message/find-message.js b/packages/backend/src/apps/slack/actions/find-message/find-message.js new file mode 100644 index 0000000000000000000000000000000000000000..44de43bc3f7cf066a36b111456cabc39f5bc9431 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/find-message/find-message.js @@ -0,0 +1,22 @@ +const findMessage = async ($, options) => { + const params = { + query: options.query, + sort: options.sortBy, + sort_dir: options.sortDirection, + count: options.count || 1, + }; + + const response = await $.http.get('/search.messages', { + params, + }); + + const data = response.data; + + if (!data.ok && data) { + throw new Error(JSON.stringify(response.data)); + } + + $.setActionItem({ raw: data?.messages.matches[0] }); +}; + +export default findMessage; diff --git a/packages/backend/src/apps/slack/actions/find-message/index.js b/packages/backend/src/apps/slack/actions/find-message/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3a5c06a45b85c694c7e4d593cb627648eed34fe0 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/find-message/index.js @@ -0,0 +1,76 @@ +import defineAction from '../../../../helpers/define-action.js'; +import findMessage from './find-message.js'; + +export default defineAction({ + name: 'Find a message', + key: 'findMessage', + description: 'Finds a message using the Slack feature.', + arguments: [ + { + label: 'Search Query', + key: 'query', + type: 'string', + required: true, + description: + 'Search query to use for finding matching messages. See the Slack Search Documentation for more information on constructing a query.', + variables: true, + }, + { + label: 'Sort by', + key: 'sortBy', + type: 'dropdown', + description: + 'Sort messages by their match strength or by their date. Default is score.', + required: true, + value: 'score', + variables: true, + options: [ + { + label: 'Match strength', + value: 'score', + }, + { + label: 'Message date time', + value: 'timestamp', + }, + ], + }, + { + label: 'Sort direction', + key: 'sortDirection', + type: 'dropdown', + description: + 'Sort matching messages in ascending or descending order. Default is descending.', + required: true, + value: 'desc', + variables: true, + options: [ + { + label: 'Descending (newest or best match first)', + value: 'desc', + }, + { + label: 'Ascending (oldest or worst match first)', + value: 'asc', + }, + ], + }, + ], + + async run($) { + const parameters = $.step.parameters; + const query = parameters.query; + const sortBy = parameters.sortBy; + const sortDirection = parameters.sortDirection; + const count = 1; + + const messages = await findMessage($, { + query, + sortBy, + sortDirection, + count, + }); + + return messages; + }, +}); diff --git a/packages/backend/src/apps/slack/actions/find-user-by-email/index.js b/packages/backend/src/apps/slack/actions/find-user-by-email/index.js new file mode 100644 index 0000000000000000000000000000000000000000..10b99cae9220204c4e5961f1111c44956c3827f4 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/find-user-by-email/index.js @@ -0,0 +1,30 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Find user by email', + key: 'findUserByEmail', + description: 'Finds a user by email.', + arguments: [ + { + label: 'Email', + key: 'email', + type: 'string', + required: true, + variables: true, + }, + ], + + async run($) { + const params = { + email: $.step.parameters.email, + }; + + const { data } = await $.http.get('/users.lookupByEmail', { + params, + }); + + if (data.ok) { + $.setActionItem({ raw: data.user }); + } + }, +}); diff --git a/packages/backend/src/apps/slack/actions/index.js b/packages/backend/src/apps/slack/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f10776f6e9d31b7bb16e2c314a30ac968ea4dfbe --- /dev/null +++ b/packages/backend/src/apps/slack/actions/index.js @@ -0,0 +1,11 @@ +import findMessage from './find-message/index.js'; +import findUserByEmail from './find-user-by-email/index.js'; +import sendMessageToChannel from './send-a-message-to-channel/index.js'; +import sendDirectMessage from './send-a-direct-message/index.js'; + +export default [ + findMessage, + findUserByEmail, + sendMessageToChannel, + sendDirectMessage, +]; diff --git a/packages/backend/src/apps/slack/actions/send-a-direct-message/index.js b/packages/backend/src/apps/slack/actions/send-a-direct-message/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0d6fa916d19eed861532ff1cbd810f64a1a75503 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/send-a-direct-message/index.js @@ -0,0 +1,77 @@ +import defineAction from '../../../../helpers/define-action.js'; +import postMessage from './post-message.js'; + +export default defineAction({ + name: 'Send a direct message', + key: 'sendDirectMessage', + description: + 'Sends a direct message to a user or yourself from the Slackbot.', + arguments: [ + { + label: 'To username', + key: 'toUsername', + type: 'dropdown', + required: true, + description: 'Pick a user to send the message to.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + ], + }, + }, + { + label: 'Message text', + key: 'message', + type: 'string', + required: true, + description: 'The content of your new message.', + variables: true, + }, + { + label: 'Send as a bot?', + key: 'sendAsBot', + type: 'dropdown', + required: false, + value: false, + description: + 'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.', + variables: true, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listFieldsAfterSendAsBot', + }, + { + name: 'parameters.sendAsBot', + value: '{parameters.sendAsBot}', + }, + ], + }, + }, + ], + + async run($) { + const message = await postMessage($); + + return message; + }, +}); diff --git a/packages/backend/src/apps/slack/actions/send-a-direct-message/post-message.js b/packages/backend/src/apps/slack/actions/send-a-direct-message/post-message.js new file mode 100644 index 0000000000000000000000000000000000000000..0045cfcd085929c9f16ef43035b6e53002453af5 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/send-a-direct-message/post-message.js @@ -0,0 +1,46 @@ +import { URL } from 'url'; + +const postMessage = async ($) => { + const { parameters } = $.step; + const toUsername = parameters.toUsername; + const text = parameters.message; + const sendAsBot = parameters.sendAsBot; + const botName = parameters.botName; + const botIcon = parameters.botIcon; + + const data = { + channel: toUsername, + text, + }; + + if (sendAsBot) { + data.username = botName; + try { + // challenging the input to check if it is a URL! + new URL(botIcon); + data.icon_url = botIcon; + } catch { + data.icon_emoji = botIcon; + } + } + + const customConfig = { + sendAsBot, + }; + + const response = await $.http.post('/chat.postMessage', data, { + additionalProperties: customConfig, + }); + + if (response.data.ok === false) { + throw new Error(JSON.stringify(response.data)); + } + + const message = { + raw: response?.data, + }; + + $.setActionItem(message); +}; + +export default postMessage; diff --git a/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5a25e86a3b9da58cc6064b104a126dc7a6821aa0 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js @@ -0,0 +1,76 @@ +import defineAction from '../../../../helpers/define-action.js'; +import postMessage from './post-message.js'; + +export default defineAction({ + name: 'Send a message to channel', + key: 'sendMessageToChannel', + description: 'Sends a message to a channel you specify.', + arguments: [ + { + label: 'Channel', + key: 'channel', + type: 'dropdown', + required: true, + description: 'Pick a channel to send the message to.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listChannels', + }, + ], + }, + }, + { + label: 'Message text', + key: 'message', + type: 'string', + required: true, + description: 'The content of your new message.', + variables: true, + }, + { + label: 'Send as a bot?', + key: 'sendAsBot', + type: 'dropdown', + required: false, + value: false, + description: + 'If you choose no, this message will appear to come from you. Direct messages are always sent by bots.', + variables: true, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + additionalFields: { + type: 'query', + name: 'getDynamicFields', + arguments: [ + { + name: 'key', + value: 'listFieldsAfterSendAsBot', + }, + { + name: 'parameters.sendAsBot', + value: '{parameters.sendAsBot}', + }, + ], + }, + }, + ], + + async run($) { + const message = await postMessage($); + + return message; + }, +}); diff --git a/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js new file mode 100644 index 0000000000000000000000000000000000000000..3b46679510975a6fa9624587427528d3cab530c7 --- /dev/null +++ b/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js @@ -0,0 +1,46 @@ +import { URL } from 'url'; + +const postMessage = async ($) => { + const { parameters } = $.step; + const channelId = parameters.channel; + const text = parameters.message; + const sendAsBot = parameters.sendAsBot; + const botName = parameters.botName; + const botIcon = parameters.botIcon; + + const data = { + channel: channelId, + text, + }; + + if (sendAsBot) { + data.username = botName; + try { + // challenging the input to check if it is a URL! + new URL(botIcon); + data.icon_url = botIcon; + } catch { + data.icon_emoji = botIcon; + } + } + + const customConfig = { + sendAsBot, + }; + + const response = await $.http.post('/chat.postMessage', data, { + additionalProperties: customConfig, + }); + + if (response.data.ok === false) { + throw new Error(JSON.stringify(response.data)); + } + + const message = { + raw: response?.data, + }; + + $.setActionItem(message); +}; + +export default postMessage; diff --git a/packages/backend/src/apps/slack/assets/favicon.svg b/packages/backend/src/apps/slack/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..c09453bbdd707d3c3828ee487064c000c9350022 --- /dev/null +++ b/packages/backend/src/apps/slack/assets/favicon.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/slack/auth/generate-auth-url.js b/packages/backend/src/apps/slack/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..58f225ccb0c1b544c9367a9535de68fa596103bf --- /dev/null +++ b/packages/backend/src/apps/slack/auth/generate-auth-url.js @@ -0,0 +1,62 @@ +import qs from 'qs'; + +const scopes = [ + 'channels:manage', + 'channels:read', + 'channels:join', + 'chat:write', + 'chat:write.customize', + 'chat:write.public', + 'files:write', + 'im:write', + 'mpim:write', + 'team:read', + 'users.profile:read', + 'users:read', + 'workflow.steps:execute', + 'users:read.email', + 'commands', +]; +const userScopes = [ + 'channels:history', + 'channels:read', + 'channels:write', + 'chat:write', + 'emoji:read', + 'files:read', + 'files:write', + 'groups:history', + 'groups:read', + 'groups:write', + 'im:read', + 'im:write', + 'mpim:write', + 'reactions:read', + 'reminders:write', + 'search:read', + 'stars:read', + 'team:read', + 'users.profile:read', + 'users.profile:write', + 'users:read', + 'users:read.email', +]; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = qs.stringify({ + client_id: $.auth.data.consumerKey, + redirect_uri: redirectUri, + scope: scopes.join(','), + user_scope: userScopes.join(','), + }); + + const url = `${$.app.baseUrl}/oauth/v2/authorize?${searchParams}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/slack/auth/index.js b/packages/backend/src/apps/slack/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..48adcf827e5f398d605e6be0ba53817445e4f2bf --- /dev/null +++ b/packages/backend/src/apps/slack/auth/index.js @@ -0,0 +1,46 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/slack/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Slack OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'API Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/slack/auth/is-still-verified.js b/packages/backend/src/apps/slack/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/slack/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/slack/auth/verify-credentials.js b/packages/backend/src/apps/slack/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..c00f7d8b8696b47b10f0afe6aeebfa6f389574a0 --- /dev/null +++ b/packages/backend/src/apps/slack/auth/verify-credentials.js @@ -0,0 +1,45 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = { + code: $.auth.data.code, + client_id: $.auth.data.consumerKey, + client_secret: $.auth.data.consumerSecret, + redirect_uri: redirectUri, + }; + const response = await $.http.post('/oauth.v2.access', null, { params }); + + if (response.data.ok === false) { + throw new Error( + `Error occured while verifying credentials: ${response.data.error}. (More info: https://api.slack.com/methods/oauth.v2.access#errors)` + ); + } + + const { + bot_user_id: botId, + authed_user: { id: userId, access_token: userAccessToken }, + access_token: botAccessToken, + team: { name: teamName }, + } = response.data; + + await $.auth.set({ + botId, + userId, + userAccessToken, + botAccessToken, + screenName: teamName, + token: $.auth.data.accessToken, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.real_name} @ ${teamName}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/slack/common/add-auth-header.js b/packages/backend/src/apps/slack/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..bd650e456f45f2449a8ddff1f2156dd2a236323c --- /dev/null +++ b/packages/backend/src/apps/slack/common/add-auth-header.js @@ -0,0 +1,21 @@ +const addAuthHeader = ($, requestConfig) => { + const authData = $.auth.data; + if ( + requestConfig.headers && + authData?.userAccessToken && + authData?.botAccessToken + ) { + if (requestConfig.additionalProperties?.sendAsBot) { + requestConfig.headers.Authorization = `Bearer ${authData.botAccessToken}`; + } else { + requestConfig.headers.Authorization = `Bearer ${authData.userAccessToken}`; + } + } + + requestConfig.headers['Content-Type'] = + requestConfig.headers['Content-Type'] || 'application/json; charset=utf-8'; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/slack/common/get-current-user.js b/packages/backend/src/apps/slack/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..7b874868d4d0f0da0b215e1f4a98b7c0ea5488b3 --- /dev/null +++ b/packages/backend/src/apps/slack/common/get-current-user.js @@ -0,0 +1,11 @@ +const getCurrentUser = async ($) => { + const params = { + user: $.auth.data.userId, + }; + const response = await $.http.get('/users.info', { params }); + const currentUser = response.data.user; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/slack/dynamic-data/index.js b/packages/backend/src/apps/slack/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e0044a49b90fe4094075e6b8438f418178627c96 --- /dev/null +++ b/packages/backend/src/apps/slack/dynamic-data/index.js @@ -0,0 +1,4 @@ +import listChannels from './list-channels/index.js'; +import listUsers from './list-users/index.js'; + +export default [listChannels, listUsers]; diff --git a/packages/backend/src/apps/slack/dynamic-data/list-channels/index.js b/packages/backend/src/apps/slack/dynamic-data/list-channels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fe0310ca67d279d2fc8a80828c9a4e6bdeed40ec --- /dev/null +++ b/packages/backend/src/apps/slack/dynamic-data/list-channels/index.js @@ -0,0 +1,43 @@ +export default { + name: 'List channels', + key: 'listChannels', + + async run($) { + const channels = { + data: [], + error: null, + }; + + let nextCursor; + do { + const response = await $.http.get('/conversations.list', { + params: { + types: 'public_channel,private_channel', + cursor: nextCursor, + limit: 1000, + }, + }); + + nextCursor = response.data.response_metadata?.next_cursor; + + if (response.data.error === 'missing_scope') { + throw new Error( + `Missing "${response.data.needed}" scope while authorizing. Please, reconnect your connection!` + ); + } + + if (response.data.ok === false) { + throw new Error(JSON.stringify(response.data, null, 2)); + } + + for (const channel of response.data.channels) { + channels.data.push({ + value: channel.id, + name: channel.name, + }); + } + } while (nextCursor); + + return channels; + }, +}; diff --git a/packages/backend/src/apps/slack/dynamic-data/list-users/index.js b/packages/backend/src/apps/slack/dynamic-data/list-users/index.js new file mode 100644 index 0000000000000000000000000000000000000000..49935bc1c65d06598875e91c9db771e51f08c996 --- /dev/null +++ b/packages/backend/src/apps/slack/dynamic-data/list-users/index.js @@ -0,0 +1,43 @@ +export default { + name: 'List users', + key: 'listUsers', + + async run($) { + const users = { + data: [], + error: null, + }; + + let nextCursor; + + do { + const response = await $.http.get('/users.list', { + params: { + cursor: nextCursor, + limit: 1000, + }, + }); + + nextCursor = response.data.response_metadata?.next_cursor; + + if (response.data.error === 'missing_scope') { + throw new Error( + `Missing "${response.data.needed}" scope while authorizing. Please, reconnect your connection!` + ); + } + + if (response.data.ok === false) { + throw new Error(JSON.stringify(response.data, null, 2)); + } + + for (const member of response.data.members) { + users.data.push({ + value: member.id, + name: member.profile.real_name_normalized, + }); + } + } while (nextCursor); + + return users; + }, +}; diff --git a/packages/backend/src/apps/slack/dynamic-fields/index.js b/packages/backend/src/apps/slack/dynamic-fields/index.js new file mode 100644 index 0000000000000000000000000000000000000000..05098366006d115d42d349215fece49bea6b4bf4 --- /dev/null +++ b/packages/backend/src/apps/slack/dynamic-fields/index.js @@ -0,0 +1,3 @@ +import listFieldsAfterSendAsBot from './send-as-bot/index.js'; + +export default [listFieldsAfterSendAsBot]; diff --git a/packages/backend/src/apps/slack/dynamic-fields/send-as-bot/index.js b/packages/backend/src/apps/slack/dynamic-fields/send-as-bot/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a7a23a752ddbcf5a11a1f4ce0d512b3951f46663 --- /dev/null +++ b/packages/backend/src/apps/slack/dynamic-fields/send-as-bot/index.js @@ -0,0 +1,30 @@ +export default { + name: 'List fields after send as bot', + key: 'listFieldsAfterSendAsBot', + + async run($) { + if ($.step.parameters.sendAsBot) { + return [ + { + label: 'Bot name', + key: 'botName', + type: 'string', + required: true, + value: 'Automatisch', + description: + 'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.', + variables: true, + }, + { + label: 'Bot icon', + key: 'botIcon', + type: 'string', + required: false, + description: + 'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:', + variables: true, + }, + ]; + } + }, +}; diff --git a/packages/backend/src/apps/slack/index.js b/packages/backend/src/apps/slack/index.js new file mode 100644 index 0000000000000000000000000000000000000000..535b4eedbd58b791e777a45a942364869f192924 --- /dev/null +++ b/packages/backend/src/apps/slack/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import actions from './actions/index.js'; +import auth from './auth/index.js'; +import dynamicData from './dynamic-data/index.js'; +import dynamicFields from './dynamic-fields/index.js'; + +export default defineApp({ + name: 'Slack', + key: 'slack', + iconUrl: '{BASE_URL}/apps/slack/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/slack/connection', + supportsConnections: true, + baseUrl: 'https://slack.com', + apiBaseUrl: 'https://slack.com/api', + primaryColor: '4a154b', + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, + dynamicFields, +}); diff --git a/packages/backend/src/apps/smtp/actions/index.js b/packages/backend/src/apps/smtp/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e1d8a996fa6d5a826969357ddf8bef389f15fa1e --- /dev/null +++ b/packages/backend/src/apps/smtp/actions/index.js @@ -0,0 +1,3 @@ +import sendEmail from './send-email/index.js'; + +export default [sendEmail]; diff --git a/packages/backend/src/apps/smtp/actions/send-email/index.js b/packages/backend/src/apps/smtp/actions/send-email/index.js new file mode 100644 index 0000000000000000000000000000000000000000..44985de26739bb0c43added7b9068307f9558205 --- /dev/null +++ b/packages/backend/src/apps/smtp/actions/send-email/index.js @@ -0,0 +1,90 @@ +import defineAction from '../../../../helpers/define-action.js'; +import transporter from '../../common/transporter.js'; + +export default defineAction({ + name: 'Send an email', + key: 'sendEmail', + description: 'Sends an email', + arguments: [ + { + label: 'From name', + key: 'fromName', + type: 'string', + required: false, + description: 'Display name of the sender.', + variables: true, + }, + { + label: 'From email', + key: 'fromEmail', + type: 'string', + required: true, + description: 'Email address of the sender.', + variables: true, + }, + { + label: 'Reply to', + key: 'replyTo', + type: 'string', + required: false, + description: + 'Email address to reply to. Defaults to the from email address.', + variables: true, + }, + { + label: 'To', + key: 'to', + type: 'string', + required: true, + description: + 'Comma seperated list of email addresses to send the email to.', + variables: true, + }, + { + label: 'Cc', + key: 'cc', + type: 'string', + required: false, + description: 'Comma seperated list of email addresses.', + variables: true, + }, + { + label: 'Bcc', + key: 'bcc', + type: 'string', + required: false, + description: 'Comma seperated list of email addresses.', + variables: true, + }, + { + label: 'Subject', + key: 'subject', + type: 'string', + required: true, + description: 'Subject of the email.', + variables: true, + }, + { + label: 'Body', + key: 'body', + type: 'string', + required: true, + description: 'Body of the email.', + variables: true, + }, + ], + + async run($) { + const info = await transporter($).sendMail({ + from: `${$.step.parameters.fromName} <${$.step.parameters.fromEmail}>`, + to: $.step.parameters.to.split(','), + replyTo: $.step.parameters.replyTo, + cc: $.step.parameters.cc.split(','), + bcc: $.step.parameters.bcc.split(','), + subject: $.step.parameters.subject, + text: $.step.parameters.body, + }); + + $.setActionItem({ raw: info }); + }, +}); diff --git a/packages/backend/src/apps/smtp/assets/favicon.svg b/packages/backend/src/apps/smtp/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..57f0fa58d4bb8c40cc87188680c24c175f1d5bc1 --- /dev/null +++ b/packages/backend/src/apps/smtp/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/backend/src/apps/smtp/auth/index.js b/packages/backend/src/apps/smtp/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..090e691f40389d6c2f8b2ac2c78fced11d9fe77e --- /dev/null +++ b/packages/backend/src/apps/smtp/auth/index.js @@ -0,0 +1,91 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'host', + label: 'Host', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'The host information Automatisch will connect to.', + docUrl: 'https://automatisch.io/docs/smtp#host', + clickToCopy: false, + }, + { + key: 'username', + label: 'Email/Username', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Your SMTP login credentials.', + docUrl: 'https://automatisch.io/docs/smtp#username', + clickToCopy: false, + }, + { + key: 'password', + label: 'Password', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/smtp#password', + clickToCopy: false, + }, + { + key: 'useTls', + label: 'Use TLS?', + type: 'dropdown', + required: false, + readOnly: false, + value: false, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/smtp#use-tls', + clickToCopy: false, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + { + key: 'port', + label: 'Port', + type: 'string', + required: false, + readOnly: false, + value: '25', + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/smtp#port', + clickToCopy: false, + }, + { + key: 'fromEmail', + label: 'From Email', + type: 'string', + required: false, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/smtp#from-email', + clickToCopy: false, + }, + ], + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/smtp/auth/is-still-verified.js b/packages/backend/src/apps/smtp/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/smtp/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/smtp/auth/verify-credentials.js b/packages/backend/src/apps/smtp/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..ef4218e07b0e81a130e8cfbdd65688183da4d066 --- /dev/null +++ b/packages/backend/src/apps/smtp/auth/verify-credentials.js @@ -0,0 +1,11 @@ +import transporter from '../common/transporter.js'; + +const verifyCredentials = async ($) => { + await transporter($).verify(); + + await $.auth.set({ + screenName: $.auth.data.username, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/smtp/common/transporter.js b/packages/backend/src/apps/smtp/common/transporter.js new file mode 100644 index 0000000000000000000000000000000000000000..50718058424f32e59d41f7d85384df029b46336a --- /dev/null +++ b/packages/backend/src/apps/smtp/common/transporter.js @@ -0,0 +1,15 @@ +import nodemailer from 'nodemailer'; + +const transporter = ($) => { + return nodemailer.createTransport({ + host: $.auth.data.host, + port: $.auth.data.port, + secure: $.auth.data.useTls, + auth: { + user: $.auth.data.username, + pass: $.auth.data.password, + }, + }); +}; + +export default transporter; diff --git a/packages/backend/src/apps/smtp/index.js b/packages/backend/src/apps/smtp/index.js new file mode 100644 index 0000000000000000000000000000000000000000..77e2a896a331d6f8dd48f53353c3114093964443 --- /dev/null +++ b/packages/backend/src/apps/smtp/index.js @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'SMTP', + key: 'smtp', + iconUrl: '{BASE_URL}/apps/smtp/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/smtp/connection', + supportsConnections: true, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '2DAAE1', + auth, + actions, +}); diff --git a/packages/backend/src/apps/spotify/actions/create-playlist/index.js b/packages/backend/src/apps/spotify/actions/create-playlist/index.js new file mode 100644 index 0000000000000000000000000000000000000000..85766949c88964b666a001c80ab3769917b871da --- /dev/null +++ b/packages/backend/src/apps/spotify/actions/create-playlist/index.js @@ -0,0 +1,55 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create playlist', + key: 'createPlaylist', + description: `Create playlist on user's account.`, + arguments: [ + { + label: 'Playlist name', + key: 'playlistName', + type: 'string', + required: true, + description: 'Playlist name', + variables: true, + }, + { + label: 'Playlist visibility', + key: 'playlistVisibility', + type: 'dropdown', + required: true, + description: 'Playlist visibility', + variables: true, + options: [ + { label: 'public', value: 'Public' }, + { label: 'private', value: 'Private' }, + ], + }, + { + label: 'Playlist description', + key: 'playlistDescription', + type: 'string', + required: false, + description: 'Playlist description', + variables: true, + }, + ], + + async run($) { + const playlistName = $.step.parameters.playlistName; + const playlistDescription = $.step.parameters.playlistDescription; + const playlistVisibility = + $.step.parameters.playlistVisibility === 'public' ? true : false; + + const response = await $.http.post( + `v1/users/${$.auth.data.userId}/playlists`, + { + name: playlistName, + public: playlistVisibility, + description: playlistDescription, + } + ); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/spotify/actions/index.js b/packages/backend/src/apps/spotify/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a520f0d751a2bf739d7672d6c9246fec492d0d91 --- /dev/null +++ b/packages/backend/src/apps/spotify/actions/index.js @@ -0,0 +1,3 @@ +import cratePlaylist from './create-playlist/index.js'; + +export default [cratePlaylist]; diff --git a/packages/backend/src/apps/spotify/assets/favicon.svg b/packages/backend/src/apps/spotify/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..f84a03c6d453a154a1ab681ea647dab44e824f5d --- /dev/null +++ b/packages/backend/src/apps/spotify/assets/favicon.svg @@ -0,0 +1,6 @@ + + Spotify + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/spotify/auth/generate-auth-url.js b/packages/backend/src/apps/spotify/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..ed70fc018e1060f29572ee3ed24ba4d79b577c5b --- /dev/null +++ b/packages/backend/src/apps/spotify/auth/generate-auth-url.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'url'; +import scopes from '../common/scopes.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const state = Math.random().toString(); + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'client_credentials', + redirect_uri: redirectUri, + response_type: 'code', + scope: scopes.join(','), + state: state, + }); + + const url = `https://accounts.spotify.com/authorize?${searchParams}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/spotify/auth/index.js b/packages/backend/src/apps/spotify/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1db348c4cbb556874822fb6a33eaaff0e12803cd --- /dev/null +++ b/packages/backend/src/apps/spotify/auth/index.js @@ -0,0 +1,47 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/spotify/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Spotify OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client Id', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + refreshToken, + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/spotify/auth/is-still-verified.js b/packages/backend/src/apps/spotify/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..1b9d46d0ab1de198651ea90a78eb6113059b3693 --- /dev/null +++ b/packages/backend/src/apps/spotify/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/spotify/auth/refresh-token.js b/packages/backend/src/apps/spotify/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..b3ea8c5658b0be064da0df1c0ab4bf7bbe0721e7 --- /dev/null +++ b/packages/backend/src/apps/spotify/auth/refresh-token.js @@ -0,0 +1,31 @@ +import { Buffer } from 'node:buffer'; + +const refreshToken = async ($) => { + const response = await $.http.post( + 'https://accounts.spotify.com/api/token', + null, + { + headers: { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { + refresh_token: $.auth.data.refreshToken, + grant_type: 'refresh_token', + }, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + await $.auth.set({ + accessToken: response.data.access_token, + expiresIn: response.data.expires_in, + tokenType: response.data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/spotify/auth/verify-credentials.js b/packages/backend/src/apps/spotify/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..9f37fa78bc098335280bebbb14ff93958d488298 --- /dev/null +++ b/packages/backend/src/apps/spotify/auth/verify-credentials.js @@ -0,0 +1,52 @@ +import getCurrentUser from '../common/get-current-user.js'; +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const params = new URLSearchParams({ + code: $.auth.data.code, + redirect_uri: redirectUri, + grant_type: 'authorization_code', + }); + + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const response = await $.http.post( + 'https://accounts.spotify.com/api/token', + params.toString(), + { headers } + ); + + const { + access_token: accessToken, + refresh_token: refreshToken, + expires_in: expiresIn, + scope: scope, + token_type: tokenType, + } = response.data; + + await $.auth.set({ + accessToken, + refreshToken, + expiresIn, + scope, + tokenType, + }); + + const user = await getCurrentUser($); + + await $.auth.set({ + userId: user.id, + screenName: user.display_name, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/spotify/common/add-auth-header.js b/packages/backend/src/apps/spotify/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..38e690943f2fcca9530b7af9cf532b637162ccb6 --- /dev/null +++ b/packages/backend/src/apps/spotify/common/add-auth-header.js @@ -0,0 +1,13 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; + + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/spotify/common/get-current-user.js b/packages/backend/src/apps/spotify/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..170b252def9721ebc7232c0a6e8628fe5df6c8a7 --- /dev/null +++ b/packages/backend/src/apps/spotify/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/v1/me'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/spotify/common/scopes.js b/packages/backend/src/apps/spotify/common/scopes.js new file mode 100644 index 0000000000000000000000000000000000000000..66360c401db8619d1218e0196c39ad08f85c0580 --- /dev/null +++ b/packages/backend/src/apps/spotify/common/scopes.js @@ -0,0 +1,13 @@ +const scopes = [ + 'user-follow-read', + 'playlist-read-private', + 'playlist-read-collaborative', + 'user-library-read', + 'playlist-modify-public', + 'playlist-modify-private', + 'user-library-modify', + 'user-follow-modify', + 'user-follow-read', +]; + +export default scopes; diff --git a/packages/backend/src/apps/spotify/index.js b/packages/backend/src/apps/spotify/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4d72872ef742164a633eafd2642f8c3cf6063f54 --- /dev/null +++ b/packages/backend/src/apps/spotify/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import actions from './actions/index.js'; +import auth from './auth/index.js'; + +export default defineApp({ + name: 'Spotify', + key: 'spotify', + iconUrl: '{BASE_URL}/apps/spotify/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/spotify/connection', + supportsConnections: true, + baseUrl: 'https://spotify.com', + apiBaseUrl: 'https://api.spotify.com', + primaryColor: '000000', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/strava/actions/create-totals-and-stats-report/index.js b/packages/backend/src/apps/strava/actions/create-totals-and-stats-report/index.js new file mode 100644 index 0000000000000000000000000000000000000000..be3fa623cd05f25100bbd102f0b336a05d64b5ce --- /dev/null +++ b/packages/backend/src/apps/strava/actions/create-totals-and-stats-report/index.js @@ -0,0 +1,18 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create totals and stats report', + key: 'createTotalsAndStatsReport', + description: + 'Create a report with recent, year to date, and all time stats of your activities', + + async run($) { + const { data } = await $.http.get( + `/v3/athletes/${$.auth.data.athleteId}/stats` + ); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/strava/actions/index.js b/packages/backend/src/apps/strava/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7474a6144dfa5215ad601b547a892fa3efe82eca --- /dev/null +++ b/packages/backend/src/apps/strava/actions/index.js @@ -0,0 +1,3 @@ +import createTotalsAndStatsReport from './create-totals-and-stats-report/index.js'; + +export default [createTotalsAndStatsReport]; diff --git a/packages/backend/src/apps/strava/assets/favicon.svg b/packages/backend/src/apps/strava/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..ddd7c85581073318bb69e4aea40c094167318b3f --- /dev/null +++ b/packages/backend/src/apps/strava/assets/favicon.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/strava/auth/generate-auth-url.js b/packages/backend/src/apps/strava/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..e9542fc6dacbaa4adb179e1179ae4c58beca67d0 --- /dev/null +++ b/packages/backend/src/apps/strava/auth/generate-auth-url.js @@ -0,0 +1,19 @@ +import { URLSearchParams } from 'node:url'; + +export default async function createAuthData($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + approval_prompt: 'force', + response_type: 'code', + scope: 'read_all,profile:read_all,activity:read_all,activity:write', + }); + + await $.auth.set({ + url: `${$.app.baseUrl}/oauth/authorize?${searchParams}`, + }); +} diff --git a/packages/backend/src/apps/strava/auth/index.js b/packages/backend/src/apps/strava/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2488ca85f850b27afeb795873c2badb0dfb6e142 --- /dev/null +++ b/packages/backend/src/apps/strava/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/strava/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Strava OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/strava/auth/is-still-verified.js b/packages/backend/src/apps/strava/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..f59ee3b47dd017e65a838069305a42c9275c0bcf --- /dev/null +++ b/packages/backend/src/apps/strava/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/strava/auth/refresh-token.js b/packages/backend/src/apps/strava/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..9e7f2dabc220c93777667f15c129ccda4c1c5466 --- /dev/null +++ b/packages/backend/src/apps/strava/auth/refresh-token.js @@ -0,0 +1,20 @@ +const refreshToken = async ($) => { + const params = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }; + + const { data } = await $.http.post('/v3/oauth/token', null, { params }); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + expiresAt: data.expires_at, + tokenType: data.token_type, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/strava/auth/verify-credentials.js b/packages/backend/src/apps/strava/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..4e61da492b0dc5dc7547d1410e7d55977ccae687 --- /dev/null +++ b/packages/backend/src/apps/strava/auth/verify-credentials.js @@ -0,0 +1,19 @@ +const verifyCredentials = async ($) => { + const params = { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + }; + const { data } = await $.http.post('/v3/oauth/token', null, { params }); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + tokenType: data.token_type, + athleteId: data.athlete.id, + screenName: `${data.athlete.firstname} ${data.athlete.lastname}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/strava/common/add-auth-header.js b/packages/backend/src/apps/strava/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..e8347e51d62a9a5aff427cea7c7e09a899c1433c --- /dev/null +++ b/packages/backend/src/apps/strava/common/add-auth-header.js @@ -0,0 +1,11 @@ +const addAuthHeader = ($, requestConfig) => { + const { accessToken, tokenType } = $.auth.data; + + if (accessToken && tokenType) { + requestConfig.headers.Authorization = `${tokenType} ${accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/strava/common/get-current-user.js b/packages/backend/src/apps/strava/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..db93f1f6c70cb7747290e4e4e5b1fd964d60cada --- /dev/null +++ b/packages/backend/src/apps/strava/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/v3/athlete'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/strava/index.js b/packages/backend/src/apps/strava/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1eea697ae21934b63178f4ae9adbdbd21669abc3 --- /dev/null +++ b/packages/backend/src/apps/strava/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import actions from './actions/index.js'; +import auth from './auth/index.js'; + +export default defineApp({ + name: 'Strava', + key: 'strava', + iconUrl: '{BASE_URL}/apps/strava/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/connections/strava', + supportsConnections: true, + baseUrl: 'https://www.strava.com', + apiBaseUrl: 'https://www.strava.com/api', + primaryColor: 'fc4c01', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/stripe/assets/favicon.svg b/packages/backend/src/apps/stripe/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..25d00aaa5e21c13ef5a6eb71c917e01e1102ab56 --- /dev/null +++ b/packages/backend/src/apps/stripe/assets/favicon.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/stripe/auth/index.js b/packages/backend/src/apps/stripe/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e6da3efe623a23774b91667c558c39af286e092a --- /dev/null +++ b/packages/backend/src/apps/stripe/auth/index.js @@ -0,0 +1,32 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'secretKey', + label: 'Secret Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'displayName', + label: 'Account Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'The display name that identifies this stripe connection - most likely the associated account name', + clickToCopy: false, + }, + ], + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/stripe/auth/is-still-verified.js b/packages/backend/src/apps/stripe/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/stripe/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/stripe/auth/verify-credentials.js b/packages/backend/src/apps/stripe/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..fab0a866edbfedb09c79dd5b754ac5b396475735 --- /dev/null +++ b/packages/backend/src/apps/stripe/auth/verify-credentials.js @@ -0,0 +1,8 @@ +const verifyCredentials = async ($) => { + await $.http.get(`/v1/events`); + await $.auth.set({ + screenName: $.auth.data?.displayName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/stripe/common/add-auth-header.js b/packages/backend/src/apps/stripe/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..7299246f8f68e03a6e8f57d3e8bb9d52674b1e99 --- /dev/null +++ b/packages/backend/src/apps/stripe/common/add-auth-header.js @@ -0,0 +1,6 @@ +const addAuthHeader = ($, requestConfig) => { + requestConfig.headers['Authorization'] = `Bearer ${$.auth.data?.secretKey}`; + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/stripe/index.js b/packages/backend/src/apps/stripe/index.js new file mode 100644 index 0000000000000000000000000000000000000000..794706e141838180addc83cceded80b334afc5e9 --- /dev/null +++ b/packages/backend/src/apps/stripe/index.js @@ -0,0 +1,19 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Stripe', + key: 'stripe', + iconUrl: '{BASE_URL}/apps/stripe/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/stripe/connection', + supportsConnections: true, + baseUrl: 'https://stripe.com', + apiBaseUrl: 'https://api.stripe.com', + primaryColor: '635bff', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions: [], +}); diff --git a/packages/backend/src/apps/stripe/triggers/balance-transaction/get-balance-transactions.js b/packages/backend/src/apps/stripe/triggers/balance-transaction/get-balance-transactions.js new file mode 100644 index 0000000000000000000000000000000000000000..e28c261b327a3de10682c00e247909faac4bc1d0 --- /dev/null +++ b/packages/backend/src/apps/stripe/triggers/balance-transaction/get-balance-transactions.js @@ -0,0 +1,32 @@ +import { URLSearchParams } from 'url'; +import isEmpty from 'lodash/isEmpty.js'; +import omitBy from 'lodash/omitBy.js'; + +const getBalanceTransactions = async ($) => { + let response; + let lastId = undefined; + + do { + const params = { + starting_after: lastId, + ending_before: $.flow.lastInternalId, + }; + const queryParams = new URLSearchParams(omitBy(params, isEmpty)); + const requestPath = `/v1/balance_transactions${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = (await $.http.get(requestPath)).data; + for (const entry of response.data) { + $.pushTriggerItem({ + raw: entry, + meta: { + internalId: entry.id, + }, + }); + lastId = entry.id; + } + } while (response.has_more); +}; + +export default getBalanceTransactions; diff --git a/packages/backend/src/apps/stripe/triggers/balance-transaction/index.js b/packages/backend/src/apps/stripe/triggers/balance-transaction/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ecdf05206f9bac0386520deca97d5b98f0ee966c --- /dev/null +++ b/packages/backend/src/apps/stripe/triggers/balance-transaction/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getBalanceTransactions from './get-balance-transactions.js'; + +export default defineTrigger({ + name: 'New balance transactions', + key: 'newBalanceTransactions', + description: + 'Triggers when a new transaction is processed (refund, payout, adjustment, ...)', + pollInterval: 15, + async run($) { + await getBalanceTransactions($); + }, +}); diff --git a/packages/backend/src/apps/stripe/triggers/index.js b/packages/backend/src/apps/stripe/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..aa6b75529adc2f7db1fcf2525c8cb21c1902bf1d --- /dev/null +++ b/packages/backend/src/apps/stripe/triggers/index.js @@ -0,0 +1,4 @@ +import balanceTransaction from './balance-transaction/index.js'; +import payouts from './payouts/index.js'; + +export default [balanceTransaction, payouts]; diff --git a/packages/backend/src/apps/stripe/triggers/payouts/get-payouts.js b/packages/backend/src/apps/stripe/triggers/payouts/get-payouts.js new file mode 100644 index 0000000000000000000000000000000000000000..fbc6f7bb7a9ae423ca620408ad23f70f5eea0187 --- /dev/null +++ b/packages/backend/src/apps/stripe/triggers/payouts/get-payouts.js @@ -0,0 +1,32 @@ +import { URLSearchParams } from 'url'; +import isEmpty from 'lodash/isEmpty.js'; +import omitBy from 'lodash/omitBy.js'; + +const getPayouts = async ($) => { + let response; + let lastId = undefined; + + do { + const params = { + starting_after: lastId, + ending_before: $.flow.lastInternalId, + }; + const queryParams = new URLSearchParams(omitBy(params, isEmpty)); + const requestPath = `/v1/payouts${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = (await $.http.get(requestPath)).data; + for (const entry of response.data) { + $.pushTriggerItem({ + raw: entry, + meta: { + internalId: entry.id, + }, + }); + lastId = entry.id; + } + } while (response.has_more); +}; + +export default getPayouts; diff --git a/packages/backend/src/apps/stripe/triggers/payouts/index.js b/packages/backend/src/apps/stripe/triggers/payouts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9a5a0afc8cfd71dabedac9b0f3934fc69869c74d --- /dev/null +++ b/packages/backend/src/apps/stripe/triggers/payouts/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getPayouts from './get-payouts.js'; + +export default defineTrigger({ + name: 'New payouts', + key: 'newPayouts', + description: + 'Triggers when a payout (Stripe <-> Bank account) has been updated', + pollInterval: 15, + async run($) { + await getPayouts($); + }, +}); diff --git a/packages/backend/src/apps/telegram-bot/actions/index.js b/packages/backend/src/apps/telegram-bot/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..92d67c2c5c3cb44f1613841137575e141ce4f79a --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/actions/index.js @@ -0,0 +1,3 @@ +import sendMessage from './send-message/index.js'; + +export default [sendMessage]; diff --git a/packages/backend/src/apps/telegram-bot/actions/send-message/index.js b/packages/backend/src/apps/telegram-bot/actions/send-message/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f4ec95dbc51f22f124cc7db46f98139a61521f98 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/actions/send-message/index.js @@ -0,0 +1,60 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send message', + key: 'sendMessage', + description: 'Sends a message to a chat you specify.', + arguments: [ + { + label: 'Chat ID', + key: 'chatId', + type: 'string', + required: true, + description: + 'Unique identifier for the target chat or username of the target channel (in the format @channelusername).', + variables: true, + }, + { + label: 'Message text', + key: 'text', + type: 'string', + required: true, + description: 'Text of the message to be sent, 1-4096 characters.', + variables: true, + }, + { + label: 'Disable notification?', + key: 'disableNotification', + type: 'dropdown', + required: false, + value: false, + description: + 'Sends the message silently. Users will receive a notification with no sound.', + variables: true, + options: [ + { + label: 'Yes', + value: true, + }, + { + label: 'No', + value: false, + }, + ], + }, + ], + + async run($) { + const payload = { + chat_id: $.step.parameters.chatId, + text: $.step.parameters.text, + disable_notification: $.step.parameters.disableNotification, + }; + + const response = await $.http.post('/sendMessage', payload); + + $.setActionItem({ + raw: response.data, + }); + }, +}); diff --git a/packages/backend/src/apps/telegram-bot/assets/favicon.svg b/packages/backend/src/apps/telegram-bot/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..8f16fb17ea7a18e4e2613c325a3507a2c9f30c20 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/assets/favicon.svg @@ -0,0 +1,14 @@ + + + Telegram + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/telegram-bot/auth/index.js b/packages/backend/src/apps/telegram-bot/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c8a7aaa3df72d543d1d3ec426496553db88bc887 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/index.js @@ -0,0 +1,21 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'token', + label: 'Bot token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Bot token which should be retrieved from @botfather.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/telegram-bot/auth/is-still-verified.js b/packages/backend/src/apps/telegram-bot/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/telegram-bot/auth/verify-credentials.js b/packages/backend/src/apps/telegram-bot/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..594fc1a8999bfcbeb78cf1258c612aa144748c7f --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/auth/verify-credentials.js @@ -0,0 +1,10 @@ +const verifyCredentials = async ($) => { + const { data } = await $.http.get('/getMe'); + const { result: me } = data; + + await $.auth.set({ + screenName: me.first_name, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/telegram-bot/common/add-auth-header.js b/packages/backend/src/apps/telegram-bot/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..0c0228dccd011cb90db9c4fb03a3485369d67c10 --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/common/add-auth-header.js @@ -0,0 +1,15 @@ +import { URL } from 'node:url'; + +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.token) { + const token = $.auth.data.token; + requestConfig.baseURL = new URL( + `/bot${token}`, + requestConfig.baseURL + ).toString(); + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/telegram-bot/index.js b/packages/backend/src/apps/telegram-bot/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1a99bbcefb9fed56ba078ea91630cc8522e25c3f --- /dev/null +++ b/packages/backend/src/apps/telegram-bot/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'Telegram', + key: 'telegram-bot', + iconUrl: '{BASE_URL}/apps/telegram-bot/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/telegram-bot/connection', + supportsConnections: true, + baseUrl: 'https://telegram.org', + apiBaseUrl: 'https://api.telegram.org', + primaryColor: '2AABEE', + beforeRequest: [addAuthHeader], + auth, + actions, +}); diff --git a/packages/backend/src/apps/todoist/actions/create-task/index.js b/packages/backend/src/apps/todoist/actions/create-task/index.js new file mode 100644 index 0000000000000000000000000000000000000000..239e2ed1f818f208cfd67ec315bcd5d5836d44b0 --- /dev/null +++ b/packages/backend/src/apps/todoist/actions/create-task/index.js @@ -0,0 +1,93 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create task', + key: 'createTask', + description: 'Creates a Task in Todoist', + arguments: [ + { + label: 'Project ID', + key: 'projectId', + type: 'dropdown', + required: false, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, + }, + { + label: 'Section ID', + key: 'sectionId', + type: 'dropdown', + required: false, + variables: true, + dependsOn: ['parameters.projectId'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSections', + }, + { + name: 'parameters.projectId', + value: '{parameters.projectId}', + }, + ], + }, + }, + { + label: 'Labels', + key: 'labels', + type: 'string', + required: false, + variables: true, + description: + 'Labels to add to task (comma separated). Examples: "work" "work,imported"', + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: true, + variables: true, + description: 'Task content, may be markdown. Example: "Foo"', + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + variables: true, + description: 'Task description, may be markdown. Example: "Foo"', + }, + ], + + async run($) { + const requestPath = `/tasks`; + const { projectId, sectionId, labels, content, description } = + $.step.parameters; + + const labelsArray = labels.split(','); + + const payload = { + content, + description: description || null, + project_id: projectId || null, + labels: labelsArray || null, + section_id: sectionId || null, + }; + + const response = await $.http.post(requestPath, payload); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/todoist/actions/index.js b/packages/backend/src/apps/todoist/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..dc3e333a85ae06baaeccee1ed424d264e0a38aa4 --- /dev/null +++ b/packages/backend/src/apps/todoist/actions/index.js @@ -0,0 +1,3 @@ +import createTask from './create-task/index.js'; + +export default [createTask]; diff --git a/packages/backend/src/apps/todoist/assets/favicon.svg b/packages/backend/src/apps/todoist/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..679cdc6315139df8b030f040f3dbd0734facbbfe --- /dev/null +++ b/packages/backend/src/apps/todoist/assets/favicon.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/todoist/auth/generate-auth-url.js b/packages/backend/src/apps/todoist/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..6c9cb2afe134688802ae89a73b66816f6e552b2b --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/generate-auth-url.js @@ -0,0 +1,15 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const scopes = ['data:read_write']; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + scope: scopes.join(','), + }); + + const url = `${$.app.baseUrl}/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/todoist/auth/index.js b/packages/backend/src/apps/todoist/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3c9037abae50c5b7ff2100fb43ec12d86d48e867 --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/index.js @@ -0,0 +1,58 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/todoist/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Todoist OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/todoist#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Name your connection (only used for Automatisch UI).', + clickToCopy: false, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/todoist/auth/is-still-verified.js b/packages/backend/src/apps/todoist/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..367cf917f18842cc268613771f389671b969916a --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/is-still-verified.js @@ -0,0 +1,6 @@ +const isStillVerified = async ($) => { + await $.http.get('/projects'); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/todoist/auth/verify-credentials.js b/packages/backend/src/apps/todoist/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..92d0a40a06a3a8941776428505f0f1483b8afa55 --- /dev/null +++ b/packages/backend/src/apps/todoist/auth/verify-credentials.js @@ -0,0 +1,14 @@ +const verifyCredentials = async ($) => { + const { data } = await $.http.post(`${$.app.baseUrl}/oauth/access_token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + }); + + await $.auth.set({ + tokenType: data.token_type, + accessToken: data.access_token, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/todoist/common/add-auth-header.js b/packages/backend/src/apps/todoist/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..2730feea01e3e8d4d8d6b51ed9c154c63eada15b --- /dev/null +++ b/packages/backend/src/apps/todoist/common/add-auth-header.js @@ -0,0 +1,11 @@ +const addAuthHeader = ($, requestConfig) => { + const authData = $.auth.data; + if (authData?.accessToken && authData?.tokenType) { + const authorizationHeader = `${authData.tokenType} ${authData.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/todoist/dynamic-data/index.js b/packages/backend/src/apps/todoist/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..31040d194692079baf2f0e531b6ecc746d3f6c62 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/index.js @@ -0,0 +1,5 @@ +import listProjects from './list-projects/index.js'; +import listSections from './list-sections/index.js'; +import listLabels from './list-labels/index.js'; + +export default [listProjects, listSections, listLabels]; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.js b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bee03fa6b65e9757b71df2d535c432138964a1f3 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-labels/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List labels', + key: 'listLabels', + + async run($) { + const response = await $.http.get('/labels'); + + response.data = response.data.map((label) => { + return { + value: label.name, + name: label.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.js b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ad1317391355c845184e50d9cd083dcbdb3c2eb8 --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-projects/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List projects', + key: 'listProjects', + + async run($) { + const response = await $.http.get('/projects'); + + response.data = response.data.map((project) => { + return { + value: project.id, + name: project.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.js b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c4ea1d65344f63abafd781679a8ed5110988ff2a --- /dev/null +++ b/packages/backend/src/apps/todoist/dynamic-data/list-sections/index.js @@ -0,0 +1,21 @@ +export default { + name: 'List sections', + key: 'listSections', + + async run($) { + const params = { + project_id: $.step.parameters.projectId, + }; + + const response = await $.http.get('/sections', { params }); + + response.data = response.data.map((section) => { + return { + value: section.id, + name: section.name, + }; + }); + + return response; + }, +}; diff --git a/packages/backend/src/apps/todoist/index.js b/packages/backend/src/apps/todoist/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1f833dad999c2d110e92f6686e51c53318a691fe --- /dev/null +++ b/packages/backend/src/apps/todoist/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Todoist', + key: 'todoist', + iconUrl: '{BASE_URL}/apps/todoist/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/todoist/connection', + supportsConnections: true, + baseUrl: 'https://todoist.com', + apiBaseUrl: 'https://api.todoist.com/rest/v2', + primaryColor: 'e44332', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.js b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.js new file mode 100644 index 0000000000000000000000000000000000000000..b4b0b893853fec15190c7cf7b40fa3cf7f9241a0 --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/get-tasks/get-tasks.js @@ -0,0 +1,26 @@ +const getActiveTasks = async ($) => { + const params = { + project_id: $.step.parameters.projectId?.trim(), + section_id: $.step.parameters.sectionId?.trim(), + label: $.step.parameters.label?.trim(), + filter: $.step.parameters.filter?.trim(), + }; + + const response = await $.http.get('/tasks', { params }); + + // todoist api doesn't offer sorting, so we inverse sort on id here + response.data.sort((a, b) => { + return b.id - a.id; + }); + + for (const task of response.data) { + $.pushTriggerItem({ + raw: task, + meta: { + internalId: task.id, + }, + }); + } +}; + +export default getActiveTasks; diff --git a/packages/backend/src/apps/todoist/triggers/get-tasks/index.js b/packages/backend/src/apps/todoist/triggers/get-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..74dedd1389abfe7145ea8de90f73f37d6c8f467e --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/get-tasks/index.js @@ -0,0 +1,80 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getActiveTasks from './get-tasks.js'; + +export default defineTrigger({ + name: 'Get active tasks', + key: 'getActiveTasks', + pollInterval: 15, + description: 'Triggers when new Task(s) are found', + arguments: [ + { + label: 'Project ID', + key: 'projectId', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, + }, + { + label: 'Section ID', + key: 'sectionId', + type: 'dropdown', + required: false, + variables: false, + dependsOn: ['parameters.projectId'], + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSections', + }, + { + name: 'parameters.projectId', + value: '{parameters.projectId}', + }, + ], + }, + }, + { + label: 'Label', + key: 'label', + type: 'dropdown', + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLabels', + }, + ], + }, + }, + { + label: 'Filter', + key: 'filter', + type: 'string', + required: false, + variables: false, + description: + 'Limit queried tasks to this filter. Example: "Meeting & today"', + }, + ], + + async run($) { + await getActiveTasks($); + }, +}); diff --git a/packages/backend/src/apps/todoist/triggers/index.js b/packages/backend/src/apps/todoist/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..deac848d85ba41d59e05e1037920196f473881ea --- /dev/null +++ b/packages/backend/src/apps/todoist/triggers/index.js @@ -0,0 +1,3 @@ +import getTasks from './get-tasks/index.js'; + +export default [getTasks]; diff --git a/packages/backend/src/apps/trello/actions/create-card/index.js b/packages/backend/src/apps/trello/actions/create-card/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a407bd7f50fc910345f91ef0eb2a2aa8809ce939 --- /dev/null +++ b/packages/backend/src/apps/trello/actions/create-card/index.js @@ -0,0 +1,186 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create card', + key: 'createCard', + description: 'Creates a new card within a specified board and list.', + arguments: [ + { + label: 'Board', + key: 'boardId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBoards', + }, + ], + }, + }, + { + label: 'List', + key: 'listId', + type: 'dropdown', + required: true, + dependsOn: ['parameters.boardId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBoardLists', + }, + { + name: 'parameters.boardId', + value: '{parameters.boardId}', + }, + ], + }, + }, + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + variables: true, + description: '', + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + variables: true, + description: '', + }, + + { + label: 'Label', + key: 'label', + type: 'dropdown', + required: false, + dependsOn: ['parameters.boardId'], + description: 'Select a color tag to attach to the card.', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBoardLabels', + }, + { + name: 'parameters.boardId', + value: '{parameters.boardId}', + }, + ], + }, + }, + { + label: 'Card Position', + key: 'cardPosition', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { + label: 'top', + value: 'top', + }, + { + label: 'bottom', + value: 'bottom', + }, + ], + }, + { + label: 'Members', + key: 'memberIds', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Member', + key: 'memberId', + type: 'dropdown', + required: false, + dependsOn: ['parameters.boardId'], + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listMembers', + }, + { + name: 'parameters.boardId', + value: '{parameters.boardId}', + }, + ], + }, + }, + ], + }, + { + label: 'Due Date', + key: 'dueDate', + type: 'string', + required: false, + variables: true, + description: 'Format: mm-dd-yyyy HH:mm:ss or yyyy-MM-dd HH:mm:ss.', + }, + { + label: 'URL Attachment', + key: 'urlSource', + type: 'string', + required: false, + variables: true, + description: 'A URL to attach to the card.', + }, + ], + + async run($) { + const { + listId, + name, + description, + cardPosition, + dueDate, + label, + urlSource, + } = $.step.parameters; + + const memberIds = $.step.parameters.memberIds; + const idMembers = memberIds.map((memberId) => memberId.memberId); + + const fields = { + name, + desc: description, + idList: listId, + pos: cardPosition, + due: dueDate, + idMembers: idMembers.join(','), + idLabels: label, + urlSource, + }; + + const response = await $.http.post('/1/cards', fields); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/trello/actions/index.js b/packages/backend/src/apps/trello/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a44a1bf9486b99930030d7225b32d215cf1a4a18 --- /dev/null +++ b/packages/backend/src/apps/trello/actions/index.js @@ -0,0 +1,3 @@ +import createCard from './create-card/index.js'; + +export default [createCard]; diff --git a/packages/backend/src/apps/trello/assets/favicon.svg b/packages/backend/src/apps/trello/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..7c63adb974aa2dfc498b23ac0f413e5c54794909 --- /dev/null +++ b/packages/backend/src/apps/trello/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/trello/auth/generate-auth-url.js b/packages/backend/src/apps/trello/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..80bd2ea251ab0193cee54d2c842cfc5c6e3177d0 --- /dev/null +++ b/packages/backend/src/apps/trello/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + return_url: redirectUri, + scope: authScope.join(','), + expiration: 'never', + key: $.auth.data.apiKey, + response_type: 'token', + }); + + const url = `https://trello.com/1/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/trello/auth/index.js b/packages/backend/src/apps/trello/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cf7b881a3c641861e2b62642c5fb706ef0d41013 --- /dev/null +++ b/packages/backend/src/apps/trello/auth/index.js @@ -0,0 +1,34 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/trello/connections/add', + placeholder: null, + description: '', + clickToCopy: true, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API Key for your Trello account', + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/trello/auth/is-still-verified.js b/packages/backend/src/apps/trello/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/trello/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/trello/auth/verify-credentials.js b/packages/backend/src/apps/trello/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..ebf87d9bb830ea84750e3deec6b1f2f4f0df5996 --- /dev/null +++ b/packages/backend/src/apps/trello/auth/verify-credentials.js @@ -0,0 +1,14 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const currentUser = await getCurrentUser($); + const screenName = [currentUser.username, currentUser.email] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/trello/common/add-auth-header.js b/packages/backend/src/apps/trello/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..c82490b93a38111b29ff82abc0d8dd0ef3e46c8a --- /dev/null +++ b/packages/backend/src/apps/trello/common/add-auth-header.js @@ -0,0 +1,11 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.token) { + requestConfig.headers.Authorization = `OAuth oauth_consumer_key="${$.auth.data.apiKey}", oauth_token="${$.auth.data.token}"`; + } + + requestConfig.headers.Accept = 'application/json'; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/trello/common/auth-scope.js b/packages/backend/src/apps/trello/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..805e5b7f8c0b9c916112b32919fde83fc1c2aeec --- /dev/null +++ b/packages/backend/src/apps/trello/common/auth-scope.js @@ -0,0 +1,3 @@ +const authScope = ['read', 'write', 'account']; + +export default authScope; diff --git a/packages/backend/src/apps/trello/common/get-current-user.js b/packages/backend/src/apps/trello/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..a2657cbfddbb27292f891335dbec28c05d63411f --- /dev/null +++ b/packages/backend/src/apps/trello/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/1/members/me/'); + const currentUser = response.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/trello/dynamic-data/index.js b/packages/backend/src/apps/trello/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cc30ab9eab4560b00b4727faa9286a0b3c196aa --- /dev/null +++ b/packages/backend/src/apps/trello/dynamic-data/index.js @@ -0,0 +1,6 @@ +import listBoardLabels from './list-board-labels/index.js'; +import listBoardLists from './list-board-lists/index.js'; +import listBoards from './list-boards/index.js'; +import listMembers from './listMembers/index.js'; + +export default [listBoardLabels, listBoardLists, listBoards, listMembers]; diff --git a/packages/backend/src/apps/trello/dynamic-data/list-board-labels/index.js b/packages/backend/src/apps/trello/dynamic-data/list-board-labels/index.js new file mode 100644 index 0000000000000000000000000000000000000000..981d62d9f1b2d529c9de7ebf4f43580ed967cc8a --- /dev/null +++ b/packages/backend/src/apps/trello/dynamic-data/list-board-labels/index.js @@ -0,0 +1,35 @@ +export default { + name: 'List board labels', + key: 'listBoardLabels', + + async run($) { + const boardLabels = { + data: [], + }; + + const boardId = $.step.parameters.boardId; + + if (!boardId) { + return boardLabels; + } + + const params = { + fields: 'color', + }; + + const { data } = await $.http.get(`/1/boards/${boardId}/labels`, { + params, + }); + + if (data?.length) { + for (const boardLabel of data) { + boardLabels.data.push({ + value: boardLabel.id, + name: boardLabel.color, + }); + } + } + + return boardLabels; + }, +}; diff --git a/packages/backend/src/apps/trello/dynamic-data/list-board-lists/index.js b/packages/backend/src/apps/trello/dynamic-data/list-board-lists/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ea46115f118374f3395f79605797e2610b2329e8 --- /dev/null +++ b/packages/backend/src/apps/trello/dynamic-data/list-board-lists/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List board lists', + key: 'listBoardLists', + + async run($) { + const boards = { + data: [], + }; + + const boardId = $.step.parameters.boardId; + + if (!boardId) { + return boards; + } + + const { data } = await $.http.get(`/1/boards/${boardId}/lists`); + + if (data?.length) { + for (const list of data) { + boards.data.push({ + value: list.id, + name: list.name, + }); + } + } + + return boards; + }, +}; diff --git a/packages/backend/src/apps/trello/dynamic-data/list-boards/index.js b/packages/backend/src/apps/trello/dynamic-data/list-boards/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2cf198ae23b5b42f731e9cba158e2537e0c87e83 --- /dev/null +++ b/packages/backend/src/apps/trello/dynamic-data/list-boards/index.js @@ -0,0 +1,23 @@ +export default { + name: 'List boards', + key: 'listBoards', + + async run($) { + const boards = { + data: [], + }; + + const { data } = await $.http.get(`/1/members/me/boards`); + + if (data?.length) { + for (const board of data) { + boards.data.push({ + value: board.id, + name: board.name, + }); + } + } + + return boards; + }, +}; diff --git a/packages/backend/src/apps/trello/dynamic-data/listMembers/index.js b/packages/backend/src/apps/trello/dynamic-data/listMembers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4f0070203681ea713c4b55a40bc81cd3bdc9b502 --- /dev/null +++ b/packages/backend/src/apps/trello/dynamic-data/listMembers/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List members', + key: 'listMembers', + + async run($) { + const members = { + data: [], + }; + + const boardId = $.step.parameters.boardId; + + if (!boardId) { + return members; + } + + const { data } = await $.http.get(`/1/boards/${boardId}/members`); + + if (data?.length) { + for (const member of data) { + members.data.push({ + value: member.id, + name: member.fullName, + }); + } + } + + return members; + }, +}; diff --git a/packages/backend/src/apps/trello/index.js b/packages/backend/src/apps/trello/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ed557b2b7dbce271d286b5b15d5d9f13b046ffbf --- /dev/null +++ b/packages/backend/src/apps/trello/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Trello', + key: 'trello', + baseUrl: 'https://trello.com/', + apiBaseUrl: 'https://api.trello.com', + iconUrl: '{BASE_URL}/apps/trello/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/trello/connection', + supportsConnections: true, + primaryColor: '0079bf', + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/twilio/actions/index.js b/packages/backend/src/apps/twilio/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..18a261f9d10ebb238d25ebbbb8e946ee4e78d4d8 --- /dev/null +++ b/packages/backend/src/apps/twilio/actions/index.js @@ -0,0 +1,3 @@ +import sendSms from './send-sms/index.js'; + +export default [sendSms]; diff --git a/packages/backend/src/apps/twilio/actions/send-sms/index.js b/packages/backend/src/apps/twilio/actions/send-sms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0e0c394eb4a24b81fd8211b7f9b35b24da77bcec --- /dev/null +++ b/packages/backend/src/apps/twilio/actions/send-sms/index.js @@ -0,0 +1,64 @@ +import { URLSearchParams } from 'node:url'; +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Send an SMS', + key: 'sendSms', + description: 'Sends an SMS', + arguments: [ + { + label: 'From Number', + key: 'fromNumber', + type: 'dropdown', + required: true, + description: + 'The number to send the SMS from. Include country code. Example: 15551234567', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listIncomingPhoneNumbers', + }, + ], + }, + }, + { + label: 'To Number', + key: 'toNumber', + type: 'string', + required: true, + description: + 'The number to send the SMS to. Include country code. Example: 15551234567', + variables: true, + }, + { + label: 'Message', + key: 'message', + type: 'string', + required: true, + description: 'The message to send.', + variables: true, + }, + ], + + async run($) { + const requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json`; + const messageBody = $.step.parameters.message; + + const fromNumber = $.step.parameters.fromNumber.trim(); + const toNumber = $.step.parameters.toNumber.trim(); + + const payload = new URLSearchParams({ + Body: messageBody, + From: fromNumber, + To: toNumber, + }).toString(); + + const response = await $.http.post(requestPath, payload); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/twilio/assets/favicon.svg b/packages/backend/src/apps/twilio/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..7c20e190d990a553757072507ad0681330d59e54 --- /dev/null +++ b/packages/backend/src/apps/twilio/assets/favicon.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/backend/src/apps/twilio/auth/index.js b/packages/backend/src/apps/twilio/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d71192da0a17a9daeb5cf04974130471b65013da --- /dev/null +++ b/packages/backend/src/apps/twilio/auth/index.js @@ -0,0 +1,33 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'accountSid', + label: 'Account SID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Log into your Twilio account and find "API Credentials" on this page https://www.twilio.com/user/account/settings', + clickToCopy: false, + }, + { + key: 'authToken', + label: 'Auth Token', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Found directly below your Account SID.', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/twilio/auth/is-still-verified.js b/packages/backend/src/apps/twilio/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/twilio/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/twilio/auth/verify-credentials.js b/packages/backend/src/apps/twilio/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..89f3692050d8e679b043916abc892da9e725c763 --- /dev/null +++ b/packages/backend/src/apps/twilio/auth/verify-credentials.js @@ -0,0 +1,9 @@ +const verifyCredentials = async ($) => { + await $.http.get('/2010-04-01/Accounts.json?PageSize=1'); + + await $.auth.set({ + screenName: $.auth.data.accountSid, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/twilio/common/add-auth-header.js b/packages/backend/src/apps/twilio/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..097edf0ae432b1ff398532b66032d8e12a4bdd54 --- /dev/null +++ b/packages/backend/src/apps/twilio/common/add-auth-header.js @@ -0,0 +1,18 @@ +const addAuthHeader = ($, requestConfig) => { + if ( + requestConfig.headers && + $.auth.data?.accountSid && + $.auth.data?.authToken + ) { + requestConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + + requestConfig.auth = { + username: $.auth.data.accountSid, + password: $.auth.data.authToken, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/twilio/common/get-incoming-phone-number.js b/packages/backend/src/apps/twilio/common/get-incoming-phone-number.js new file mode 100644 index 0000000000000000000000000000000000000000..cb2cdff0bb6c8f0a4c82a74c08f2eb976568cd99 --- /dev/null +++ b/packages/backend/src/apps/twilio/common/get-incoming-phone-number.js @@ -0,0 +1,7 @@ +export default async function getIncomingPhoneNumber($) { + const phoneNumberSid = $.step.parameters.phoneNumberSid; + const path = `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`; + const response = await $.http.get(path); + + return response.data; +} diff --git a/packages/backend/src/apps/twilio/dynamic-data/index.js b/packages/backend/src/apps/twilio/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..758d4abea90f5cdbace5382a61d7c72c275b33ab --- /dev/null +++ b/packages/backend/src/apps/twilio/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listIncomingPhoneNumbers from './list-incoming-phone-numbers/index.js'; + +export default [listIncomingPhoneNumbers]; diff --git a/packages/backend/src/apps/twilio/dynamic-data/list-incoming-phone-numbers/index.js b/packages/backend/src/apps/twilio/dynamic-data/list-incoming-phone-numbers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f71806b06005fe140f2f12c28190bd2332533451 --- /dev/null +++ b/packages/backend/src/apps/twilio/dynamic-data/list-incoming-phone-numbers/index.js @@ -0,0 +1,35 @@ +export default { + name: 'List incoming phone numbers', + key: 'listIncomingPhoneNumbers', + + async run($) { + const valueType = $.step.parameters.valueType; + const isSid = valueType === 'sid'; + + const aggregatedResponse = { data: [] }; + let pathname = `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers.json`; + + do { + const response = await $.http.get(pathname); + + for (const incomingPhoneNumber of response.data.incoming_phone_numbers) { + if (incomingPhoneNumber.capabilities.sms === false) { + continue; + } + + const friendlyName = incomingPhoneNumber.friendly_name; + const phoneNumber = incomingPhoneNumber.phone_number; + const name = [friendlyName, phoneNumber].filter(Boolean).join(' - '); + + aggregatedResponse.data.push({ + value: isSid ? incomingPhoneNumber.sid : phoneNumber, + name, + }); + } + + pathname = response.data.next_page_uri; + } while (pathname); + + return aggregatedResponse; + }, +}; diff --git a/packages/backend/src/apps/twilio/index.js b/packages/backend/src/apps/twilio/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ab208eb8efa5f36b1cb0101f12806b296976ce83 --- /dev/null +++ b/packages/backend/src/apps/twilio/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Twilio', + key: 'twilio', + iconUrl: '{BASE_URL}/apps/twilio/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/twilio/connection', + supportsConnections: true, + baseUrl: 'https://twilio.com', + apiBaseUrl: 'https://api.twilio.com', + primaryColor: 'e1000f', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/twilio/triggers/index.js b/packages/backend/src/apps/twilio/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c7219e503d403b4eddf8db665df9b9895566b262 --- /dev/null +++ b/packages/backend/src/apps/twilio/triggers/index.js @@ -0,0 +1,3 @@ +import receiveSms from './receive-sms/index.js'; + +export default [receiveSms]; diff --git a/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.js b/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.js new file mode 100644 index 0000000000000000000000000000000000000000..7445cedd96a078e4f00ea7b6e02e2ce4e326e807 --- /dev/null +++ b/packages/backend/src/apps/twilio/triggers/receive-sms/fetch-messages.js @@ -0,0 +1,39 @@ +import getIncomingPhoneNumber from '../../common/get-incoming-phone-number.js'; + +const fetchMessages = async ($) => { + const incomingPhoneNumber = await getIncomingPhoneNumber($); + + let response; + let requestPath = `/2010-04-01/Accounts/${$.auth.data.accountSid}/Messages.json?To=${incomingPhoneNumber.phone_number}`; + + do { + response = await $.http.get(requestPath); + + response.data.messages.forEach((message) => { + const computedMessage = { + To: message.to, + Body: message.body, + From: message.from, + SmsSid: message.sid, + NumMedia: message.num_media, + SmsStatus: message.status, + AccountSid: message.account_sid, + ApiVersion: message.api_version, + NumSegments: message.num_segments, + }; + + const dataItem = { + raw: computedMessage, + meta: { + internalId: message.date_sent, + }, + }; + + $.pushTriggerItem(dataItem); + }); + + requestPath = response.data.next_page_uri; + } while (requestPath); +}; + +export default fetchMessages; diff --git a/packages/backend/src/apps/twilio/triggers/receive-sms/index.js b/packages/backend/src/apps/twilio/triggers/receive-sms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d5224ca1a136596d28b2ef40cf36829febeff3d7 --- /dev/null +++ b/packages/backend/src/apps/twilio/triggers/receive-sms/index.js @@ -0,0 +1,90 @@ +import { URLSearchParams } from 'node:url'; +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; +import fetchMessages from './fetch-messages.js'; + +export default defineTrigger({ + name: 'Receive SMS', + key: 'receiveSms', + type: 'webhook', + description: 'Triggers when a new SMS is received.', + arguments: [ + { + label: 'To Number', + key: 'phoneNumberSid', + type: 'dropdown', + required: true, + description: + 'The number to receive the SMS on. It should be a Twilio number.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listIncomingPhoneNumbers', + }, + { + name: 'parameters.valueType', + value: 'sid', + }, + ], + }, + }, + ], + + useSingletonWebhook: true, + singletonWebhookRefValueParameter: 'phoneNumberSid', + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + await fetchMessages($); + + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, + + async registerHook($) { + const phoneNumberSid = $.step.parameters.phoneNumberSid; + const payload = new URLSearchParams({ + SmsUrl: $.webhookUrl, + }).toString(); + + await $.http.post( + `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`, + payload + ); + }, + + async unregisterHook($) { + const phoneNumberSid = $.step.parameters.phoneNumberSid; + const payload = new URLSearchParams({ + SmsUrl: '', + }).toString(); + + await $.http.post( + `/2010-04-01/Accounts/${$.auth.data.accountSid}/IncomingPhoneNumbers/${phoneNumberSid}.json`, + payload + ); + }, +}); diff --git a/packages/backend/src/apps/twitter/actions/create-tweet/index.js b/packages/backend/src/apps/twitter/actions/create-tweet/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f3f6d14f37ec2814d7103af3becd993452046433 --- /dev/null +++ b/packages/backend/src/apps/twitter/actions/create-tweet/index.js @@ -0,0 +1,26 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Create tweet', + key: 'createTweet', + description: 'Create a tweet.', + arguments: [ + { + label: 'Tweet body', + key: 'tweet', + type: 'string', + required: true, + description: 'The content of your new tweet.', + variables: true, + }, + ], + + async run($) { + const text = $.step.parameters.tweet; + const response = await $.http.post('/2/tweets', { + text, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/twitter/actions/index.js b/packages/backend/src/apps/twitter/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b090ea0485b00b9e8925850855ce7a1e1bf12a65 --- /dev/null +++ b/packages/backend/src/apps/twitter/actions/index.js @@ -0,0 +1,4 @@ +import createTweet from './create-tweet/index.js'; +import searchUser from './search-user/index.js'; + +export default [createTweet, searchUser]; diff --git a/packages/backend/src/apps/twitter/actions/search-user/index.js b/packages/backend/src/apps/twitter/actions/search-user/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a535e0e3f64eab279fd5e012a3c09f2a7cc4de54 --- /dev/null +++ b/packages/backend/src/apps/twitter/actions/search-user/index.js @@ -0,0 +1,35 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Search user', + key: 'searchUser', + description: 'Search a user on Twitter', + arguments: [ + { + label: 'Username', + key: 'username', + type: 'string', + required: true, + description: 'The username of the Twitter user you want to search for', + variables: true, + }, + ], + + async run($) { + const { data } = await $.http.get( + `/2/users/by/username/${$.step.parameters.username}`, + { + params: { + expansions: 'pinned_tweet_id', + 'tweet.fields': + 'attachments,author_id,context_annotations,conversation_id,created_at,edit_controls,entities,geo,id,in_reply_to_user_id,lang,non_public_metrics,public_metrics,organic_metrics,promoted_metrics,possibly_sensitive,referenced_tweets,reply_settings,source,text,withheld', + 'user.fields': + 'created_at,description,entities,id,location,name,pinned_tweet_id,profile_image_url,protected,public_metrics,url,username,verified,verified_type,withheld', + }, + } + ); + $.setActionItem({ + raw: data.data, + }); + }, +}); diff --git a/packages/backend/src/apps/twitter/assets/favicon.svg b/packages/backend/src/apps/twitter/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..576611f2e9cd330cccdde69564264f72ab657b95 --- /dev/null +++ b/packages/backend/src/apps/twitter/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/twitter/auth/generate-auth-url.js b/packages/backend/src/apps/twitter/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..f1e0db8109a3b752327e2b176047682e973747a2 --- /dev/null +++ b/packages/backend/src/apps/twitter/auth/generate-auth-url.js @@ -0,0 +1,20 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const callbackUrl = oauthRedirectUrlField.value; + const requestPath = '/oauth/request_token'; + const data = { oauth_callback: callbackUrl }; + + const response = await $.http.post(requestPath, data); + const responseData = Object.fromEntries(new URLSearchParams(response.data)); + + await $.auth.set({ + url: `${$.app.baseUrl}/oauth/authorize?oauth_token=${responseData.oauth_token}`, + accessToken: responseData.oauth_token, + accessSecret: responseData.oauth_token_secret, + }); +} diff --git a/packages/backend/src/apps/twitter/auth/index.js b/packages/backend/src/apps/twitter/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..78135f81afacf9fb5b710d4f7643825a98926343 --- /dev/null +++ b/packages/backend/src/apps/twitter/auth/index.js @@ -0,0 +1,46 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/twitter/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Twitter OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'consumerKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'consumerSecret', + label: 'API Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/twitter/auth/is-still-verified.js b/packages/backend/src/apps/twitter/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..f59ee3b47dd017e65a838069305a42c9275c0bcf --- /dev/null +++ b/packages/backend/src/apps/twitter/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const user = await getCurrentUser($); + return !!user; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/twitter/auth/verify-credentials.js b/packages/backend/src/apps/twitter/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..262088342ba5f8a3064e6fafb4532056aa03aa57 --- /dev/null +++ b/packages/backend/src/apps/twitter/auth/verify-credentials.js @@ -0,0 +1,19 @@ +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const response = await $.http.post( + `/oauth/access_token?oauth_verifier=${$.auth.data.oauth_verifier}&oauth_token=${$.auth.data.accessToken}`, + null + ); + + const responseData = Object.fromEntries(new URLSearchParams(response.data)); + + await $.auth.set({ + accessToken: responseData.oauth_token, + accessSecret: responseData.oauth_token_secret, + userId: responseData.user_id, + screenName: responseData.screen_name, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/twitter/common/add-auth-header.js b/packages/backend/src/apps/twitter/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..824181cea648d37b4c7d5e8d03dc72051f7b7af6 --- /dev/null +++ b/packages/backend/src/apps/twitter/common/add-auth-header.js @@ -0,0 +1,39 @@ +import { URLSearchParams } from 'node:url'; +import oauthClient from './oauth-client.js'; + +const addAuthHeader = ($, requestConfig) => { + const { baseURL, url, method, data, params } = requestConfig; + + const token = { + key: $.auth.data?.accessToken, + secret: $.auth.data?.accessSecret, + }; + + const searchParams = new URLSearchParams(params); + const stringifiedParams = searchParams.toString(); + let fullUrl = `${baseURL}${url}`; + + // append the search params + if (stringifiedParams) { + fullUrl = `${fullUrl}?${stringifiedParams}`; + } + + const requestData = { + url: fullUrl, + method, + }; + + if (url === '/oauth/request_token') { + requestData.data = data; + } + + const authHeader = oauthClient($).toHeader( + oauthClient($).authorize(requestData, token) + ); + + requestConfig.headers.Authorization = authHeader.Authorization; + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/twitter/common/get-current-user.js b/packages/backend/src/apps/twitter/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..5dd99b4d0854849ff52f7dd29e09e39ee926f0a0 --- /dev/null +++ b/packages/backend/src/apps/twitter/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/2/users/me'); + const currentUser = response.data.data; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/twitter/common/get-user-by-username.js b/packages/backend/src/apps/twitter/common/get-user-by-username.js new file mode 100644 index 0000000000000000000000000000000000000000..0e4b9654a54bbd4d9810875bffcdb8526ae8f01f --- /dev/null +++ b/packages/backend/src/apps/twitter/common/get-user-by-username.js @@ -0,0 +1,16 @@ +const getUserByUsername = async ($, username) => { + const response = await $.http.get(`/2/users/by/username/${username}`); + + if (response.data.errors) { + const errorMessages = response.data.errors + .map((error) => error.detail) + .join(' '); + + throw new Error(`Error occured while fetching user data: ${errorMessages}`); + } + + const user = response.data.data; + return user; +}; + +export default getUserByUsername; diff --git a/packages/backend/src/apps/twitter/common/get-user-followers.js b/packages/backend/src/apps/twitter/common/get-user-followers.js new file mode 100644 index 0000000000000000000000000000000000000000..9b339f9713ea26ab9d9fd92301389f85446430e4 --- /dev/null +++ b/packages/backend/src/apps/twitter/common/get-user-followers.js @@ -0,0 +1,36 @@ +import { URLSearchParams } from 'url'; +import omitBy from 'lodash/omitBy.js'; +import isEmpty from 'lodash/isEmpty.js'; + +const getUserFollowers = async ($, options) => { + let response; + + do { + const params = { + pagination_token: response?.data?.meta?.next_token, + }; + + const queryParams = new URLSearchParams(omitBy(params, isEmpty)); + + const requestPath = `/2/users/${options.userId}/followers${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = await $.http.get(requestPath); + + if (response.data?.errors) { + throw new Error(response.data.errors); + } + + if (response.data.meta.result_count > 0) { + for (const follower of response.data.data) { + $.pushTriggerItem({ + raw: follower, + meta: { internalId: follower.id }, + }); + } + } + } while (response.data.meta.next_token); +}; + +export default getUserFollowers; diff --git a/packages/backend/src/apps/twitter/common/get-user-tweets.js b/packages/backend/src/apps/twitter/common/get-user-tweets.js new file mode 100644 index 0000000000000000000000000000000000000000..c63727aafcd6458d020be0b18d8214a235bdb29e --- /dev/null +++ b/packages/backend/src/apps/twitter/common/get-user-tweets.js @@ -0,0 +1,56 @@ +import { URLSearchParams } from 'url'; +import omitBy from 'lodash/omitBy.js'; +import isEmpty from 'lodash/isEmpty.js'; +import getCurrentUser from './get-current-user.js'; +import getUserByUsername from './get-user-by-username.js'; + +const fetchTweets = async ($, username) => { + const user = await getUserByUsername($, username); + + let response; + + do { + const params = { + since_id: $.flow.lastInternalId, + pagination_token: response?.data?.meta?.next_token, + }; + + const queryParams = new URLSearchParams(omitBy(params, isEmpty)); + + const requestPath = `/2/users/${user.id}/tweets${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = await $.http.get(requestPath); + + if (response.data.meta.result_count > 0) { + response.data.data.forEach((tweet) => { + const dataItem = { + raw: tweet, + meta: { + internalId: tweet.id, + }, + }; + + $.pushTriggerItem(dataItem); + }); + } + } while (response.data.meta.next_token); + + return $.triggerOutput; +}; + +const getUserTweets = async ($, options) => { + let username; + + if (options.currentUser) { + const currentUser = await getCurrentUser($); + username = currentUser.username; + } else { + username = $.step.parameters.username; + } + + await fetchTweets($, username); +}; + +export default getUserTweets; diff --git a/packages/backend/src/apps/twitter/common/oauth-client.js b/packages/backend/src/apps/twitter/common/oauth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..d89c4889d0a1f199b3ce9f88e35228b904b349f6 --- /dev/null +++ b/packages/backend/src/apps/twitter/common/oauth-client.js @@ -0,0 +1,22 @@ +import crypto from 'crypto'; +import OAuth from 'oauth-1.0a'; + +const oauthClient = ($) => { + const consumerData = { + key: $.auth.data.consumerKey, + secret: $.auth.data.consumerSecret, + }; + + return new OAuth({ + consumer: consumerData, + signature_method: 'HMAC-SHA1', + hash_function(base_string, key) { + return crypto + .createHmac('sha1', key) + .update(base_string) + .digest('base64'); + }, + }); +}; + +export default oauthClient; diff --git a/packages/backend/src/apps/twitter/index.js b/packages/backend/src/apps/twitter/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2e1289fa9351867190921e4fdf4f24ae77542815 --- /dev/null +++ b/packages/backend/src/apps/twitter/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Twitter', + key: 'twitter', + iconUrl: '{BASE_URL}/apps/twitter/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/twitter/connection', + supportsConnections: true, + baseUrl: 'https://twitter.com', + apiBaseUrl: 'https://api.twitter.com', + primaryColor: '1da1f2', + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, +}); diff --git a/packages/backend/src/apps/twitter/triggers/index.js b/packages/backend/src/apps/twitter/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9e36c62af55d3d0467169f35853ca0c0ee7eaaa6 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/index.js @@ -0,0 +1,6 @@ +import myTweets from './my-tweets/index.js'; +import newFollowerOfMe from './new-follower-of-me/index.js'; +import searchTweets from './search-tweets/index.js'; +import userTweets from './user-tweets/index.js'; + +export default [myTweets, newFollowerOfMe, searchTweets, userTweets]; diff --git a/packages/backend/src/apps/twitter/triggers/my-tweets/index.js b/packages/backend/src/apps/twitter/triggers/my-tweets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..756e43b1d0df52127c6f3dde762e23955d118156 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/my-tweets/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getUserTweets from '../../common/get-user-tweets.js'; + +export default defineTrigger({ + name: 'My tweets', + key: 'myTweets', + pollInterval: 15, + description: 'Triggers when you tweet something new.', + + async run($) { + await getUserTweets($, { currentUser: true }); + }, +}); diff --git a/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.js b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9a3c06b893b6002a995f802c8fa41cce56e686be --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/index.js @@ -0,0 +1,13 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import myFollowers from './my-followers.js'; + +export default defineTrigger({ + name: 'New follower of me', + key: 'newFollowerOfMe', + pollInterval: 15, + description: 'Triggers when you have a new follower.', + + async run($) { + await myFollowers($); + }, +}); diff --git a/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.js b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.js new file mode 100644 index 0000000000000000000000000000000000000000..41eb952e42daad8789b511b15f912b48c6b74f79 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/new-follower-of-me/my-followers.js @@ -0,0 +1,15 @@ +import getCurrentUser from '../../common/get-current-user.js'; +import getUserByUsername from '../../common/get-user-by-username.js'; +import getUserFollowers from '../../common/get-user-followers.js'; + +const myFollowers = async ($) => { + const { username } = await getCurrentUser($); + const user = await getUserByUsername($, username); + + const tweets = await getUserFollowers($, { + userId: user.id, + }); + return tweets; +}; + +export default myFollowers; diff --git a/packages/backend/src/apps/twitter/triggers/search-tweets/index.js b/packages/backend/src/apps/twitter/triggers/search-tweets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..163729084cea941bb70e8b8675805d343164ab89 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/search-tweets/index.js @@ -0,0 +1,22 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import searchTweets from './search-tweets.js'; + +export default defineTrigger({ + name: 'Search tweets', + key: 'searchTweets', + pollInterval: 15, + description: + 'Triggers when there is a new tweet containing a specific keyword, phrase, username or hashtag.', + arguments: [ + { + label: 'Search Term', + key: 'searchTerm', + type: 'string', + required: true, + }, + ], + + async run($) { + await searchTweets($); + }, +}); diff --git a/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.js b/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.js new file mode 100644 index 0000000000000000000000000000000000000000..ddedda2e0bb754d46b33314cb694fde63d1f8188 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/search-tweets/search-tweets.js @@ -0,0 +1,44 @@ +import qs from 'qs'; +import omitBy from 'lodash/omitBy.js'; +import isEmpty from 'lodash/isEmpty.js'; + +const searchTweets = async ($) => { + const searchTerm = $.step.parameters.searchTerm; + + let response; + + do { + const params = { + query: searchTerm, + since_id: $.flow.lastInternalId, + pagination_token: response?.data?.meta?.next_token, + }; + + const queryParams = qs.stringify(omitBy(params, isEmpty)); + + const requestPath = `/2/tweets/search/recent${ + queryParams.toString() ? `?${queryParams.toString()}` : '' + }`; + + response = await $.http.get(requestPath); + + if (response.data.errors) { + throw new Error(JSON.stringify(response.data.errors)); + } + + if (response.data.meta.result_count > 0) { + response.data.data.forEach((tweet) => { + const dataItem = { + raw: tweet, + meta: { + internalId: tweet.id, + }, + }; + + $.pushTriggerItem(dataItem); + }); + } + } while (response.data.meta.next_token); +}; + +export default searchTweets; diff --git a/packages/backend/src/apps/twitter/triggers/user-tweets/index.js b/packages/backend/src/apps/twitter/triggers/user-tweets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c14c5a25a7d1a4a77cb9feeb6bff01acfa86c590 --- /dev/null +++ b/packages/backend/src/apps/twitter/triggers/user-tweets/index.js @@ -0,0 +1,21 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; +import getUserTweets from '../../common/get-user-tweets.js'; + +export default defineTrigger({ + name: 'User tweets', + key: 'userTweets', + pollInterval: 15, + description: 'Triggers when a specific user tweet something new.', + arguments: [ + { + label: 'Username', + key: 'username', + type: 'string', + required: true, + }, + ], + + async run($) { + await getUserTweets($, { currentUser: false }); + }, +}); diff --git a/packages/backend/src/apps/typeform/assets/favicon.svg b/packages/backend/src/apps/typeform/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..f0fabb1cbec52e51e852d6e6cf9e2960b0a03e1e --- /dev/null +++ b/packages/backend/src/apps/typeform/assets/favicon.svg @@ -0,0 +1,4 @@ + + Typeform + + diff --git a/packages/backend/src/apps/typeform/auth/generate-auth-url.js b/packages/backend/src/apps/typeform/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..4012e5ec968526d8232b412254458018316f5a6c --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/generate-auth-url.js @@ -0,0 +1,20 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrl = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ).value; + + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: oauthRedirectUrl, + scope: authScope.join(' '), + }); + + const url = `${$.app.apiBaseUrl}/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/typeform/auth/index.js b/packages/backend/src/apps/typeform/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..81e6906d58cfcd051bc92677104a80b983155520 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/index.js @@ -0,0 +1,50 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; +import refreshToken from './refresh-token.js'; +import verifyWebhook from './verify-webhook.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/typeform/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Typeform OAuth, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, + verifyWebhook, +}; diff --git a/packages/backend/src/apps/typeform/auth/is-still-verified.js b/packages/backend/src/apps/typeform/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..5f5c85ed493c305382693153b39da9b6fdf24d71 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/is-still-verified.js @@ -0,0 +1,7 @@ +const isStillVerified = async ($) => { + await $.http.get('/me'); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/typeform/auth/refresh-token.js b/packages/backend/src/apps/typeform/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..a2d7a256eb277a92fb2805500079925a9a876c03 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/refresh-token.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + grant_type: 'refresh_token', + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + refresh_token: $.auth.data.refreshToken, + scope: authScope.join(' '), + }); + + const { data } = await $.http.post('/oauth/token', params.toString()); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + tokenType: data.token_type, + refreshToken: data.refresh_token, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/typeform/auth/verify-credentials.js b/packages/backend/src/apps/typeform/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..87ee6bde88b255351ad850ead8b592fcb292fe09 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/verify-credentials.js @@ -0,0 +1,45 @@ +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrl = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ).value; + + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code, + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + redirect_uri: oauthRedirectUrl, + }); + + const { data: verifiedCredentials } = await $.http.post( + '/oauth/token', + params.toString() + ); + + const { + access_token: accessToken, + expires_in: expiresIn, + token_type: tokenType, + refresh_token: refreshToken, + } = verifiedCredentials; + + const { data: user } = await $.http.get('/me', { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + + await $.auth.set({ + accessToken, + expiresIn, + tokenType, + userId: user.user_id, + screenName: user.alias, + email: user.email, + refreshToken, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/typeform/auth/verify-webhook.js b/packages/backend/src/apps/typeform/auth/verify-webhook.js new file mode 100644 index 0000000000000000000000000000000000000000..a1672ea3f0d4d68293a72244de177ca395d17818 --- /dev/null +++ b/packages/backend/src/apps/typeform/auth/verify-webhook.js @@ -0,0 +1,20 @@ +import crypto from 'crypto'; + +import appConfig from '../../../config/app.js'; + +const verifyWebhook = async ($) => { + const signature = $.request.headers['typeform-signature']; + const isValid = verifySignature(signature, $.request.rawBody.toString()); + + return isValid; +}; + +const verifySignature = function (receivedSignature, payload) { + const hash = crypto + .createHmac('sha256', appConfig.webhookSecretKey) + .update(payload) + .digest('base64'); + return receivedSignature === `sha256=${hash}`; +}; + +export default verifyWebhook; diff --git a/packages/backend/src/apps/typeform/common/add-auth-header.js b/packages/backend/src/apps/typeform/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..bf9ea77202c370d161c34a651b61a0bdd055590a --- /dev/null +++ b/packages/backend/src/apps/typeform/common/add-auth-header.js @@ -0,0 +1,10 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/typeform/common/auth-scope.js b/packages/backend/src/apps/typeform/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..1063656cfc842f939b6e5915834b2a4bd27f04d2 --- /dev/null +++ b/packages/backend/src/apps/typeform/common/auth-scope.js @@ -0,0 +1,12 @@ +const authScope = [ + 'forms:read', + 'forms:write', + 'webhooks:read', + 'webhooks:write', + 'responses:read', + 'accounts:read', + 'workspaces:read', + 'offline', +]; + +export default authScope; diff --git a/packages/backend/src/apps/typeform/dynamic-data/index.js b/packages/backend/src/apps/typeform/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0a58430e6d8b4532f1277ea8336d8dad3340061f --- /dev/null +++ b/packages/backend/src/apps/typeform/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listForms from './list-forms/index.js'; + +export default [listForms]; diff --git a/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.js b/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..93e56f5acd0e494d39f8aa8ebd44756ff7044d3a --- /dev/null +++ b/packages/backend/src/apps/typeform/dynamic-data/list-forms/index.js @@ -0,0 +1,21 @@ +export default { + name: 'List forms', + key: 'listForms', + + async run($) { + const forms = { + data: [], + }; + + const response = await $.http.get('/forms'); + + forms.data = response.data.items.map((form) => { + return { + value: form.id, + name: form.title, + }; + }); + + return forms; + }, +}; diff --git a/packages/backend/src/apps/typeform/index.js b/packages/backend/src/apps/typeform/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3d59d69c8424372eedb1ca6c4de77cb1bd2492fd --- /dev/null +++ b/packages/backend/src/apps/typeform/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Typeform', + key: 'typeform', + iconUrl: '{BASE_URL}/apps/typeform/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/typeform/connection', + supportsConnections: true, + baseUrl: 'https://typeform.com', + apiBaseUrl: 'https://api.typeform.com', + primaryColor: '262627', + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/typeform/triggers/index.js b/packages/backend/src/apps/typeform/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..38b6a209ac301106db612c5baaff37f3a230b862 --- /dev/null +++ b/packages/backend/src/apps/typeform/triggers/index.js @@ -0,0 +1,3 @@ +import newEntry from './new-entry/index.js'; + +export default [newEntry]; diff --git a/packages/backend/src/apps/typeform/triggers/new-entry/index.js b/packages/backend/src/apps/typeform/triggers/new-entry/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2b666eefda64fc7b167740a987b8f82eb213445c --- /dev/null +++ b/packages/backend/src/apps/typeform/triggers/new-entry/index.js @@ -0,0 +1,101 @@ +import Crypto from 'crypto'; +import appConfig from '../../../../config/app.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New entry', + key: 'newEntry', + type: 'webhook', + description: 'Triggers when a new form is submitted.', + arguments: [ + { + label: 'Form', + key: 'formId', + type: 'dropdown', + required: true, + description: 'Pick a form to receive submissions.', + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listForms', + }, + ], + }, + }, + ], + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const { data: form } = await $.http.get( + `/forms/${$.step.parameters.formId}` + ); + + const { data: responses } = await $.http.get( + `/forms/${$.step.parameters.formId}/responses` + ); + + const lastResponse = responses.items[0]; + + if (!lastResponse) { + return; + } + + const computedWebhookEvent = { + event_type: 'form_response', + form_response: { + form_id: form.id, + token: lastResponse.token, + landed_at: lastResponse.landed_at, + submitted_at: lastResponse.submitted_at, + definition: { + id: $.step.parameters.formId, + title: form.title, + fields: form?.fields, + }, + answers: lastResponse.answers, + }, + }; + + const dataItem = { + raw: computedWebhookEvent, + meta: { + internalId: computedWebhookEvent.form_response.token, + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async registerHook($) { + const subscriptionPayload = { + enabled: true, + url: $.webhookUrl, + secret: appConfig.webhookSecretKey, + }; + + await $.http.put( + `/forms/${$.step.parameters.formId}/webhooks/${$.flow.id}`, + subscriptionPayload + ); + }, + + async unregisterHook($) { + await $.http.delete( + `/forms/${$.step.parameters.formId}/webhooks/${$.flow.id}` + ); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-case/fields.js b/packages/backend/src/apps/vtiger-crm/actions/create-case/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..cc03b26a4690f7b7c5b3424a2b4886be16ac109f --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-case/fields.js @@ -0,0 +1,408 @@ +export const fields = [ + { + label: 'Summary', + key: 'summary', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Record Currency', + key: 'recordCurrencyId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRecordCurrencies', + }, + ], + }, + }, + { + label: 'Case Title', + key: 'caseTitle', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.status', + value: 'casestatus', + }, + ], + }, + }, + { + label: 'Priority', + key: 'priority', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.priority', + value: 'casepriority', + }, + ], + }, + }, + { + label: 'Contact', + key: 'contactId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContacts', + }, + ], + }, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Group', + key: 'groupId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listGroups', + }, + ], + }, + }, + { + label: 'Assigned To', + key: 'assignedTo', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Product', + key: 'productId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProducts', + }, + ], + }, + }, + { + label: 'Channel', + key: 'channel', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.channel', + value: 'casechannel', + }, + ], + }, + }, + { + label: 'Resolution', + key: 'resolution', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Category', + key: 'category', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.category', + value: 'impact_type', + }, + ], + }, + }, + { + label: 'Sub Category', + key: 'subCategory', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.subCategory', + value: 'impact_area', + }, + ], + }, + }, + { + label: 'Resolution Type', + key: 'resolutionType', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.resolutionType', + value: 'resolution_type', + }, + ], + }, + }, + { + label: 'Deferred Date', + key: 'deferredDate', + type: 'string', + required: false, + description: 'format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Service Contract', + key: 'serviceContractId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listServiceContracts', + }, + ], + }, + }, + { + label: 'Asset', + key: 'assetId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listAssets', + }, + ], + }, + }, + { + label: 'SLA', + key: 'slaId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSlaNames', + }, + ], + }, + }, + { + label: 'Is Billable', + key: 'isBillable', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: '1' }, + { label: 'False', value: '-1' }, + ], + }, + { + label: 'Service', + key: 'service', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listServices', + }, + ], + }, + }, + { + label: 'Rate', + key: 'rate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Service Type', + key: 'serviceType', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.serviceType', + value: 'servicetype', + }, + ], + }, + }, + { + label: 'Service Location', + key: 'serviceLocation', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCaseOptions', + }, + { + name: 'parameters.serviceLocation', + value: 'servicelocation', + }, + ], + }, + }, + { + label: 'Work Location', + key: 'workLocation', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-case/index.js b/packages/backend/src/apps/vtiger-crm/actions/create-case/index.js new file mode 100644 index 0000000000000000000000000000000000000000..1c931d6da0834ec1808031f362d4df5678efcc7a --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-case/index.js @@ -0,0 +1,78 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create case', + key: 'createCase', + description: 'Create a new case.', + arguments: fields, + + async run($) { + const { + summary, + recordCurrencyId, + caseTitle, + status, + priority, + contactId, + organizationId, + groupId, + assignedTo, + productId, + channel, + resolution, + category, + subCategory, + resolutionType, + deferredDate, + serviceContractId, + assetId, + slaId, + isBillable, + service, + rate, + serviceType, + serviceLocation, + workLocation, + } = $.step.parameters; + + const elementData = { + description: summary, + record_currency_id: recordCurrencyId, + title: caseTitle, + casestatus: status, + casepriority: priority, + contact_id: contactId, + parent_id: organizationId, + group_id: groupId, + assigned_user_id: assignedTo, + product_id: productId, + casechannel: channel, + resolution: resolution, + impact_type: category, + impact_area: subCategory, + resolution_type: resolutionType, + deferred_date: deferredDate, + servicecontract_id: serviceContractId, + asset_id: assetId, + slaid: slaId, + is_billable: isBillable, + billing_service: service, + rate: rate, + servicetype: serviceType, + servicelocation: serviceLocation, + work_location: workLocation, + }; + + const body = { + operation: 'create', + sessionName: $.auth.data.sessionName, + element: JSON.stringify(elementData), + elementType: 'Cases', + }; + + const response = await $.http.post('/webservice.php', body); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-contact/fields.js b/packages/backend/src/apps/vtiger-crm/actions/create-contact/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..c3725d29c337a071420a1f363b2debeb668d24e8 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-contact/fields.js @@ -0,0 +1,649 @@ +export const fields = [ + { + label: 'Salutation', + key: 'salutation', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'Mr.', value: 'Mr.' }, + { label: 'Ms.', value: 'Ms.' }, + { label: 'Mrs.', value: 'Mrs.' }, + { label: 'Dr.', value: 'Dr.' }, + { label: 'Prof.', value: 'Prof.' }, + ], + }, + { + label: 'First Name', + key: 'firstName', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Last Name', + key: 'lastName', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Primary Email', + key: 'primaryEmail', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Office Phone', + key: 'officePhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Mobile Phone', + key: 'mobilePhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Home Phone', + key: 'homePhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Date of Birth', + key: 'dateOfBirth', + type: 'string', + required: false, + description: 'format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Fax', + key: 'fax', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Title', + key: 'title', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.title', + value: 'listContactOptions', + }, + ], + }, + }, + { + label: 'Department', + key: 'department', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Reports To', + key: 'reportsTo', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContacts', + }, + ], + }, + }, + { + label: 'Lead Source', + key: 'leadSource', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.leadSource', + value: 'leadsource', + }, + ], + }, + }, + { + label: 'Secondary Email', + key: 'secondaryEmail', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Assigned To', + key: 'assignedTo', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Do Not Call', + key: 'doNotCall', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: '1' }, + { label: 'False', value: '-1' }, + ], + }, + { + label: 'Notify Owner', + key: 'notifyOwner', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: '1' }, + { label: 'False', value: '-1' }, + ], + }, + { + label: 'Twitter Username', + key: 'twitterUsername', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'SLA', + key: 'slaId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSlaNames', + }, + ], + }, + }, + { + label: 'Lifecycle Stage', + key: 'lifecycleStage', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.lifecycleStage', + value: 'contacttype', + }, + ], + }, + }, + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.status', + value: 'contactstatus', + }, + ], + }, + }, + { + label: 'Happiness Rating', + key: 'happinessRating', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.happinessRating', + value: 'happiness_rating', + }, + ], + }, + }, + { + label: 'Record Currency', + key: 'recordCurrencyId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRecordCurrencies', + }, + ], + }, + }, + { + label: 'Referred By', + key: 'referredBy', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContacts', + }, + ], + }, + }, + { + label: 'Email Opt-in', + key: 'emailOptin', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.emailOptin', + value: 'emailoptin', + }, + ], + }, + }, + { + label: 'SMS Opt-in', + key: 'smsOptin', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.smsOptin', + value: 'smsoptin', + }, + ], + }, + }, + { + label: 'Language', + key: 'language', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.language', + value: 'language', + }, + ], + }, + }, + { + label: 'Source Campaign', + key: 'sourceCampaignId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCampaignSources', + }, + ], + }, + }, + { + label: 'Portal User', + key: 'portalUser', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: '1' }, + { label: 'False', value: '-1' }, + ], + }, + { + label: 'Support Start Date', + key: 'supportStartDate', + type: 'string', + required: false, + description: 'format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Support End Date', + key: 'supportEndDate', + type: 'string', + required: false, + description: 'format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Other Country', + key: 'otherCountry', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.otherCountry', + value: 'othercountry', + }, + ], + }, + }, + { + label: 'Mailing Country', + key: 'mailingCountry', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.mailingCountry', + value: 'mailingcountry', + }, + ], + }, + }, + { + label: 'Mailing Street', + key: 'mailingStreet', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Other Street', + key: 'otherStreet', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Mailing PO Box', + key: 'mailingPoBox', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Other PO Box', + key: 'otherPoBox', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Mailing City', + key: 'mailingCity', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Other City', + key: 'otherCity', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Mailing State', + key: 'mailingState', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.mailingState', + value: 'mailingstate', + }, + ], + }, + }, + { + label: 'Other State', + key: 'otherState', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContactOptions', + }, + { + name: 'parameters.otherState', + value: 'otherstate', + }, + ], + }, + }, + { + label: 'Mailing Zip', + key: 'mailingZip', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Other Zip', + key: 'otherZip', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Contact Image', + key: 'contactImage', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Linkedin URL', + key: 'linkedinUrl', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Linkedin Followers', + key: 'linkedinFollowers', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Facebook URL', + key: 'facebookUrl', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Facebook Followers', + key: 'facebookFollowers', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-contact/index.js b/packages/backend/src/apps/vtiger-crm/actions/create-contact/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5432e82d91abea89845355b44a998cbd723836c4 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-contact/index.js @@ -0,0 +1,129 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create contact', + key: 'createContact', + description: 'Create a new contact.', + arguments: fields, + + async run($) { + const { + salutation, + firstName, + lastName, + primaryEmail, + officePhone, + mobilePhone, + homePhone, + dateOfBirth, + fax, + organizationId, + title, + department, + reportsTo, + leadSource, + secondaryEmail, + assignedTo, + doNotCall, + notifyOwner, + twitterUsername, + slaId, + lifecycleStage, + status, + happinessRating, + recordCurrencyId, + referredBy, + emailOptin, + smsOptin, + language, + sourceCampaignId, + portalUser, + supportStartDate, + supportEndDate, + otherCountry, + mailingCountry, + mailingStreet, + otherStreet, + mailingPoBox, + otherPoBox, + mailingCity, + otherCity, + mailingState, + otherState, + mailingZip, + otherZip, + description, + contactImage, + linkedinUrl, + linkedinFollowers, + facebookUrl, + facebookFollowers, + } = $.step.parameters; + + const elementData = { + salutationtype: salutation, + firstname: firstName, + lastname: lastName, + email: primaryEmail, + phone: officePhone, + mobile: mobilePhone, + homephone: homePhone, + birthday: dateOfBirth, + fax: fax, + account_id: organizationId, + title: title, + department: department, + contact_id: reportsTo, + leadsource: leadSource, + secondaryemail: secondaryEmail, + assigned_user_id: assignedTo || $.auth.data.userId, + donotcall: doNotCall, + notify_owner: notifyOwner, + emailoptout: emailOptin, + primary_twitter: twitterUsername, + slaid: slaId, + contacttype: lifecycleStage, + contactstatus: status, + happiness_rating: happinessRating, + record_currency_id: recordCurrencyId, + referred_by: referredBy, + emailoptin: emailOptin, + smsoptin: smsOptin, + language: language, + source_campaign: sourceCampaignId, + portal: portalUser, + support_start_date: supportStartDate, + support_end_date: supportEndDate, + othercountry: otherCountry, + mailingcountry: mailingCountry, + mailingstreet: mailingStreet, + otherstreet: otherStreet, + mailingpobox: mailingPoBox, + otherpobox: otherPoBox, + mailingcity: mailingCity, + othercity: otherCity, + mailingstate: mailingState, + otherstate: otherState, + mailingzip: mailingZip, + otherzip: otherZip, + description: description, + imagename: contactImage, + primary_linkedin: linkedinUrl, + followers_linkedin: linkedinFollowers, + primary_facebook: facebookUrl, + followers_facebook: facebookFollowers, + }; + + const body = { + operation: 'create', + sessionName: $.auth.data.sessionName, + element: JSON.stringify(elementData), + elementType: 'Contacts', + }; + + const response = await $.http.post('/webservice.php', body); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-lead/fields.js b/packages/backend/src/apps/vtiger-crm/actions/create-lead/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..9f02573683c1996475bf25c9cf5950f43d57d598 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-lead/fields.js @@ -0,0 +1,395 @@ +export const fields = [ + { + label: 'Salutation', + key: 'salutation', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'Mr.', value: 'Mr.' }, + { label: 'Ms.', value: 'Ms.' }, + { label: 'Mrs.', value: 'Mrs.' }, + { label: 'Dr.', value: 'Dr.' }, + { label: 'Prof.', value: 'Prof.' }, + ], + }, + { + label: 'First Name', + key: 'firstName', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Last Name', + key: 'lastName', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Company', + key: 'company', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Primary Email', + key: 'primaryEmail', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Office Phone', + key: 'officePhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Designation', + key: 'designation', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.designation', + value: 'designation', + }, + ], + }, + }, + { + label: 'Mobile Phone', + key: 'mobilePhone', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Industry', + key: 'industry', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.industry', + value: 'industry', + }, + ], + }, + }, + { + label: 'Website', + key: 'website', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Annual Revenue', + key: 'annualRevenue', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Lead Source', + key: 'leadSource', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.leadSource', + value: 'leadsource', + }, + ], + }, + }, + { + label: 'Lead Status', + key: 'leadStatus', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.leadStatus', + value: 'leadstatus', + }, + ], + }, + }, + { + label: 'Assigned To', + key: 'assignedTo', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Fax', + key: 'fax', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Number of Employees', + key: 'numberOfEmployees', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Twitter Username', + key: 'twitterUsername', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Record Currency', + key: 'recordCurrencyId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRecordCurrencies', + }, + ], + }, + }, + { + label: 'Email Opt-in', + key: 'emailOptin', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.emailOptin', + value: 'emailoptin', + }, + ], + }, + }, + { + label: 'SMS Opt-in', + key: 'smsOptin', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.smsOptin', + value: 'smsoptin', + }, + ], + }, + }, + { + label: 'Language', + key: 'language', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.language', + value: 'language', + }, + ], + }, + }, + { + label: 'Source Campaign', + key: 'sourceCampaignId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCampaignSources', + }, + ], + }, + }, + { + label: 'Country', + key: 'country', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.country', + value: 'country', + }, + ], + }, + }, + { + label: 'Street', + key: 'street', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'PO Box', + key: 'poBox', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Postal Code', + key: 'postalCode', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'City', + key: 'city', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'State', + key: 'state', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listLeadOptions', + }, + { + name: 'parameters.state', + value: 'state', + }, + ], + }, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Lead Image', + key: 'leadImage', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-lead/index.js b/packages/backend/src/apps/vtiger-crm/actions/create-lead/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a4865cbfe3413dfaa312bc541357e29c8b242815 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-lead/index.js @@ -0,0 +1,88 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create lead', + key: 'createLead', + description: 'Create a new lead.', + arguments: fields, + + async run($) { + const { + salutation, + firstName, + lastName, + company, + primaryEmail, + officePhone, + designation, + mobilePhone, + industry, + website, + annualRevenue, + leadSource, + leadStatus, + assignedTo, + fax, + numberOfEmployees, + twitterUsername, + recordCurrencyId, + emailOptin, + smsOptin, + language, + sourceCampaignId, + country, + street, + poBox, + postalCode, + city, + state, + description, + leadImage, + } = $.step.parameters; + + const elementData = { + salutationtype: salutation, + firstname: firstName, + lastname: lastName, + company: company, + email: primaryEmail, + phone: officePhone, + designation: designation, + mobile: mobilePhone, + industry: industry, + website: website, + annualrevenue: annualRevenue, + leadsource: leadSource, + leadstatus: leadStatus, + assigned_user_id: assignedTo || $.auth.data.userId, + fax: fax, + noofemployees: numberOfEmployees, + primary_twitter: twitterUsername, + record_currency_id: recordCurrencyId, + emailoptin: emailOptin, + smsoptin: smsOptin, + language: language, + source_campaign: sourceCampaignId, + country: country, + lane: street, + pobox: poBox, + code: postalCode, + city: city, + state: state, + description: description, + imagename: leadImage, + }; + + const body = { + operation: 'create', + sessionName: $.auth.data.sessionName, + element: JSON.stringify(elementData), + elementType: 'Leads', + }; + + const response = await $.http.post('/webservice.php', body); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/fields.js b/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..a252da8c2b1c5e85cfa077d324ccd153418b7622 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/fields.js @@ -0,0 +1,244 @@ +export const fields = [ + { + label: 'Deal Name', + key: 'dealName', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Amount', + key: 'amount', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Contact', + key: 'contactId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContacts', + }, + ], + }, + }, + { + label: 'Expected Close Date', + key: 'expectedCloseDate', + type: 'string', + required: true, + description: 'Format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Pipeline', + key: 'pipeline', + type: 'dropdown', + required: true, + value: 'Standart', + description: '', + variables: true, + options: [{ label: 'Standart', value: 'Standart' }], + }, + { + label: 'Sales Stage', + key: 'salesStage', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOpportunityOptions', + }, + { + name: 'parameters.salesStage', + value: 'sales_stage', + }, + ], + }, + }, + { + label: 'Assigned To', + key: 'assignedTo', + type: 'string', + required: false, + description: 'Default is the id of the account connected to Automatisch.', + variables: true, + }, + { + label: 'Lead Source', + key: 'leadSource', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOpportunityOptions', + }, + { + name: 'parameters.leadSource', + value: 'leadsource', + }, + ], + }, + }, + { + label: 'Next Step', + key: 'nextStep', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOpportunityOptions', + }, + { + name: 'parameters.type', + value: 'opportunity_type', + }, + ], + }, + }, + { + label: 'Probability', + key: 'probability', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Campaign Source', + key: 'campaignSourceId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listCampaignSources', + }, + ], + }, + }, + { + label: 'Weighted Revenue', + key: 'weightedRevenue', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Adjusted Amount', + key: 'adjustedAmount', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Lost Reason', + key: 'lostReason', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOpportunityOptions', + }, + { + name: 'parameters.lostReason', + value: 'lost_reason', + }, + ], + }, + }, + { + label: 'Record Currency', + key: 'recordCurrencyId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRecordCurrencies', + }, + ], + }, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: '', + variables: true, + }, +]; diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/index.js b/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/index.js new file mode 100644 index 0000000000000000000000000000000000000000..12d2f7c2c24e1666a6c85ae99b2bc013cf7dd6cd --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-opportunity/index.js @@ -0,0 +1,64 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create opportunity', + key: 'createOpportunity', + description: 'Create a new opportunity.', + arguments: fields, + + async run($) { + const { + dealName, + amount, + organizationId, + contactId, + expectedCloseDate, + pipeline, + salesStage, + assignedTo, + leadSource, + nextStep, + type, + probability, + campaignSourceId, + weightedRevenue, + adjustedAmount, + lostReason, + recordCurrencyId, + description, + } = $.step.parameters; + + const elementData = { + potentialname: dealName, + amount, + related_to: organizationId, + contact_id: contactId, + closingdate: expectedCloseDate, + pipeline, + sales_stage: salesStage, + assigned_user_id: assignedTo || $.auth.data.userId, + leadsource: leadSource, + nextstep: nextStep, + opportunity_type: type, + probability: probability, + campaignid: campaignSourceId, + forecast_amount: weightedRevenue, + adjusted_amount: adjustedAmount, + lost_reason: lostReason, + record_currency_id: recordCurrencyId, + description, + }; + + const body = { + operation: 'create', + sessionName: $.auth.data.sessionName, + element: JSON.stringify(elementData), + elementType: 'Potentials', + }; + + const response = await $.http.post('/webservice.php', body); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-todo/fields.js b/packages/backend/src/apps/vtiger-crm/actions/create-todo/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..4cb8ee3eb03b54e1584cff1e0450fb1e89ccb548 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-todo/fields.js @@ -0,0 +1,357 @@ +export const fields = [ + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + description: '', + variables: true, + }, + { + label: 'Assigned To', + key: 'assignedTo', + type: 'string', + required: false, + description: 'Default is the id of the account connected to Automatisch.', + variables: true, + }, + { + label: 'Start Date & Time', + key: 'startDateAndTime', + type: 'string', + required: false, + description: 'Format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Due Date', + key: 'dueDate', + type: 'string', + required: false, + description: 'Format: yyyy-mm-dd', + variables: true, + }, + { + label: 'Stage', + key: 'stage', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTodoOptions', + }, + { + name: 'parameters.stage', + value: 'taskstatus', + }, + ], + }, + }, + { + label: 'Contact', + key: 'contactId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listContacts', + }, + ], + }, + }, + { + label: 'Priority', + key: 'priority', + type: 'dropdown', + required: true, + description: '', + variables: true, + options: [ + { label: 'High', value: 'High' }, + { label: 'Medium', value: 'Medium' }, + { label: 'Low', value: 'Low' }, + ], + }, + { + label: 'Send Notification', + key: 'sendNotification', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: 'true' }, + { label: 'False', value: 'false' }, + ], + }, + { + label: 'Location', + key: 'location', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Record Currency', + key: 'recordCurrencyId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listRecordCurrencies', + }, + ], + }, + }, + { + label: 'Milestone', + key: 'milestone', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listMilestones', + }, + ], + }, + }, + { + label: 'Previous Task', + key: 'previousTask', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTasks', + }, + ], + }, + }, + { + label: 'Parent Task', + key: 'parentTask', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTasks', + }, + ], + }, + }, + { + label: 'Task Type', + key: 'taskType', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTodoOptions', + }, + { + name: 'parameters.taskType', + value: 'tasktype', + }, + ], + }, + }, + { + label: 'Skipped Reason', + key: 'skippedReason', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTodoOptions', + }, + { + name: 'parameters.skippedReason', + value: 'skipped_reason', + }, + ], + }, + }, + { + label: 'Estimate', + key: 'estimate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Related Task', + key: 'relatedTask', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTasks', + }, + ], + }, + }, + { + label: 'Project', + key: 'projectId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listProjects', + }, + ], + }, + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Send Email Reminder Before', + key: 'sendEmailReminderBefore', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Description', + key: 'description', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'Is Billable', + key: 'isBillable', + type: 'dropdown', + required: false, + description: '', + variables: true, + options: [ + { label: 'True', value: '1' }, + { label: 'False', value: '-1' }, + ], + }, + { + label: 'Service', + key: 'service', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listServices', + }, + ], + }, + }, + { + label: 'Rate', + key: 'rate', + type: 'string', + required: false, + description: '', + variables: true, + }, + { + label: 'SLA', + key: 'slaId', + type: 'dropdown', + required: false, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSlaNames', + }, + ], + }, + }, +]; diff --git a/packages/backend/src/apps/vtiger-crm/actions/create-todo/index.js b/packages/backend/src/apps/vtiger-crm/actions/create-todo/index.js new file mode 100644 index 0000000000000000000000000000000000000000..60419d3ba37e4a6e63bb221c8ea50a67a7342a82 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/create-todo/index.js @@ -0,0 +1,78 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create todo', + key: 'createTodo', + description: 'Create a new todo.', + arguments: fields, + + async run($) { + const { + name, + assignedTo, + startDateAndTime, + dueDate, + stage, + contactId, + priority, + sendNotification, + location, + recordCurrencyId, + milestone, + previousTask, + parentTask, + taskType, + skippedReason, + estimate, + relatedTask, + projectId, + organizationId, + sendEmailReminderBefore, + description, + isBillable, + service, + rate, + slaId, + } = $.step.parameters; + + const elementData = { + subject: name, + assigned_user_id: assignedTo || $.auth.data.userId, + date_start: startDateAndTime, + due_date: dueDate, + taskstatus: stage, + contact_id: contactId, + taskpriority: priority, + sendnotification: sendNotification, + location: location, + record_currency_id: recordCurrencyId, + milestone: milestone, + dependent_on: previousTask, + parent_task: parentTask, + tasktype: taskType, + skipped_reason: skippedReason, + estimate: estimate, + related_task: relatedTask, + related_project: projectId, + account_id: organizationId, + reminder_time: sendEmailReminderBefore, + description: description, + is_billable: isBillable, + billing_service: service, + rate: rate, + slaid: slaId, + }; + + const body = { + operation: 'create', + sessionName: $.auth.data.sessionName, + element: JSON.stringify(elementData), + elementType: 'Calendar', + }; + + const response = await $.http.post('/webservice.php', body); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/actions/index.js b/packages/backend/src/apps/vtiger-crm/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a4cd130d03d662c005bbd4f84d3278309786e21c --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/actions/index.js @@ -0,0 +1,13 @@ +import createCase from './create-case/index.js'; +import createContact from './create-contact/index.js'; +import createLead from './create-lead/index.js'; +import createOpportunity from './create-opportunity/index.js'; +import createTodo from './create-todo/index.js'; + +export default [ + createCase, + createContact, + createLead, + createOpportunity, + createTodo, +]; diff --git a/packages/backend/src/apps/vtiger-crm/assets/favicon.svg b/packages/backend/src/apps/vtiger-crm/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..0d95870c39314fe3eebbbf68b5db18e39fa01bf2 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/assets/favicon.svg @@ -0,0 +1,925 @@ + + + + diff --git a/packages/backend/src/apps/vtiger-crm/auth/index.js b/packages/backend/src/apps/vtiger-crm/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..03536e69d1680663ba5eec2ef2a6504b3c01956d --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/auth/index.js @@ -0,0 +1,44 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'username', + label: 'Username', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Email address of your Vtiger CRM account', + clickToCopy: false, + }, + { + key: 'accessKey', + label: 'Access Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'Access Key of your Vtiger CRM account', + clickToCopy: false, + }, + { + key: 'domain', + label: 'Domain', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'For example: acmeco.od1 if your dashboard url is https://acmeco.od1.vtiger.com. (Unfortunately, we are not able to offer support for self-hosted instances at this moment.)', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/vtiger-crm/auth/is-still-verified.js b/packages/backend/src/apps/vtiger-crm/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6663679aaba8914963eb6c59dbdef4e0359cf585 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/vtiger-crm/auth/verify-credentials.js b/packages/backend/src/apps/vtiger-crm/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..eddc779a0ffdc81a3431c57a71c9bbf6c2ce520e --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/auth/verify-credentials.js @@ -0,0 +1,32 @@ +import crypto from 'crypto'; + +const verifyCredentials = async ($) => { + const params = { + operation: 'getchallenge', + username: $.auth.data.username, + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + const accessKey = crypto + .createHash('md5') + .update(data.result.token + $.auth.data.accessKey) + .digest('hex'); + + const body = { + operation: 'login', + username: $.auth.data.username, + accessKey, + }; + + const { data: result } = await $.http.post('/webservice.php', body); + + const response = await $.http.get('/restapi/v1/vtiger/default/me'); + + await $.auth.set({ + screenName: `${response.data.result?.first_name} ${response.data.result?.last_name}`, + sessionName: result.result.sessionName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/vtiger-crm/common/add-auth-header.js b/packages/backend/src/apps/vtiger-crm/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..52de16ea0f9968794c4029fef06fa178563a9f71 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/common/add-auth-header.js @@ -0,0 +1,15 @@ +const addAuthHeader = ($, requestConfig) => { + const { data } = $.auth; + + if (data?.username && data?.accessKey) { + requestConfig.headers['Content-Type'] = 'application/x-www-form-urlencoded'; + requestConfig.auth = { + username: data.username, + password: data.accessKey, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/vtiger-crm/common/set-base-url.js b/packages/backend/src/apps/vtiger-crm/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..a0dcffe1d32e8d642aa238a693434b3a5132803f --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/common/set-base-url.js @@ -0,0 +1,10 @@ +const setBaseUrl = ($, requestConfig) => { + const domain = $.auth.data.domain; + if (domain) { + requestConfig.baseURL = `https://${domain}.vtiger.com`; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c3ee6aca0a13c571bf7ac3ecfd7c70b2ac297acd --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/index.js @@ -0,0 +1,39 @@ +import listAssets from './list-assets/index.js'; +import listCampaignSources from './list-campaign-sources/index.js'; +import listCaseOptions from './list-case-options/index.js'; +import listContactOptions from './list-contact-options/index.js'; +import listContacts from './list-contacts/index.js'; +import listGroups from './list-groups/index.js'; +import listLeadOptions from './list-lead-options/index.js'; +import listMilestones from './list-milestones/index.js'; +import listOpportunityOptions from './list-opportunity-options/index.js'; +import listOrganizations from './list-organizations/index.js'; +import listProducts from './list-products/index.js'; +import listProjects from './list-projects/index.js'; +import listRecordCurrencies from './list-record-currencies/index.js'; +import listServiceContracts from './list-service-contracts/index.js'; +import listServices from './list-services/index.js'; +import listSlaNames from './list-sla-names/index.js'; +import listTasks from './list-tasks/index.js'; +import listTodoOptions from './list-todo-options/index.js'; + +export default [ + listAssets, + listCampaignSources, + listCaseOptions, + listContactOptions, + listContacts, + listGroups, + listLeadOptions, + listMilestones, + listOpportunityOptions, + listOrganizations, + listProducts, + listProjects, + listRecordCurrencies, + listServiceContracts, + listServices, + listSlaNames, + listTasks, + listTodoOptions, +]; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-assets/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-assets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3606a4f9736248ad82cd7320812d588831244e34 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-assets/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List assets', + key: 'listAssets', + + async run($) { + const assets = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Assets ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const asset of data.result) { + assets.data.push({ + value: asset.id, + name: `${asset.assetname} (${asset.assetstatus})`, + }); + } + } + + return assets; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-campaign-sources/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-campaign-sources/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e255127869c029b14d14c5b2715ebf8b5bdc5000 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-campaign-sources/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List campaign sources', + key: 'listCampaignSources', + + async run($) { + const campaignSources = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Campaigns ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result?.length) { + for (const campaignSource of data.result) { + campaignSources.data.push({ + value: campaignSource.id, + name: campaignSource.campaignname, + }); + } + } + + return campaignSources; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-case-options/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-case-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9cc0163b4610b9c7aca91b7857ac3733930eba77 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-case-options/index.js @@ -0,0 +1,58 @@ +export default { + name: 'List case options', + key: 'listCaseOptions', + + async run($) { + const caseOptions = { + data: [], + }; + const { + status, + priority, + contactName, + productName, + channel, + category, + subCategory, + resolutionType, + serviceType, + serviceLocation, + } = $.step.parameters; + + const picklistFields = [ + status, + priority, + contactName, + productName, + channel, + category, + subCategory, + resolutionType, + serviceType, + serviceLocation, + ]; + + const params = { + operation: 'describe', + sessionName: $.auth.data.sessionName, + elementType: 'Cases', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result.fields?.length) { + for (const field of data.result.fields) { + if (picklistFields.includes(field.name)) { + field.type.picklistValues.map((item) => + caseOptions.data.push({ + value: item.value, + name: item.label, + }) + ); + } + } + } + + return caseOptions; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contact-options/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contact-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e27257d400cd88e6cd00aada8dcb32caf137d010 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contact-options/index.js @@ -0,0 +1,62 @@ +export default { + name: 'List contact options', + key: 'listContactOptions', + + async run($) { + const leadOptions = { + data: [], + }; + const { + leadSource, + lifecycleStage, + status, + title, + happinessRating, + emailOptin, + smsOptin, + language, + otherCountry, + mailingCountry, + mailingState, + otherState, + } = $.step.parameters; + + const picklistFields = [ + leadSource, + lifecycleStage, + status, + title, + happinessRating, + emailOptin, + smsOptin, + language, + otherCountry, + mailingCountry, + mailingState, + otherState, + ]; + + const params = { + operation: 'describe', + sessionName: $.auth.data.sessionName, + elementType: 'Contacts', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result.fields?.length) { + for (const field of data.result.fields) { + if (picklistFields.includes(field.name)) { + field.type.picklistValues.map((item) => + leadOptions.data.push({ + value: item.value, + name: item.label, + }) + ); + } + } + } + + return leadOptions; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contacts/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contacts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c2d8c6ff4b692fb72f3cd37f4b6b5d401fc931f0 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-contacts/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List contacts', + key: 'listContacts', + + async run($) { + const contacts = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Contacts ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result?.length) { + for (const contact of data.result) { + contacts.data.push({ + value: contact.id, + name: `${contact.firstname} ${contact.lastname}`, + }); + } + } + + return contacts; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-groups/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-groups/index.js new file mode 100644 index 0000000000000000000000000000000000000000..51112b04148544811eff18d736ef7169dad94f03 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-groups/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List groups', + key: 'listGroups', + + async run($) { + const groups = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Groups;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const group of data.result) { + groups.data.push({ + value: group.id, + name: group.groupname, + }); + } + } + + return groups; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-lead-options/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-lead-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bd7ab650f6dcab3dfb80069b83ac708e6c62e0bd --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-lead-options/index.js @@ -0,0 +1,56 @@ +export default { + name: 'List lead options', + key: 'listLeadOptions', + + async run($) { + const leadOptions = { + data: [], + }; + const { + designation, + industry, + leadSource, + leadStatus, + emailOptin, + smsOptin, + language, + country, + state, + } = $.step.parameters; + + const picklistFields = [ + designation, + industry, + leadSource, + leadStatus, + emailOptin, + smsOptin, + language, + country, + state, + ]; + + const params = { + operation: 'describe', + sessionName: $.auth.data.sessionName, + elementType: 'Leads', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result.fields?.length) { + for (const field of data.result.fields) { + if (picklistFields.includes(field.name)) { + field.type.picklistValues.map((item) => + leadOptions.data.push({ + value: item.value, + name: item.label, + }) + ); + } + } + } + + return leadOptions; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-milestones/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-milestones/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9bc7a50cf933165b50cd75af330f212e5c3c716a --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-milestones/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List milestones', + key: 'listMilestones', + + async run($) { + const milestones = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM ProjectMilestone ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const milestone of data.result) { + milestones.data.push({ + value: milestone.id, + name: milestone.projectmilestonename, + }); + } + } + + return milestones; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-opportunity-options/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-opportunity-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b86cd7315d34125f1847ae93797ff68ea76fcb39 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-opportunity-options/index.js @@ -0,0 +1,38 @@ +export default { + name: 'List opportunity options', + key: 'listOpportunityOptions', + + async run($) { + const opportunityOptions = { + data: [], + }; + const leadSource = $.step.parameters.leadSource; + const lostReason = $.step.parameters.lostReason; + const type = $.step.parameters.type; + const salesStage = $.step.parameters.salesStage; + const picklistFields = [leadSource, lostReason, type, salesStage]; + + const params = { + operation: 'describe', + sessionName: $.auth.data.sessionName, + elementType: 'Potentials', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result.fields?.length) { + for (const field of data.result.fields) { + if (picklistFields.includes(field.name)) { + field.type.picklistValues.map((item) => + opportunityOptions.data.push({ + value: item.value, + name: item.label, + }) + ); + } + } + } + + return opportunityOptions; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-organizations/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-organizations/index.js new file mode 100644 index 0000000000000000000000000000000000000000..77b60302df9cddcd3f14b2beb0b7192454ec2856 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-organizations/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List organizations', + key: 'listOrganizations', + + async run($) { + const organizations = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Accounts ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result?.length) { + for (const organization of data.result) { + organizations.data.push({ + value: organization.id, + name: organization.accountname, + }); + } + } + + return organizations; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-products/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-products/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f4be10c47247a9db6d0941bccd96393f2b6b9928 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-products/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List products', + key: 'listProducts', + + async run($) { + const products = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Products ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const product of data.result) { + products.data.push({ + value: product.id, + name: product.productname, + }); + } + } + + return products; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-projects/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-projects/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ec12956bc4a0d610d8615b8cf62cda5329b69254 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-projects/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List projects', + key: 'listProjects', + + async run($) { + const projects = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Project ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const project of data.result) { + projects.data.push({ + value: project.id, + name: project.projectname, + }); + } + } + + return projects; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-record-currencies/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-record-currencies/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b416a6507428956aab11d94c31322b6fa3d0e0fa --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-record-currencies/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List record currencies', + key: 'listRecordCurrencies', + + async run($) { + const recordCurrencies = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Currency;', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result?.length) { + for (const recordCurrency of data.result) { + recordCurrencies.data.push({ + value: recordCurrency.id, + name: recordCurrency.currency_code, + }); + } + } + + return recordCurrencies; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-service-contracts/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-service-contracts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d67e5d02de892394c2c4238daeb2795fce453c6a --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-service-contracts/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List service contracts', + key: 'listServiceContracts', + + async run($) { + const serviceContracts = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM ServiceContracts ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const serviceContract of data.result) { + serviceContracts.data.push({ + value: serviceContract.id, + name: serviceContract.subject, + }); + } + } + + return serviceContracts; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-services/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-services/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c08c4ae17961ff96eeb670b3328567c471954f54 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-services/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List services', + key: 'listServices', + + async run($) { + const services = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Services ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const service of data.result) { + services.data.push({ + value: service.id, + name: service.servicename, + }); + } + } + + return services; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-sla-names/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-sla-names/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5026c82822f985f5b8dd1747c830cb180aa422b8 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-sla-names/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List sla names', + key: 'listSlaNames', + + async run($) { + const slaNames = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM SLA ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get(`/webservice.php`, { params }); + + if (data.result?.length) { + for (const slaName of data.result) { + slaNames.data.push({ + value: slaName.id, + name: slaName.policy_name, + }); + } + } + + return slaNames; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-tasks/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-tasks/index.js new file mode 100644 index 0000000000000000000000000000000000000000..87c174f96719226a120207fe7b280a6bd659ed21 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-tasks/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List tasks', + key: 'listTasks', + + async run($) { + const tasks = { + data: [], + }; + + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: 'SELECT * FROM Calendar ORDER BY createdtime DESC;', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result?.length) { + for (const task of data.result) { + tasks.data.push({ + value: task.id, + name: task.subject, + }); + } + } + + return tasks; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/dynamic-data/list-todo-options/index.js b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-todo-options/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ca0d1f4cf3c651ca6c930e62ba4780c336d10d9e --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/dynamic-data/list-todo-options/index.js @@ -0,0 +1,37 @@ +export default { + name: 'List todo options', + key: 'listTodoOptions', + + async run($) { + const todoOptions = { + data: [], + }; + const stage = $.step.parameters.stage; + const taskType = $.step.parameters.taskType; + const skippedReason = $.step.parameters.skippedReason; + const picklistFields = [stage, taskType, skippedReason]; + + const params = { + operation: 'describe', + sessionName: $.auth.data.sessionName, + elementType: 'Calendar', + }; + + const { data } = await $.http.get('/webservice.php', { params }); + + if (data.result.fields?.length) { + for (const field of data.result.fields) { + if (picklistFields.includes(field.name)) { + field.type.picklistValues.map((item) => + todoOptions.data.push({ + value: item.value, + name: item.label, + }) + ); + } + } + } + + return todoOptions; + }, +}; diff --git a/packages/backend/src/apps/vtiger-crm/index.js b/packages/backend/src/apps/vtiger-crm/index.js new file mode 100644 index 0000000000000000000000000000000000000000..504903044c799c505348b583b17125a302b75e62 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/index.js @@ -0,0 +1,23 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Vtiger CRM', + key: 'vtiger-crm', + iconUrl: '{BASE_URL}/apps/vtiger-crm/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/vtiger-crm/connection', + supportsConnections: true, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '39a86d', + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/index.js b/packages/backend/src/apps/vtiger-crm/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..faf1bff61986e83ff1255aba2c1bf9a5ceac2a88 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/index.js @@ -0,0 +1,15 @@ +import newCases from './new-cases/index.js'; +import newContacts from './new-contacts/index.js'; +import newInvoices from './new-invoices/index.js'; +import newLeads from './new-leads/index.js'; +import newOpportunities from './new-opportunities/index.js'; +import newTodos from './new-todos/index.js'; + +export default [ + newCases, + newContacts, + newInvoices, + newLeads, + newOpportunities, + newTodos, +]; diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-cases/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-cases/index.js new file mode 100644 index 0000000000000000000000000000000000000000..40915153d6bdf635b53aa86fd88f3116c5b719e4 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-cases/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New cases', + key: 'newCases', + pollInterval: 15, + description: 'Triggers when a new case is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Cases ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-contacts/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-contacts/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0a8da87019f234468258a582a53670aba4715628 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-contacts/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New contacts', + key: 'newContacts', + pollInterval: 15, + description: 'Triggers when a new contact is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Contacts ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-invoices/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-invoices/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ac17d6f96842279d8ec62ed87d1c1f05fded1521 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-invoices/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New invoices', + key: 'newInvoices', + pollInterval: 15, + description: 'Triggers when a new invoice is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Invoice ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-leads/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-leads/index.js new file mode 100644 index 0000000000000000000000000000000000000000..03999ba5ab2ba70a214bf0488a31b3b304a3d705 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-leads/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New leads', + key: 'newLeads', + pollInterval: 15, + description: 'Triggers when a new lead is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Leads ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-opportunities/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-opportunities/index.js new file mode 100644 index 0000000000000000000000000000000000000000..db3cd151c9fd22d91f9a531c162f4bab490186d7 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-opportunities/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New opportunities', + key: 'newOpportunities', + pollInterval: 15, + description: 'Triggers when a new opportunity is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Potentials ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/vtiger-crm/triggers/new-todos/index.js b/packages/backend/src/apps/vtiger-crm/triggers/new-todos/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4bf72294f301d50f23207a8a27b28020cb1c9283 --- /dev/null +++ b/packages/backend/src/apps/vtiger-crm/triggers/new-todos/index.js @@ -0,0 +1,40 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New todos', + key: 'newTodos', + pollInterval: 15, + description: 'Triggers when a new todo is created.', + + async run($) { + let offset = 0; + const limit = 100; + let hasMore = true; + + do { + const params = { + operation: 'query', + sessionName: $.auth.data.sessionName, + query: `SELECT * FROM Calendar ORDER BY createdtime DESC LIMIT ${offset}, ${limit};`, + }; + + const { data } = await $.http.get('/webservice.php', { + params, + }); + offset = limit + offset; + + if (!data.result?.length || data.result.length < limit) { + hasMore = false; + } + + for (const item of data.result) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.id, + }, + }); + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/webhook/actions/index.js b/packages/backend/src/apps/webhook/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a7c48975bc87983b6e268d2f9ac928938574545b --- /dev/null +++ b/packages/backend/src/apps/webhook/actions/index.js @@ -0,0 +1,3 @@ +import respondWith from './respond-with/index.js'; + +export default [respondWith]; diff --git a/packages/backend/src/apps/webhook/actions/respond-with/index.js b/packages/backend/src/apps/webhook/actions/respond-with/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b33d10f7ffbd2683e5a44fd4c82690edfcd34e08 --- /dev/null +++ b/packages/backend/src/apps/webhook/actions/respond-with/index.js @@ -0,0 +1,69 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Respond with', + key: 'respondWith', + description: 'Respond with defined JSON body.', + arguments: [ + { + label: 'Status code', + key: 'statusCode', + type: 'string', + required: true, + variables: true, + value: '200', + }, + { + label: 'Headers', + key: 'headers', + type: 'dynamic', + required: false, + description: 'Add or remove headers as needed', + fields: [ + { + label: 'Key', + key: 'key', + type: 'string', + required: true, + description: 'Header key', + variables: true, + }, + { + label: 'Value', + key: 'value', + type: 'string', + required: true, + description: 'Header value', + variables: true, + }, + ], + }, + { + label: 'Body', + key: 'body', + type: 'string', + required: true, + description: 'The content of the response body.', + variables: true, + }, + ], + + async run($) { + const statusCode = parseInt($.step.parameters.statusCode, 10); + const body = $.step.parameters.body; + const headers = $.step.parameters.headers.reduce((result, entry) => { + return { + ...result, + [entry.key]: entry.value, + }; + }, {}); + + $.setActionItem({ + raw: { + headers, + body, + statusCode, + }, + }); + }, +}); diff --git a/packages/backend/src/apps/webhook/assets/favicon.svg b/packages/backend/src/apps/webhook/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..140ebd66f877f4ceda0efcd6ab6929296f55b1f8 --- /dev/null +++ b/packages/backend/src/apps/webhook/assets/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/webhook/index.js b/packages/backend/src/apps/webhook/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9cde3525f4868b0499fdf28470f3bd9d418530f1 --- /dev/null +++ b/packages/backend/src/apps/webhook/index.js @@ -0,0 +1,16 @@ +import defineApp from '../../helpers/define-app.js'; +import actions from './actions/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Webhook', + key: 'webhook', + iconUrl: '{BASE_URL}/apps/webhook/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/webhook/connection', + supportsConnections: false, + baseUrl: '', + apiBaseUrl: '', + primaryColor: '0059F7', + actions, + triggers, +}); diff --git a/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.js b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0494d22c7f43f22d2387b038423612b9fb79507e --- /dev/null +++ b/packages/backend/src/apps/webhook/triggers/catch-raw-webhook/index.js @@ -0,0 +1,52 @@ +import Crypto from 'crypto'; +import isEmpty from 'lodash/isEmpty.js'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'Catch raw webhook', + key: 'catchRawWebhook', + type: 'webhook', + showWebhookUrl: true, + description: + 'Triggers (immediately if configured) when the webhook receives a request.', + arguments: [ + { + label: 'Wait until flow is done', + key: 'workSynchronously', + type: 'dropdown', + required: true, + options: [ + { label: 'Yes', value: true }, + { label: 'No', value: false }, + ], + }, + ], + + async run($) { + const dataItem = { + raw: { + headers: $.request.headers, + body: $.request.body, + query: $.request.query, + }, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const lastExecutionStep = await $.getLastExecutionStep(); + + if (!isEmpty(lastExecutionStep?.dataOut)) { + $.pushTriggerItem({ + raw: lastExecutionStep.dataOut, + meta: { + internalId: '', + }, + }); + } + }, +}); diff --git a/packages/backend/src/apps/webhook/triggers/index.js b/packages/backend/src/apps/webhook/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..166d42750ef8a66bcd150a1c360a5dc679834b05 --- /dev/null +++ b/packages/backend/src/apps/webhook/triggers/index.js @@ -0,0 +1,3 @@ +import catchRawWebhook from './catch-raw-webhook/index.js'; + +export default [catchRawWebhook]; diff --git a/packages/backend/src/apps/wordpress/assets/favicon.svg b/packages/backend/src/apps/wordpress/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..39be6e125d8d54c92cd8cff6188003a4f10e06b8 --- /dev/null +++ b/packages/backend/src/apps/wordpress/assets/favicon.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/wordpress/auth/generate-auth-url.js b/packages/backend/src/apps/wordpress/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..d7eeb961df1a814da1e66a54e7c0fd600d6d42bd --- /dev/null +++ b/packages/backend/src/apps/wordpress/auth/generate-auth-url.js @@ -0,0 +1,26 @@ +import { URL, URLSearchParams } from 'node:url'; + +import appConfig from '../../../config/app.js'; +import getInstanceUrl from '../common/get-instance-url.js'; + +export default async function generateAuthUrl($) { + const successUrl = new URL( + '/app/wordpress/connections/add', + appConfig.webAppUrl + ).toString(); + const baseUrl = getInstanceUrl($); + + const searchParams = new URLSearchParams({ + app_name: 'automatisch', + success_url: successUrl, + }); + + const url = new URL( + `/wp-admin/authorize-application.php?${searchParams}`, + baseUrl + ).toString(); + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/wordpress/auth/index.js b/packages/backend/src/apps/wordpress/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d906621ed9d34a85f0660769d7382134a9ef5ddb --- /dev/null +++ b/packages/backend/src/apps/wordpress/auth/index.js @@ -0,0 +1,24 @@ +import generateAuthUrl from './generate-auth-url.js'; +import isStillVerified from './is-still-verified.js'; +import verifyCredentials from './verify-credentials.js'; + +export default { + fields: [ + { + key: 'instanceUrl', + label: 'WordPress instance URL', + type: 'string', + required: false, + readOnly: false, + value: null, + placeholder: null, + description: 'Your WordPress instance URL.', + docUrl: 'https://automatisch.io/docs/wordpress#instance-url', + clickToCopy: true, + }, + ], + + generateAuthUrl, + isStillVerified, + verifyCredentials, +}; diff --git a/packages/backend/src/apps/wordpress/auth/is-still-verified.js b/packages/backend/src/apps/wordpress/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..d77bac26c88862580a648a10cadf47419a74797f --- /dev/null +++ b/packages/backend/src/apps/wordpress/auth/is-still-verified.js @@ -0,0 +1,7 @@ +const isStillVerified = async ($) => { + await $.http.get('?rest_route=/wp/v2/settings'); + + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/wordpress/auth/verify-credentials.js b/packages/backend/src/apps/wordpress/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..b1cf86ba1abee19cf54df44e7ba947af1da02a06 --- /dev/null +++ b/packages/backend/src/apps/wordpress/auth/verify-credentials.js @@ -0,0 +1,22 @@ +const verifyCredentials = async ($) => { + const instanceUrl = $.auth.data.instanceUrl; + const password = $.auth.data.password; + const siteUrl = $.auth.data.site_url; + const url = $.auth.data.url; + const userLogin = $.auth.data.user_login; + + if (!password) { + throw new Error('Failed while authorizing!'); + } + + await $.auth.set({ + screenName: `${userLogin} @ ${siteUrl}`, + instanceUrl, + password, + siteUrl, + url, + userLogin, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/wordpress/common/add-auth-header.js b/packages/backend/src/apps/wordpress/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..e3a668c63553ee9db25c983c2411f168da463f33 --- /dev/null +++ b/packages/backend/src/apps/wordpress/common/add-auth-header.js @@ -0,0 +1,15 @@ +const addAuthHeader = ($, requestConfig) => { + const userLogin = $.auth.data.userLogin; + const password = $.auth.data.password; + + if (userLogin && password) { + requestConfig.auth = { + username: userLogin, + password, + }; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/wordpress/common/get-instance-url.js b/packages/backend/src/apps/wordpress/common/get-instance-url.js new file mode 100644 index 0000000000000000000000000000000000000000..0d7125a3251c41b5466495caacfd4245abca6fa3 --- /dev/null +++ b/packages/backend/src/apps/wordpress/common/get-instance-url.js @@ -0,0 +1,5 @@ +const getInstanceUrl = ($) => { + return $.auth.data.instanceUrl; +}; + +export default getInstanceUrl; diff --git a/packages/backend/src/apps/wordpress/common/set-base-url.js b/packages/backend/src/apps/wordpress/common/set-base-url.js new file mode 100644 index 0000000000000000000000000000000000000000..def1330c364c900c907502a08bf1da104c01542f --- /dev/null +++ b/packages/backend/src/apps/wordpress/common/set-base-url.js @@ -0,0 +1,10 @@ +const setBaseUrl = ($, requestConfig) => { + const instanceUrl = $.auth.data.instanceUrl; + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } + + return requestConfig; +}; + +export default setBaseUrl; diff --git a/packages/backend/src/apps/wordpress/dynamic-data/index.js b/packages/backend/src/apps/wordpress/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ce289b38265279df5addc02adc9474096e58a7e1 --- /dev/null +++ b/packages/backend/src/apps/wordpress/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listStatuses from './list-statuses/index.js'; + +export default [listStatuses]; diff --git a/packages/backend/src/apps/wordpress/dynamic-data/list-statuses/index.js b/packages/backend/src/apps/wordpress/dynamic-data/list-statuses/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f457228bd086ca84444f18dcdcd3fb44f307d7e6 --- /dev/null +++ b/packages/backend/src/apps/wordpress/dynamic-data/list-statuses/index.js @@ -0,0 +1,27 @@ +export default { + name: 'List statuses', + key: 'listStatuses', + + async run($) { + const statuses = { + data: [], + }; + + const { data } = await $.http.get('?rest_route=/wp/v2/statuses'); + + if (!data) return statuses; + + const values = Object.values(data); + + if (!values?.length) return statuses; + + for (const status of values) { + statuses.data.push({ + value: status.slug, + name: status.name, + }); + } + + return statuses; + }, +}; diff --git a/packages/backend/src/apps/wordpress/index.js b/packages/backend/src/apps/wordpress/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6a95024c066e3177c2002c07a4afc800fdba2169 --- /dev/null +++ b/packages/backend/src/apps/wordpress/index.js @@ -0,0 +1,21 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import setBaseUrl from './common/set-base-url.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'WordPress', + key: 'wordpress', + iconUrl: '{BASE_URL}/apps/wordpress/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/wordpress/connection', + supportsConnections: true, + baseUrl: 'https://wordpress.com', + apiBaseUrl: '', + primaryColor: '464342', + beforeRequest: [setBaseUrl, addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/wordpress/triggers/index.js b/packages/backend/src/apps/wordpress/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..619c8ca193c7796284bcc2a1a237102322f7933d --- /dev/null +++ b/packages/backend/src/apps/wordpress/triggers/index.js @@ -0,0 +1,5 @@ +import newComment from './new-comment/index.js'; +import newPage from './new-page/index.js'; +import newPost from './new-post/index.js'; + +export default [newComment, newPage, newPost]; diff --git a/packages/backend/src/apps/wordpress/triggers/new-comment/index.js b/packages/backend/src/apps/wordpress/triggers/new-comment/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3ab2423eb847f97527dde489543221c13890366b --- /dev/null +++ b/packages/backend/src/apps/wordpress/triggers/new-comment/index.js @@ -0,0 +1,58 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New comment', + key: 'newComment', + description: 'Triggers when a new comment is created.', + arguments: [ + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: true, + variables: true, + options: [ + { label: 'Approve', value: 'approve' }, + { label: 'Unapprove', value: 'hold' }, + { label: 'Spam', value: 'spam' }, + { label: 'Trash', value: 'trash' }, + ], + }, + ], + + async run($) { + const params = { + per_page: 100, + page: 1, + order: 'desc', + orderby: 'date', + status: $.step.parameters.status || '', + }; + + let totalPages = 1; + do { + const { data, headers } = await $.http.get( + '?rest_route=/wp/v2/comments', + { + params, + } + ); + + params.page = params.page + 1; + totalPages = Number(headers['x-wp-totalpages']); + + if (data.length) { + for (const page of data) { + const dataItem = { + raw: page, + meta: { + internalId: page.id.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (params.page <= totalPages); + }, +}); diff --git a/packages/backend/src/apps/wordpress/triggers/new-page/index.js b/packages/backend/src/apps/wordpress/triggers/new-page/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3fb5086e3a0e0f6643af68a0694fed16ec6fe1b9 --- /dev/null +++ b/packages/backend/src/apps/wordpress/triggers/new-page/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New page', + key: 'newPage', + description: 'Triggers when a new page is created.', + arguments: [ + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listStatuses', + }, + ], + }, + }, + ], + + async run($) { + const params = { + per_page: 100, + page: 1, + order: 'desc', + orderby: 'date', + status: $.step.parameters.status || '', + }; + + let totalPages = 1; + do { + const { data, headers } = await $.http.get('?rest_route=/wp/v2/pages', { + params, + }); + + params.page = params.page + 1; + totalPages = Number(headers['x-wp-totalpages']); + + if (data.length) { + for (const page of data) { + const dataItem = { + raw: page, + meta: { + internalId: page.id.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (params.page <= totalPages); + }, +}); diff --git a/packages/backend/src/apps/wordpress/triggers/new-post/index.js b/packages/backend/src/apps/wordpress/triggers/new-post/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8b8dec79cb40281996e6a3bd4e4ef884f48796bf --- /dev/null +++ b/packages/backend/src/apps/wordpress/triggers/new-post/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New post', + key: 'newPost', + description: 'Triggers when a new post is created.', + arguments: [ + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listStatuses', + }, + ], + }, + }, + ], + + async run($) { + const params = { + per_page: 100, + page: 1, + order: 'desc', + orderby: 'date', + status: $.step.parameters.status || '', + }; + + let totalPages = 1; + do { + const { data, headers } = await $.http.get('?rest_route=/wp/v2/posts', { + params, + }); + + params.page = params.page + 1; + totalPages = Number(headers['x-wp-totalpages']); + + if (data.length) { + for (const post of data) { + const dataItem = { + raw: post, + meta: { + internalId: post.id.toString(), + }, + }; + + $.pushTriggerItem(dataItem); + } + } + } while (params.page <= totalPages); + }, +}); diff --git a/packages/backend/src/apps/xero/assets/favicon.svg b/packages/backend/src/apps/xero/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e1cd725fc04a3a94e8eeff0c70f0a54084708e16 --- /dev/null +++ b/packages/backend/src/apps/xero/assets/favicon.svg @@ -0,0 +1 @@ +Xero homepageBeautiful business \ No newline at end of file diff --git a/packages/backend/src/apps/xero/auth/generate-auth-url.js b/packages/backend/src/apps/xero/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..e535dce5507b1910e6b06fdd4820e5af6a869c82 --- /dev/null +++ b/packages/backend/src/apps/xero/auth/generate-auth-url.js @@ -0,0 +1,21 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + response_type: 'code', + client_id: $.auth.data.clientId, + scope: authScope.join(' '), + redirect_uri: redirectUri, + }); + + const url = `https://login.xero.com/identity/connect/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/xero/auth/index.js b/packages/backend/src/apps/xero/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d84c9a7b0f5f48527d00c2b77b3e34be4843c944 --- /dev/null +++ b/packages/backend/src/apps/xero/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/xero/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Xero, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/xero/auth/is-still-verified.js b/packages/backend/src/apps/xero/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..45b89a690b70f814ae52959b6d2026b9be385e6a --- /dev/null +++ b/packages/backend/src/apps/xero/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.tenantName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/xero/auth/refresh-token.js b/packages/backend/src/apps/xero/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..75d1c21c0e52e027c12d903932f8571b01bb6bc0 --- /dev/null +++ b/packages/backend/src/apps/xero/auth/refresh-token.js @@ -0,0 +1,39 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + + const params = new URLSearchParams({ + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://identity.xero.com/connect/token', + params.toString(), + { + headers, + additionalProperties: { + skipAddingAuthHeader: true, + }, + } + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + idToken: data.id_token, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/xero/auth/verify-credentials.js b/packages/backend/src/apps/xero/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..61170d1abbb116b5d5efae553763fd10ccbad5b7 --- /dev/null +++ b/packages/backend/src/apps/xero/auth/verify-credentials.js @@ -0,0 +1,52 @@ +import getCurrentUser from '../common/get-current-user.js'; +import { URLSearchParams } from 'url'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const headers = { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }; + const params = new URLSearchParams({ + grant_type: 'authorization_code', + code: $.auth.data.code, + redirect_uri: redirectUri, + }); + + const { data } = await $.http.post( + 'https://identity.xero.com/connect/token', + params.toString(), + { + headers, + } + ); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + idToken: data.id_token, + }); + + const currentUser = await getCurrentUser($); + + const screenName = [currentUser.tenantName, currentUser.tenantType] + .filter(Boolean) + .join(' @ '); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + tenantId: currentUser.tenantId, + screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/xero/common/add-auth-header.js b/packages/backend/src/apps/xero/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..045e4d21e14fa0a8b9c25bb9629f5079de621f96 --- /dev/null +++ b/packages/backend/src/apps/xero/common/add-auth-header.js @@ -0,0 +1,16 @@ +const addAuthHeader = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) + return requestConfig; + + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + if ($.auth.data?.tenantId) { + requestConfig.headers['Xero-tenant-id'] = $.auth.data.tenantId; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/xero/common/auth-scope.js b/packages/backend/src/apps/xero/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..6a4b5483c016824294fdf62351e1b842e9a219d3 --- /dev/null +++ b/packages/backend/src/apps/xero/common/auth-scope.js @@ -0,0 +1,10 @@ +const authScope = [ + 'offline_access', + 'openid', + 'profile', + 'email', + 'accounting.transactions', + 'accounting.settings', +]; + +export default authScope; diff --git a/packages/backend/src/apps/xero/common/get-current-user.js b/packages/backend/src/apps/xero/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..0bc69161a735145ddc87acedb5c9b6bb6757359a --- /dev/null +++ b/packages/backend/src/apps/xero/common/get-current-user.js @@ -0,0 +1,6 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get('/connections'); + return currentUser[0]; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/xero/dynamic-data/index.js b/packages/backend/src/apps/xero/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..de48bc3fa3e72e6fc1e75e49440f234a04535ab6 --- /dev/null +++ b/packages/backend/src/apps/xero/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listOrganizations from './list-organizations/index.js'; + +export default [listOrganizations]; diff --git a/packages/backend/src/apps/xero/dynamic-data/list-organizations/index.js b/packages/backend/src/apps/xero/dynamic-data/list-organizations/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ea16b5a261196c5211ffd10cb09094187ddee056 --- /dev/null +++ b/packages/backend/src/apps/xero/dynamic-data/list-organizations/index.js @@ -0,0 +1,23 @@ +export default { + name: 'List organizations', + key: 'listOrganizations', + + async run($) { + const organizations = { + data: [], + }; + + const { data } = await $.http.get('/api.xro/2.0/Organisation'); + + if (data.Organisations?.length) { + for (const organization of data.Organisations) { + organizations.data.push({ + value: organization.OrganisationID, + name: organization.Name, + }); + } + } + + return organizations; + }, +}; diff --git a/packages/backend/src/apps/xero/index.js b/packages/backend/src/apps/xero/index.js new file mode 100644 index 0000000000000000000000000000000000000000..616ce8ba19418cdae85d36b5ca85dc53b0dc031a --- /dev/null +++ b/packages/backend/src/apps/xero/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Xero', + key: 'xero', + baseUrl: 'https://go.xero.com', + apiBaseUrl: 'https://api.xero.com', + iconUrl: '{BASE_URL}/apps/xero/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/xero/connection', + primaryColor: '13B5EA', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/xero/triggers/index.js b/packages/backend/src/apps/xero/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e4b774bfe61a9217e31e097769043d325f5db86b --- /dev/null +++ b/packages/backend/src/apps/xero/triggers/index.js @@ -0,0 +1,4 @@ +import newBankTransactions from './new-bank-transactions/index.js'; +import newPayments from './new-payments/index.js'; + +export default [newBankTransactions, newPayments]; diff --git a/packages/backend/src/apps/xero/triggers/new-bank-transactions/index.js b/packages/backend/src/apps/xero/triggers/new-bank-transactions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..b1f012e1e2d18ef6f6be45e33d5268e76fa5198f --- /dev/null +++ b/packages/backend/src/apps/xero/triggers/new-bank-transactions/index.js @@ -0,0 +1,60 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New bank transactions', + key: 'newBankTransactions', + pollInterval: 15, + description: 'Triggers when a new bank transaction occurs.', + arguments: [ + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + ], + + async run($) { + const params = { + page: 1, + order: 'Date DESC', + }; + + let nextPage = false; + do { + const { data } = await $.http.get('/api.xro/2.0/BankTransactions', { + params, + }); + params.page = params.page + 1; + + if (data.BankTransactions?.length) { + for (const bankTransaction of data.BankTransactions) { + $.pushTriggerItem({ + raw: bankTransaction, + meta: { + internalId: bankTransaction.BankTransactionID, + }, + }); + } + } + + if (data.BankTransactions?.length === 100) { + nextPage = true; + } else { + nextPage = false; + } + } while (nextPage); + }, +}); diff --git a/packages/backend/src/apps/xero/triggers/new-payments/index.js b/packages/backend/src/apps/xero/triggers/new-payments/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fbd8bd7224a5e710f526508097db3396d455fb8a --- /dev/null +++ b/packages/backend/src/apps/xero/triggers/new-payments/index.js @@ -0,0 +1,103 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New payments', + key: 'newPayments', + pollInterval: 15, + description: 'Triggers when a new payment is received.', + arguments: [ + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: true, + description: '', + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'Payment Type', + key: 'paymentType', + type: 'dropdown', + required: false, + description: '', + variables: true, + value: '', + options: [ + { label: 'Accounts Receivable', value: 'ACCRECPAYMENT' }, + { label: 'Accounts Payable', value: 'ACCPAYPAYMENT' }, + { + label: 'Accounts Receivable Credit (Refund)', + value: 'ARCREDITPAYMENT', + }, + { + label: 'Accounts Payable Credit (Refund)', + value: 'APCREDITPAYMENT', + }, + { + label: 'Accounts Receivable Overpayment (Refund)', + value: 'AROVERPAYMENTPAYMENT', + }, + { + label: 'Accounts Receivable Prepayment (Refund)', + value: 'ARPREPAYMENTPAYMENT', + }, + { + label: 'Accounts Payable Prepayment (Refund)', + value: 'APPREPAYMENTPAYMENT', + }, + { + label: 'Accounts Payable Overpayment (Refund)', + value: 'APOVERPAYMENTPAYMENT', + }, + ], + }, + ], + + async run($) { + const paymentType = $.step.parameters.paymentType; + + const params = { + page: 1, + order: 'Date DESC', + }; + + if (paymentType) { + params.where = `PaymentType="${paymentType}"`; + } + + let nextPage = false; + do { + const { data } = await $.http.get('/api.xro/2.0/Payments', { + params, + }); + params.page = params.page + 1; + + if (data.Payments?.length) { + for (const payment of data.Payments) { + $.pushTriggerItem({ + raw: payment, + meta: { + internalId: payment.PaymentID, + }, + }); + } + } + + if (data.Payments?.length === 100) { + nextPage = true; + } else { + nextPage = false; + } + } while (nextPage); + }, +}); diff --git a/packages/backend/src/apps/you-need-a-budget/assets/favicon.svg b/packages/backend/src/apps/you-need-a-budget/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..c83333c8821a630b01cad266167ad36a969198ed --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/assets/favicon.svg @@ -0,0 +1,25 @@ + + + + My Budget + + \ No newline at end of file diff --git a/packages/backend/src/apps/you-need-a-budget/auth/generate-auth-url.js b/packages/backend/src/apps/you-need-a-budget/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..12d647e194c8161faef9b6bcc71e9ea091ab4358 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const state = Math.random().toString(); + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + response_type: 'code', + state, + }); + + const url = `https://app.ynab.com/oauth/authorize?${searchParams.toString()}`; + + await $.auth.set({ + url, + originalState: state, + }); +} diff --git a/packages/backend/src/apps/you-need-a-budget/auth/index.js b/packages/backend/src/apps/you-need-a-budget/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..32eef53ef50f16502285c99ba81785d89acdb5ea --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/auth/index.js @@ -0,0 +1,60 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/you-need-a-budget/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in You Need A Budget, enter the URL above.', + clickToCopy: true, + }, + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/you-need-a-budget/auth/is-still-verified.js b/packages/backend/src/apps/you-need-a-budget/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..6d792b125dc06c5812c8375c0c31e511dc71619a --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/you-need-a-budget/auth/refresh-token.js b/packages/backend/src/apps/you-need-a-budget/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..9c7830e44f789100746e492d7ea4542b9f75aada --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/auth/refresh-token.js @@ -0,0 +1,25 @@ +import { URLSearchParams } from 'node:url'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://app.ynab.com/oauth/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + refreshToken: data.refresh_token, + expiresIn: data.expires_in, + scope: data.scope, + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/you-need-a-budget/auth/verify-credentials.js b/packages/backend/src/apps/you-need-a-budget/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..19d88fa3a27c792953a12956b05f2481a24e87c9 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/auth/verify-credentials.js @@ -0,0 +1,33 @@ +const verifyCredentials = async ($) => { + if ($.auth.data.originalState !== $.auth.data.state) { + throw new Error(`The 'state' parameter does not match.`); + } + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post('https://app.ynab.com/oauth/token', { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + redirect_uri: redirectUri, + grant_type: 'authorization_code', + code: $.auth.data.code, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: data.scope, + createdAt: data.created_at, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/you-need-a-budget/common/add-auth-header.js b/packages/backend/src/apps/you-need-a-budget/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/you-need-a-budget/common/get-current-user.js b/packages/backend/src/apps/you-need-a-budget/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..65dea829ad88e3a3413bfacbde0210f0d5bf7bb6 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/common/get-current-user.js @@ -0,0 +1,6 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get('/user'); + return currentUser.data.user.id; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/you-need-a-budget/index.js b/packages/backend/src/apps/you-need-a-budget/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0cb69d26338fb23989f561dd26a5c34f2eeab2a7 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'You Need A Budget', + key: 'you-need-a-budget', + baseUrl: 'https://app.ynab.com', + apiBaseUrl: 'https://api.ynab.com/v1', + iconUrl: '{BASE_URL}/apps/you-need-a-budget/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/you-need-a-budget/connection', + primaryColor: '19223C', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, +}); diff --git a/packages/backend/src/apps/you-need-a-budget/triggers/category-overspent/index.js b/packages/backend/src/apps/you-need-a-budget/triggers/category-overspent/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9e3c953f26ad0ef8562a09450e6969114418cace --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/triggers/category-overspent/index.js @@ -0,0 +1,35 @@ +import { DateTime } from 'luxon'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'Category overspent', + key: 'categoryOverspent', + pollInterval: 15, + description: + 'Triggers when a category exceeds its budget, resulting in a negative balance.', + + async run($) { + const monthYear = DateTime.now().toFormat('MM-yyyy'); + const categoryWithNegativeBalance = []; + + const response = await $.http.get('/budgets/default/categories'); + const categoryGroups = response.data.data.category_groups; + + categoryGroups.forEach((group) => { + group.categories.forEach((category) => { + if (category.balance < 0) { + categoryWithNegativeBalance.push(category); + } + }); + }); + + for (const category of categoryWithNegativeBalance) { + $.pushTriggerItem({ + raw: category, + meta: { + internalId: `${category.id}-${monthYear}`, + }, + }); + } + }, +}); diff --git a/packages/backend/src/apps/you-need-a-budget/triggers/goal-completed/index.js b/packages/backend/src/apps/you-need-a-budget/triggers/goal-completed/index.js new file mode 100644 index 0000000000000000000000000000000000000000..53e878401697e42c4821ecdc93dc02e04b076558 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/triggers/goal-completed/index.js @@ -0,0 +1,34 @@ +import { DateTime } from 'luxon'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'Goal completed', + key: 'goalCompleted', + pollInterval: 15, + description: 'Triggers when a goal is completed.', + + async run($) { + const monthYear = DateTime.now().toFormat('MM-yyyy'); + const goalCompletedCategories = []; + + const response = await $.http.get('/budgets/default/categories'); + const categoryGroups = response.data.data.category_groups; + + categoryGroups.forEach((group) => { + group.categories.forEach((category) => { + if (category.goal_percentage_complete === 100) { + goalCompletedCategories.push(category); + } + }); + }); + + for (const category of goalCompletedCategories) { + $.pushTriggerItem({ + raw: category, + meta: { + internalId: `${category.id}-${monthYear}`, + }, + }); + } + }, +}); diff --git a/packages/backend/src/apps/you-need-a-budget/triggers/index.js b/packages/backend/src/apps/you-need-a-budget/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..81ac969437fb54cc8076562444c2feb450292610 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/triggers/index.js @@ -0,0 +1,11 @@ +import categoryOverspent from './category-overspent/index.js'; +import goalCompleted from './goal-completed/index.js'; +import lowAccountBalance from './low-account-balance/index.js'; +import newTransactions from './new-transactions/index.js'; + +export default [ + categoryOverspent, + goalCompleted, + lowAccountBalance, + newTransactions, +]; diff --git a/packages/backend/src/apps/you-need-a-budget/triggers/low-account-balance/index.js b/packages/backend/src/apps/you-need-a-budget/triggers/low-account-balance/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2eeacae368639ed7295b8ba7641d3ebc31c31900 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/triggers/low-account-balance/index.js @@ -0,0 +1,41 @@ +import { DateTime } from 'luxon'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'Low account balance', + key: 'lowAccountBalance', + pollInterval: 15, + description: + 'Triggers when the balance of a Checking or Savings account falls below a specified amount within a given month.', + arguments: [ + { + label: 'Balance Below Amount', + key: 'balanceBelowAmount', + type: 'string', + required: true, + description: 'Account balance falls below this amount (e.g. "250.00")', + variables: true, + }, + ], + + async run($) { + const monthYear = DateTime.now().toFormat('MM-yyyy'); + const balanceBelowAmount = $.step.parameters.balanceBelowAmount; + const formattedBalance = balanceBelowAmount * 1000; + + const response = await $.http.get('/budgets/default/accounts'); + + if (response.data?.data?.accounts?.length) { + for (const account of response.data.data.accounts) { + if (account.balance < formattedBalance) { + $.pushTriggerItem({ + raw: account, + meta: { + internalId: `${account.id}-${monthYear}`, + }, + }); + } + } + } + }, +}); diff --git a/packages/backend/src/apps/you-need-a-budget/triggers/new-transactions/index.js b/packages/backend/src/apps/you-need-a-budget/triggers/new-transactions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..00aa4c02fc88f57c199de1282344c4f027495fe5 --- /dev/null +++ b/packages/backend/src/apps/you-need-a-budget/triggers/new-transactions/index.js @@ -0,0 +1,24 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New transactions', + key: 'newTransactions', + pollInterval: 15, + description: 'Triggers when a new transaction is created.', + + async run($) { + const response = await $.http.get('/budgets/default/transactions'); + const transactions = response.data.data?.transactions; + + if (transactions?.length) { + for (const transaction of transactions) { + $.pushTriggerItem({ + raw: transaction, + meta: { + internalId: transaction.id, + }, + }); + } + } + }, +}); diff --git a/packages/backend/src/apps/youtube/assets/favicon.svg b/packages/backend/src/apps/youtube/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..e54d503dd5f4ac4132df24a2fb726c72051c4754 --- /dev/null +++ b/packages/backend/src/apps/youtube/assets/favicon.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/backend/src/apps/youtube/auth/generate-auth-url.js b/packages/backend/src/apps/youtube/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..093f34b700b1e295546e6dca8d286047ecc88a9a --- /dev/null +++ b/packages/backend/src/apps/youtube/auth/generate-auth-url.js @@ -0,0 +1,23 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId, + redirect_uri: redirectUri, + response_type: 'code', + scope: authScope.join(' '), + access_type: 'offline', + prompt: 'select_account', + }); + + const url = `https://accounts.google.com/o/oauth2/v2/auth?${searchParams.toString()}`; + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/youtube/auth/index.js b/packages/backend/src/apps/youtube/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0504b1dfacb6382b5f1113545d080cb669aa74ff --- /dev/null +++ b/packages/backend/src/apps/youtube/auth/index.js @@ -0,0 +1,48 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import refreshToken from './refresh-token.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/youtube/connections/add', + placeholder: null, + description: + 'When asked to input a redirect URL in Google Cloud, enter the URL above.', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, + refreshToken, +}; diff --git a/packages/backend/src/apps/youtube/auth/is-still-verified.js b/packages/backend/src/apps/youtube/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..68f4d7dbb92124636a90db76e67f5b6300e5916f --- /dev/null +++ b/packages/backend/src/apps/youtube/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + const currentUser = await getCurrentUser($); + return !!currentUser.resourceName; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/youtube/auth/refresh-token.js b/packages/backend/src/apps/youtube/auth/refresh-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7c5b7020e9ed24981513c8786ed786f136d476eb --- /dev/null +++ b/packages/backend/src/apps/youtube/auth/refresh-token.js @@ -0,0 +1,26 @@ +import { URLSearchParams } from 'node:url'; + +import authScope from '../common/auth-scope.js'; + +const refreshToken = async ($) => { + const params = new URLSearchParams({ + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + grant_type: 'refresh_token', + refresh_token: $.auth.data.refreshToken, + }); + + const { data } = await $.http.post( + 'https://oauth2.googleapis.com/token', + params.toString() + ); + + await $.auth.set({ + accessToken: data.access_token, + expiresIn: data.expires_in, + scope: authScope.join(' '), + tokenType: data.token_type, + }); +}; + +export default refreshToken; diff --git a/packages/backend/src/apps/youtube/auth/verify-credentials.js b/packages/backend/src/apps/youtube/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..a636b72cc97498a76fd91ff991025443bd70b29b --- /dev/null +++ b/packages/backend/src/apps/youtube/auth/verify-credentials.js @@ -0,0 +1,42 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const verifyCredentials = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + const { data } = await $.http.post(`https://oauth2.googleapis.com/token`, { + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + code: $.auth.data.code, + grant_type: 'authorization_code', + redirect_uri: redirectUri, + }); + + await $.auth.set({ + accessToken: data.access_token, + tokenType: data.token_type, + }); + + const currentUser = await getCurrentUser($); + + const { displayName } = currentUser.names.find( + (name) => name.metadata.primary + ); + const { value: email } = currentUser.emailAddresses.find( + (emailAddress) => emailAddress.metadata.primary + ); + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + scope: $.auth.data.scope, + idToken: data.id_token, + expiresIn: data.expires_in, + refreshToken: data.refresh_token, + resourceName: currentUser.resourceName, + screenName: `${displayName} - ${email}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/youtube/common/add-auth-header.js b/packages/backend/src/apps/youtube/common/add-auth-header.js new file mode 100644 index 0000000000000000000000000000000000000000..02477aa41be1e755fc3f9d3c89b69de845b10482 --- /dev/null +++ b/packages/backend/src/apps/youtube/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.accessToken) { + requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/youtube/common/auth-scope.js b/packages/backend/src/apps/youtube/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..fdee5490f30e23ff5c1bf34cf5735253cb5c8398 --- /dev/null +++ b/packages/backend/src/apps/youtube/common/auth-scope.js @@ -0,0 +1,9 @@ +const authScope = [ + 'https://www.googleapis.com/auth/youtube', + 'https://www.googleapis.com/auth/youtube.readonly', + 'https://www.googleapis.com/auth/youtube.upload', + 'https://www.googleapis.com/auth/userinfo.email', + 'https://www.googleapis.com/auth/userinfo.profile', +]; + +export default authScope; diff --git a/packages/backend/src/apps/youtube/common/get-current-user.js b/packages/backend/src/apps/youtube/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..2663ad208ea29495da7eadb60d1003e3870dc3ae --- /dev/null +++ b/packages/backend/src/apps/youtube/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const { data: currentUser } = await $.http.get( + 'https://people.googleapis.com/v1/people/me?personFields=names,emailAddresses' + ); + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/youtube/index.js b/packages/backend/src/apps/youtube/index.js new file mode 100644 index 0000000000000000000000000000000000000000..03eba51c7310c83ce98643e5277fd346ecf007b3 --- /dev/null +++ b/packages/backend/src/apps/youtube/index.js @@ -0,0 +1,18 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'Youtube', + key: 'youtube', + baseUrl: 'https://www.youtube.com/', + apiBaseUrl: 'https://www.googleapis.com/youtube', + iconUrl: '{BASE_URL}/apps/youtube/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/youtube/connection', + primaryColor: 'FF0000', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, +}); diff --git a/packages/backend/src/apps/youtube/triggers/index.js b/packages/backend/src/apps/youtube/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e5b4e02a83b041d4477fdb9d22b45bc5b35e9e2a --- /dev/null +++ b/packages/backend/src/apps/youtube/triggers/index.js @@ -0,0 +1,4 @@ +import newVideoInChannel from './new-video-in-channel/index.js'; +import newVideoBySearch from './new-video-by-search/index.js'; + +export default [newVideoBySearch, newVideoInChannel]; diff --git a/packages/backend/src/apps/youtube/triggers/new-video-by-search/index.js b/packages/backend/src/apps/youtube/triggers/new-video-by-search/index.js new file mode 100644 index 0000000000000000000000000000000000000000..13ed2733eef3c5abbc383208d2be135efca61773 --- /dev/null +++ b/packages/backend/src/apps/youtube/triggers/new-video-by-search/index.js @@ -0,0 +1,47 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New video by search', + key: 'newVideoBySearch', + description: + 'Triggers when a new video is uploaded that matches a specific search string.', + arguments: [ + { + label: 'Query', + key: 'query', + type: 'string', + required: true, + description: 'Search for videos that match this query.', + variables: true, + }, + ], + + async run($) { + const query = $.step.parameters.query; + + const params = { + pageToken: undefined, + part: 'snippet', + q: query, + maxResults: 50, + order: 'date', + type: 'video', + }; + + do { + const { data } = await $.http.get('/v3/search', { params }); + params.pageToken = data.nextPageToken; + + if (data?.items?.length) { + for (const item of data.items) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.etag, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/youtube/triggers/new-video-in-channel/index.js b/packages/backend/src/apps/youtube/triggers/new-video-in-channel/index.js new file mode 100644 index 0000000000000000000000000000000000000000..2bcc51a54b9312c456d4561b3573f64b5a3f4bab --- /dev/null +++ b/packages/backend/src/apps/youtube/triggers/new-video-in-channel/index.js @@ -0,0 +1,48 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New video in channel', + key: 'newVideoInChannel', + description: + 'Triggers when a new video is published to a specific Youtube channel.', + arguments: [ + { + label: 'Channel', + key: 'channelId', + type: 'string', + required: true, + description: + 'Get the new videos uploaded to this channel. If the URL of the youtube channel looks like this www.youtube.com/channel/UCbxb2fqe9oNgglAoYqsYOtQ then you must use UCbxb2fqe9oNgglAoYqsYOtQ as a value in this field.', + variables: true, + }, + ], + + async run($) { + const channelId = $.step.parameters.channelId; + + const params = { + pageToken: undefined, + part: 'snippet', + channelId: channelId, + maxResults: 50, + order: 'date', + type: 'video', + }; + + do { + const { data } = await $.http.get('/v3/search', { params }); + params.pageToken = data.nextPageToken; + + if (data?.items?.length) { + for (const item of data.items) { + $.pushTriggerItem({ + raw: item, + meta: { + internalId: item.etag, + }, + }); + } + } + } while (params.pageToken); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/create-ticket/fields.js b/packages/backend/src/apps/zendesk/actions/create-ticket/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..e40da2be743c5344a903dba07ceee9270269e0bf --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/create-ticket/fields.js @@ -0,0 +1,301 @@ +export const fields = [ + { + label: 'Subject', + key: 'subject', + type: 'string', + required: true, + variables: true, + description: '', + }, + { + label: 'Assignee', + key: 'assigneeId', + type: 'dropdown', + required: false, + variables: true, + description: + 'Note: An error occurs if the assignee is not in the default group (or the specific group chosen below).', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.showUserRole', + value: 'true', + }, + { + name: 'parameters.includeAdmins', + value: 'true', + }, + ], + }, + }, + { + label: 'Collaborators', + key: 'collaborators', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Collaborator', + key: 'collaborator', + type: 'dropdown', + required: false, + variables: true, + description: '', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.includeAdmins', + value: 'true', + }, + ], + }, + }, + ], + }, + { + label: 'Collaborator Emails', + key: 'collaboratorEmails', + type: 'dynamic', + required: false, + description: + 'You have the option to include individuals who are not Zendesk users as Collaborators by adding their email addresses here.', + fields: [ + { + label: 'Collaborator Email', + key: 'collaboratorEmail', + type: 'string', + required: false, + variables: true, + description: '', + }, + ], + }, + { + label: 'Group', + key: 'groupId', + type: 'dropdown', + required: false, + variables: true, + description: 'Allocate this ticket to a specific group.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listGroups', + }, + ], + }, + }, + { + label: 'Requester Name', + key: 'requesterName', + type: 'string', + required: false, + variables: true, + description: + 'To specify the Requester, you need to fill in the Requester Name in this field and provide the Requestor Email in the next field.', + }, + { + label: 'Requester Email', + key: 'requesterEmail', + type: 'string', + required: false, + variables: true, + description: + 'To specify the Requester, you need to fill in the Requester Email in this field and provide the Requestor Name in the previous field.', + }, + { + label: 'First Comment/Description Format', + key: 'format', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Plain Text', value: 'Plain Text' }, + { label: 'HTML', value: 'HTML' }, + ], + }, + { + label: 'First Comment/Description', + key: 'comment', + type: 'string', + required: true, + variables: true, + description: '', + }, + { + label: 'Should the first comment be public?', + key: 'publicOrNot', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ], + }, + { + label: 'Tags', + key: 'tags', + type: 'string', + required: false, + variables: true, + description: 'A comma separated list of tags.', + }, + { + label: 'Status', + key: 'status', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'New', value: 'new' }, + { label: 'Open', value: 'open' }, + { label: 'Pending', value: 'pending' }, + { label: 'Hold', value: 'hold' }, + { label: 'Solved', value: 'solved' }, + { label: 'Closed', value: 'closed' }, + ], + }, + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Problem', value: 'problem' }, + { label: 'Incident', value: 'incident' }, + { label: 'Question', value: 'question' }, + { label: 'Task', value: 'task' }, + ], + }, + { + label: 'Due At', + key: 'dueAt', + type: 'string', + required: false, + variables: true, + description: 'Limited to tickets typed as "task".', + }, + { + label: 'Priority', + key: 'priority', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Urgent', value: 'urgent' }, + { label: 'High', value: 'high' }, + { label: 'Normal', value: 'normal' }, + { label: 'Low', value: 'low' }, + ], + }, + { + label: 'Submitter', + key: 'submitterId', + type: 'dropdown', + required: false, + variables: true, + description: '', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.includeAdmins', + value: 'false', + }, + ], + }, + }, + { + label: 'Ticket Form', + key: 'ticketForm', + type: 'dropdown', + required: false, + variables: true, + description: + 'When chosen, this will configure the form displayed for this ticket. Note: This field is solely relevant for Zendesk enterprise accounts.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listTicketForms', + }, + ], + }, + }, + { + label: 'Sharing Agreements', + key: 'sharingAgreements', + type: 'dynamic', + required: false, + description: '', + fields: [ + { + label: 'Sharing Agreement', + key: 'sharingAgreement', + type: 'dropdown', + required: false, + variables: true, + description: '', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listSharingAgreements', + }, + ], + }, + }, + ], + }, + { + label: 'Brand', + key: 'brandId', + type: 'dropdown', + required: false, + variables: true, + description: + 'This applies exclusively to Zendesk customers subscribed to plans that include multi-brand support.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listBrands', + }, + ], + }, + }, +]; diff --git a/packages/backend/src/apps/zendesk/actions/create-ticket/index.js b/packages/backend/src/apps/zendesk/actions/create-ticket/index.js new file mode 100644 index 0000000000000000000000000000000000000000..05e737eb3bfcbe103f7bbfe040e5ac768ed47ecb --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/create-ticket/index.js @@ -0,0 +1,93 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; +import isEmpty from 'lodash/isEmpty.js'; + +export default defineAction({ + name: 'Create ticket', + key: 'createTicket', + description: 'Creates a new ticket', + arguments: fields, + + async run($) { + const { + subject, + assigneeId, + groupId, + requesterName, + requesterEmail, + format, + comment, + publicOrNot, + status, + type, + dueAt, + priority, + submitterId, + ticketForm, + brandId, + } = $.step.parameters; + + const collaborators = $.step.parameters.collaborators; + const collaboratorIds = collaborators?.map( + (collaborator) => collaborator.collaborator + ); + + const collaboratorEmails = $.step.parameters.collaboratorEmails; + const formattedCollaboratorEmails = collaboratorEmails?.map( + (collaboratorEmail) => collaboratorEmail.collaboratorEmail + ); + + const formattedCollaborators = [ + ...collaboratorIds, + ...formattedCollaboratorEmails, + ]; + + const sharingAgreements = $.step.parameters.sharingAgreements; + const sharingAgreementIds = sharingAgreements + ?.filter(isEmpty) + .map((sharingAgreement) => Number(sharingAgreement.sharingAgreement)); + + const tags = $.step.parameters.tags; + const formattedTags = tags.split(','); + + const payload = { + ticket: { + subject, + assignee_id: assigneeId, + collaborators: formattedCollaborators, + group_id: groupId, + is_public: publicOrNot, + tags: formattedTags, + status, + type, + due_at: dueAt, + priority, + submitter_id: submitterId, + ticket_form_id: ticketForm, + sharing_agreement_ids: sharingAgreementIds, + brand_id: brandId, + }, + }; + + if (requesterName && requesterEmail) { + payload.ticket.requester = { + name: requesterName, + email: requesterEmail, + }; + } + + if (format === 'HTML') { + payload.ticket.comment = { + html_body: comment, + }; + } else { + payload.ticket.comment = { + body: comment, + }; + } + + const response = await $.http.post('/api/v2/tickets', payload); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/create-user/fields.js b/packages/backend/src/apps/zendesk/actions/create-user/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..1629a860db4e1d9b72474e05a28edf295feb9242 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/create-user/fields.js @@ -0,0 +1,102 @@ +export const fields = [ + { + label: 'Name', + key: 'name', + type: 'string', + required: true, + variables: true, + description: '', + }, + { + label: 'Email', + key: 'email', + type: 'string', + required: true, + variables: true, + description: + 'It is essential to be distinctive. Zendesk prohibits the existence of identical users sharing the same email address.', + }, + { + label: 'Details', + key: 'details', + type: 'string', + required: false, + variables: true, + description: '', + }, + { + label: 'Notes', + key: 'notes', + type: 'string', + required: false, + variables: true, + description: + 'Within this field, you have the capability to save any remarks or comments you may have concerning the user.', + }, + { + label: 'Phone', + key: 'phone', + type: 'string', + required: false, + variables: true, + description: + "The user's contact number should be entered in the following format: +1 (555) 123-4567.", + }, + { + label: 'Tags', + key: 'tags', + type: 'string', + required: false, + variables: true, + description: 'A comma separated list of tags.', + }, + { + label: 'Role', + key: 'role', + type: 'string', + required: false, + variables: true, + description: + "It can take on one of the designated roles: 'end-user', 'agent', or 'admin'. If a different value is set or none is specified, the default is 'end-user.'", + }, + { + label: 'Organization', + key: 'organizationId', + type: 'dropdown', + required: false, + variables: true, + description: 'Assign this user to a specific organization.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listOrganizations', + }, + ], + }, + }, + { + label: 'External Id', + key: 'externalId', + type: 'string', + required: false, + variables: true, + description: + 'An exclusive external identifier; you can utilize this to link organizations with an external record.', + }, + { + label: 'Verified', + key: 'verified', + type: 'dropdown', + required: false, + description: + "Specify if you can verify that the user's assertion of their identity is accurate.", + variables: true, + options: [ + { label: 'True', value: 'true' }, + { label: 'False', value: 'false' }, + ], + }, +]; diff --git a/packages/backend/src/apps/zendesk/actions/create-user/index.js b/packages/backend/src/apps/zendesk/actions/create-user/index.js new file mode 100644 index 0000000000000000000000000000000000000000..3a02ddf98226fde756a75417b5d88d6d4c574893 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/create-user/index.js @@ -0,0 +1,48 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; + +export default defineAction({ + name: 'Create user', + key: 'createUser', + description: 'Creates a new user.', + arguments: fields, + + async run($) { + const { + name, + email, + details, + notes, + phone, + role, + organizationId, + externalId, + verified, + } = $.step.parameters; + + const tags = $.step.parameters.tags; + const formattedTags = tags.split(','); + + const payload = { + user: { + name, + email, + details, + notes, + phone, + organization_id: organizationId, + external_id: externalId, + verified: verified || 'false', + tags: formattedTags, + }, + }; + + if (role) { + payload.user.role = role; + } + + const response = await $.http.post('/api/v2/users', payload); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/delete-ticket/index.js b/packages/backend/src/apps/zendesk/actions/delete-ticket/index.js new file mode 100644 index 0000000000000000000000000000000000000000..52ebeb9f13ff9a1bff356e7268b4a11aa894d1d4 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/delete-ticket/index.js @@ -0,0 +1,35 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Delete ticket', + key: 'deleteTicket', + description: 'Deletes an existing ticket.', + arguments: [ + { + label: 'Ticket', + key: 'ticketId', + type: 'dropdown', + required: true, + variables: true, + description: 'Select the ticket you want to delete.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFirstPageOfTickets', + }, + ], + }, + }, + ], + + async run($) { + const ticketId = $.step.parameters.ticketId; + + const response = await $.http.delete(`/api/v2/tickets/${ticketId}`); + + $.setActionItem({ raw: { data: response.data } }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/delete-user/index.js b/packages/backend/src/apps/zendesk/actions/delete-user/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8e4fbfd6c88ad6e655c6e553b1a1990147bd08cd --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/delete-user/index.js @@ -0,0 +1,43 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Delete user', + key: 'deleteUser', + description: 'Deletes an existing user.', + arguments: [ + { + label: 'User', + key: 'userId', + type: 'dropdown', + required: true, + variables: true, + description: 'Select the user you want to modify.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.showUserRole', + value: 'true', + }, + { + name: 'parameters.includeAllUsers', + value: 'true', + }, + ], + }, + }, + ], + + async run($) { + const userId = $.step.parameters.userId; + + const response = await $.http.delete(`/api/v2/users/${userId}`); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/find-ticket/index.js b/packages/backend/src/apps/zendesk/actions/find-ticket/index.js new file mode 100644 index 0000000000000000000000000000000000000000..40f6eec399c546a00a868d23b085e6395cf7a9f7 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/find-ticket/index.js @@ -0,0 +1,32 @@ +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Find ticket', + key: 'findTicket', + description: 'Finds an existing ticket.', + arguments: [ + { + label: 'Query', + key: 'query', + type: 'string', + required: true, + variables: true, + description: + 'Write a search string that specifies the way we will search for the ticket in Zendesk.', + }, + ], + + async run($) { + const query = $.step.parameters.query; + + const params = { + query: `type:ticket ${query}`, + sort_by: 'created_at', + sort_order: 'desc', + }; + + const response = await $.http.get('/api/v2/search', { params }); + + $.setActionItem({ raw: response.data.results[0] }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/actions/index.js b/packages/backend/src/apps/zendesk/actions/index.js new file mode 100644 index 0000000000000000000000000000000000000000..23079139567f738135b274e8a63a1ac57cc5c4a3 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/index.js @@ -0,0 +1,15 @@ +import createTicket from './create-ticket/index.js'; +import createUser from './create-user/index.js'; +import deleteTicket from './delete-ticket/index.js'; +import deleteUser from './delete-user/index.js'; +import findTicket from './find-ticket/index.js'; +import updateTicket from './update-ticket/index.js'; + +export default [ + createTicket, + createUser, + deleteTicket, + deleteUser, + findTicket, + updateTicket, +]; diff --git a/packages/backend/src/apps/zendesk/actions/update-ticket/fields.js b/packages/backend/src/apps/zendesk/actions/update-ticket/fields.js new file mode 100644 index 0000000000000000000000000000000000000000..29f646c7fee33f62a07313c7d2c17c9978d1b986 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/update-ticket/fields.js @@ -0,0 +1,167 @@ +export const fields = [ + { + label: 'Ticket', + key: 'ticketId', + type: 'dropdown', + required: true, + variables: true, + description: 'Select the ticket you want to change.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listFirstPageOfTickets', + }, + ], + }, + }, + { + label: 'Subject', + key: 'subject', + type: 'string', + required: false, + variables: true, + description: '', + }, + { + label: 'Assignee', + key: 'assigneeId', + type: 'dropdown', + required: false, + variables: true, + description: + 'Note: An error occurs if the assignee is not in the default group (or the specific group chosen below).', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.showUserRole', + value: 'true', + }, + { + name: 'parameters.includeAdmins', + value: 'true', + }, + ], + }, + }, + { + label: 'Group', + key: 'groupId', + type: 'dropdown', + required: false, + variables: true, + description: 'Allocate this ticket to a specific group.', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listGroups', + }, + ], + }, + }, + { + label: 'New Status', + key: 'status', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'New', value: 'new' }, + { label: 'Open', value: 'open' }, + { label: 'Pending', value: 'pending' }, + { label: 'Hold', value: 'hold' }, + { label: 'Solved', value: 'solved' }, + { label: 'Closed', value: 'closed' }, + ], + }, + { + label: 'New comment to add to the ticket', + key: 'comment', + type: 'string', + required: false, + variables: true, + description: '', + }, + { + label: 'Should the first comment be public?', + key: 'publicOrNot', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Yes', value: 'yes' }, + { label: 'No', value: 'no' }, + ], + }, + { + label: 'Tags', + key: 'tags', + type: 'string', + required: false, + variables: true, + description: 'A comma separated list of tags.', + }, + { + label: 'Type', + key: 'type', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Problem', value: 'problem' }, + { label: 'Incident', value: 'incident' }, + { label: 'Question', value: 'question' }, + { label: 'Task', value: 'task' }, + ], + }, + { + label: 'Priority', + key: 'priority', + type: 'dropdown', + required: false, + variables: true, + description: '', + options: [ + { label: 'Urgent', value: 'urgent' }, + { label: 'High', value: 'high' }, + { label: 'Normal', value: 'normal' }, + { label: 'Low', value: 'low' }, + ], + }, + { + label: 'Submitter', + key: 'submitterId', + type: 'dropdown', + required: false, + variables: true, + description: '', + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listUsers', + }, + { + name: 'parameters.includeAdmins', + value: 'false', + }, + ], + }, + }, +]; diff --git a/packages/backend/src/apps/zendesk/actions/update-ticket/index.js b/packages/backend/src/apps/zendesk/actions/update-ticket/index.js new file mode 100644 index 0000000000000000000000000000000000000000..cff7e0eec15ce5cb146193029341c7f3e24651a2 --- /dev/null +++ b/packages/backend/src/apps/zendesk/actions/update-ticket/index.js @@ -0,0 +1,57 @@ +import defineAction from '../../../../helpers/define-action.js'; +import { fields } from './fields.js'; +import isEmpty from 'lodash/isEmpty.js'; +import omitBy from 'lodash/omitBy.js'; + +export default defineAction({ + name: 'Update ticket', + key: 'updateTicket', + description: 'Modify the status of an existing ticket or append comments.', + arguments: fields, + + async run($) { + const { + ticketId, + subject, + assigneeId, + groupId, + status, + comment, + publicOrNot, + type, + priority, + submitterId, + } = $.step.parameters; + + const tags = $.step.parameters.tags; + const formattedTags = tags.split(','); + + const payload = { + subject, + assignee_id: assigneeId, + group_id: groupId, + status, + comment: { + body: comment, + public: publicOrNot, + }, + tags: formattedTags, + type, + priority, + submitter_id: submitterId, + }; + + const fieldsToRemoveIfEmpty = ['group_id', 'status', 'type', 'priority']; + + const filteredPayload = omitBy( + payload, + (value, key) => fieldsToRemoveIfEmpty.includes(key) && isEmpty(value) + ); + + const response = await $.http.put(`/api/v2/tickets/${ticketId}`, { + ticket: filteredPayload, + }); + + $.setActionItem({ raw: response.data }); + }, +}); diff --git a/packages/backend/src/apps/zendesk/assets/favicon.svg b/packages/backend/src/apps/zendesk/assets/favicon.svg new file mode 100644 index 0000000000000000000000000000000000000000..882b45164c556e9c1eb20ab7ee7dbf8274270b15 --- /dev/null +++ b/packages/backend/src/apps/zendesk/assets/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/backend/src/apps/zendesk/auth/generate-auth-url.js b/packages/backend/src/apps/zendesk/auth/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..9e93282dc2bdef5379bace5a5ee80d2b5dcdbb6d --- /dev/null +++ b/packages/backend/src/apps/zendesk/auth/generate-auth-url.js @@ -0,0 +1,22 @@ +import { URLSearchParams } from 'url'; +import authScope from '../common/auth-scope.js'; + +export default async function generateAuthUrl($) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + + const redirectUri = oauthRedirectUrlField.value; + const searchParams = new URLSearchParams({ + response_type: 'code', + redirect_uri: redirectUri, + client_id: $.auth.data.clientId, + scope: authScope.join(' '), + }); + + await $.auth.set({ + url: `${ + $.auth.data.instanceUrl + }/oauth/authorizations/new?${searchParams.toString()}`, + }); +} diff --git a/packages/backend/src/apps/zendesk/auth/index.js b/packages/backend/src/apps/zendesk/auth/index.js new file mode 100644 index 0000000000000000000000000000000000000000..67df59a981530f80e3c50306472f332befad0cf9 --- /dev/null +++ b/packages/backend/src/apps/zendesk/auth/index.js @@ -0,0 +1,55 @@ +import generateAuthUrl from './generate-auth-url.js'; +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string', + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/zendesk/connections/add', + placeholder: null, + description: '', + clickToCopy: true, + }, + { + key: 'instanceUrl', + label: 'Zendesk Subdomain Url', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: 'https://{{subdomain}}.zendesk.com', + clickToCopy: false, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/zendesk/auth/is-still-verified.js b/packages/backend/src/apps/zendesk/auth/is-still-verified.js new file mode 100644 index 0000000000000000000000000000000000000000..5400157947e5655704bcdf5be02465da7415d24d --- /dev/null +++ b/packages/backend/src/apps/zendesk/auth/is-still-verified.js @@ -0,0 +1,8 @@ +import getCurrentUser from '../common/get-current-user.js'; + +const isStillVerified = async ($) => { + await getCurrentUser($); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/zendesk/auth/verify-credentials.js b/packages/backend/src/apps/zendesk/auth/verify-credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..3a416a69801e9b51727a06289725bd86f73913cd --- /dev/null +++ b/packages/backend/src/apps/zendesk/auth/verify-credentials.js @@ -0,0 +1,55 @@ +import getCurrentUser from '../common/get-current-user.js'; +import scopes from '../common/auth-scope.js'; + +const verifyCredentials = async ($) => { + await getAccessToken($); + + const user = await getCurrentUser($); + const subdomain = extractSubdomain($.auth.data.instanceUrl); + const name = user.name; + const screenName = [name, subdomain].filter(Boolean).join(' @ '); + + await $.auth.set({ + screenName, + apiToken: $.auth.data.apiToken, + instanceUrl: $.auth.data.instanceUrl, + email: $.auth.data.email, + }); +}; + +const getAccessToken = async ($) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value; + + const response = await $.http.post(`/oauth/tokens`, { + redirect_uri: redirectUri, + code: $.auth.data.code, + grant_type: 'authorization_code', + scope: scopes.join(' '), + client_id: $.auth.data.clientId, + client_secret: $.auth.data.clientSecret, + }); + + const data = response.data; + + $.auth.data.accessToken = data.access_token; + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: data.access_token, + tokenType: data.token_type, + }); +}; + +function extractSubdomain(url) { + const match = url.match(/https:\/\/(.*?)\.zendesk\.com/); + if (match && match[1]) { + return match[1]; + } + return null; +} + +export default verifyCredentials; diff --git a/packages/backend/src/apps/zendesk/common/add-auth-headers.js b/packages/backend/src/apps/zendesk/common/add-auth-headers.js new file mode 100644 index 0000000000000000000000000000000000000000..96a0e04c9c914e47f6ce3fd5d0f7b90de931dee4 --- /dev/null +++ b/packages/backend/src/apps/zendesk/common/add-auth-headers.js @@ -0,0 +1,15 @@ +const addAuthHeader = ($, requestConfig) => { + const { instanceUrl, tokenType, accessToken } = $.auth.data; + + if (instanceUrl) { + requestConfig.baseURL = instanceUrl; + } + + if (tokenType && accessToken) { + requestConfig.headers.Authorization = `${tokenType} ${$.auth.data.accessToken}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/zendesk/common/auth-scope.js b/packages/backend/src/apps/zendesk/common/auth-scope.js new file mode 100644 index 0000000000000000000000000000000000000000..855e337b17d3679aa722ca56674417958adc44cc --- /dev/null +++ b/packages/backend/src/apps/zendesk/common/auth-scope.js @@ -0,0 +1,3 @@ +const authScope = ['read', 'write']; + +export default authScope; diff --git a/packages/backend/src/apps/zendesk/common/get-current-user.js b/packages/backend/src/apps/zendesk/common/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..5741b02ce4de26805009ae17cf87e340834226d1 --- /dev/null +++ b/packages/backend/src/apps/zendesk/common/get-current-user.js @@ -0,0 +1,8 @@ +const getCurrentUser = async ($) => { + const response = await $.http.get('/api/v2/users/me'); + const currentUser = response.data.user; + + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/index.js b/packages/backend/src/apps/zendesk/dynamic-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..d32b6fb3f9f9037eb157ae201e84d364704a8959 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/index.js @@ -0,0 +1,20 @@ +import listUsers from './list-users/index.js'; +import listBrands from './list-brands/index.js'; +import listFirstPageOfTickets from './list-first-page-of-tickets/index.js'; +import listGroups from './list-groups/index.js'; +import listOrganizations from './list-organizations/index.js'; +import listSharingAgreements from './list-sharing-agreements/index.js'; +import listTicketForms from './list-ticket-forms/index.js'; +import listViews from './list-views/index.js'; + +export default [ + listUsers, + listBrands, + listFirstPageOfTickets, + listGroups, + listOrganizations, + listSharingAgreements, + listFirstPageOfTickets, + listTicketForms, + listViews, +]; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-brands/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-brands/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a5a170c629805f45121fb909944522dacc7ec9c7 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-brands/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List brands', + key: 'listBrands', + + async run($) { + const brands = { + data: [], + }; + + const params = { + page: 1, + per_page: 100, + }; + + let nextPage; + do { + const response = await $.http.get('/api/v2/brands', { params }); + const allBrands = response?.data?.brands; + nextPage = response.data.next_page; + params.page = params.page + 1; + + if (allBrands?.length) { + for (const brand of allBrands) { + brands.data.push({ + value: brand.id, + name: brand.name, + }); + } + } + } while (nextPage); + + return brands; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-first-page-of-tickets/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-first-page-of-tickets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..bf065a5bfecfb3b992832ba5811f5d95b2a74094 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-first-page-of-tickets/index.js @@ -0,0 +1,29 @@ +export default { + name: 'List first page of tickets', + key: 'listFirstPageOfTickets', + + async run($) { + const tickets = { + data: [], + }; + + const params = { + 'page[size]': 100, + sort: '-id', + }; + + const response = await $.http.get('/api/v2/tickets', { params }); + const allTickets = response.data.tickets; + + if (allTickets?.length) { + for (const ticket of allTickets) { + tickets.data.push({ + value: ticket.id, + name: ticket.subject, + }); + } + } + + return tickets; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-groups/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-groups/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9fda32d8c2e4768c6e65e8ebefcf0db9710a48dd --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-groups/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List groups', + key: 'listGroups', + + async run($) { + const groups = { + data: [], + }; + let hasMore; + + const params = { + 'page[size]': 100, + 'page[after]': undefined, + }; + + do { + const response = await $.http.get('/api/v2/groups', { params }); + const allGroups = response?.data?.groups; + hasMore = response?.data?.meta?.has_more; + params['page[after]'] = response.data.meta?.after_cursor; + + if (allGroups?.length) { + for (const group of allGroups) { + groups.data.push({ + value: group.id, + name: group.name, + }); + } + } + } while (hasMore); + + return groups; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-organizations/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-organizations/index.js new file mode 100644 index 0000000000000000000000000000000000000000..a6c7a4db9023e34dd4df9a6fe6d288988639cf04 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-organizations/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List organizations', + key: 'listOrganizations', + + async run($) { + const organizations = { + data: [], + }; + let hasMore; + + const params = { + 'page[size]': 100, + 'page[after]': undefined, + }; + + do { + const response = await $.http.get('/api/v2/organizations', { params }); + const allOrganizations = response?.data?.organizations; + hasMore = response?.data?.meta?.has_more; + params['page[after]'] = response.data.meta?.after_cursor; + + if (allOrganizations?.length) { + for (const organization of allOrganizations) { + organizations.data.push({ + value: organization.id, + name: organization.name, + }); + } + } + } while (hasMore); + + return organizations; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-sharing-agreements/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-sharing-agreements/index.js new file mode 100644 index 0000000000000000000000000000000000000000..690f46e1f715c3e4f5a88e09a18ee399aeed7599 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-sharing-agreements/index.js @@ -0,0 +1,36 @@ +export default { + name: 'List sharing agreements', + key: 'listSharingAgreements', + + async run($) { + const sharingAgreements = { + data: [], + }; + + const params = { + page: 1, + per_page: 100, + }; + + let nextPage; + do { + const response = await $.http.get('/api/v2/sharing_agreements', { + params, + }); + const allSharingAgreements = response?.data?.sharing_agreements; + nextPage = response.data.next_page; + params.page = params.page + 1; + + if (allSharingAgreements?.length) { + for (const sharingAgreement of allSharingAgreements) { + sharingAgreements.data.push({ + value: sharingAgreement.id, + name: sharingAgreement.name, + }); + } + } + } while (nextPage); + + return sharingAgreements; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-ticket-forms/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-ticket-forms/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ad102b9397653983927c10046d23ee8a5f210209 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-ticket-forms/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List ticket forms', + key: 'listTicketForms', + + async run($) { + const ticketForms = { + data: [], + }; + + const params = { + page: 1, + per_page: 100, + }; + + let nextPage; + do { + const response = await $.http.get('/api/v2/ticket_forms', { params }); + const allTicketForms = response?.data?.ticket_forms; + nextPage = response.data.next_page; + params.page = params.page + 1; + + if (allTicketForms?.length) { + for (const ticketForm of allTicketForms) { + ticketForms.data.push({ + value: ticketForm.id, + name: ticketForm.name, + }); + } + } + } while (nextPage); + + return ticketForms; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-users/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-users/index.js new file mode 100644 index 0000000000000000000000000000000000000000..efcafbbd850ac812bbf5cee179c79110941bfaa6 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-users/index.js @@ -0,0 +1,39 @@ +export default { + name: 'List users', + key: 'listUsers', + + async run($) { + const users = { + data: [], + }; + let hasMore; + const showUserRole = $.step.parameters.showUserRole === 'true'; + const includeAdmins = $.step.parameters.includeAdmins === 'true'; + const role = includeAdmins ? ['admin', 'agent'] : ['agent']; + + const params = { + 'page[size]': 100, + role, + 'page[after]': undefined, + }; + + do { + const response = await $.http.get('/api/v2/users', { params }); + const allUsers = response?.data?.users; + hasMore = response?.data?.meta?.has_more; + params['page[after]'] = response.data.meta?.after_cursor; + + if (allUsers?.length) { + for (const user of allUsers) { + const name = showUserRole ? `${user.name} ${user.role}` : user.name; + users.data.push({ + value: user.id, + name, + }); + } + } + } while (hasMore); + + return users; + }, +}; diff --git a/packages/backend/src/apps/zendesk/dynamic-data/list-views/index.js b/packages/backend/src/apps/zendesk/dynamic-data/list-views/index.js new file mode 100644 index 0000000000000000000000000000000000000000..fd7066ae6923d331f1791b2f632b7e2f651aa8a9 --- /dev/null +++ b/packages/backend/src/apps/zendesk/dynamic-data/list-views/index.js @@ -0,0 +1,34 @@ +export default { + name: 'List views', + key: 'listViews', + + async run($) { + const views = { + data: [], + }; + let hasMore; + + const params = { + 'page[size]': 100, + 'page[after]': undefined, + }; + + do { + const response = await $.http.get('/api/v2/views', { params }); + const allViews = response?.data?.views; + hasMore = response?.data?.meta?.has_more; + params['page[after]'] = response.data.meta?.after_cursor; + + if (allViews?.length) { + for (const view of allViews) { + views.data.push({ + value: view.id, + name: view.title, + }); + } + } + } while (hasMore); + + return views; + }, +}; diff --git a/packages/backend/src/apps/zendesk/index.js b/packages/backend/src/apps/zendesk/index.js new file mode 100644 index 0000000000000000000000000000000000000000..5e66a3b0d7deb464e1de8527b8ea04250dfec6cf --- /dev/null +++ b/packages/backend/src/apps/zendesk/index.js @@ -0,0 +1,22 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-headers.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'Zendesk', + key: 'zendesk', + baseUrl: 'https://zendesk.com/', + apiBaseUrl: '', + iconUrl: '{BASE_URL}/apps/zendesk/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/zendesk/connection', + primaryColor: '17494d', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + triggers, + actions, + dynamicData, +}); diff --git a/packages/backend/src/apps/zendesk/triggers/index.js b/packages/backend/src/apps/zendesk/triggers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0006b367d96aa7cf5c97b05dc3de2582d60d909b --- /dev/null +++ b/packages/backend/src/apps/zendesk/triggers/index.js @@ -0,0 +1,4 @@ +import newTickets from './new-tickets/index.js'; +import newUsers from './new-users/index.js'; + +export default [newTickets, newUsers]; diff --git a/packages/backend/src/apps/zendesk/triggers/new-tickets/index.js b/packages/backend/src/apps/zendesk/triggers/new-tickets/index.js new file mode 100644 index 0000000000000000000000000000000000000000..31156a16875bbf8e0640e35b04e6282e19b64afc --- /dev/null +++ b/packages/backend/src/apps/zendesk/triggers/new-tickets/index.js @@ -0,0 +1,59 @@ +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New tickets', + key: 'newTickets', + pollInterval: 15, + description: 'Triggers when a new ticket is created in a specific view.', + arguments: [ + { + label: 'View', + key: 'viewId', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listViews', + }, + ], + }, + }, + ], + + async run($) { + const viewId = $.step.parameters.viewId; + + const params = { + 'page[size]': 100, + 'page[after]': undefined, + sort_by: 'nice_id', + sort_order: 'desc', + }; + let hasMore; + + do { + const response = await $.http.get(`/api/v2/views/${viewId}/tickets`, { + params, + }); + const allTickets = response?.data?.tickets; + hasMore = response?.data?.meta?.has_more; + params['page[after]'] = response.data.meta?.after_cursor; + + if (allTickets?.length) { + for (const ticket of allTickets) { + $.pushTriggerItem({ + raw: ticket, + meta: { + internalId: ticket.id.toString(), + }, + }); + } + } + } while (hasMore); + }, +}); diff --git a/packages/backend/src/apps/zendesk/triggers/new-users/index.js b/packages/backend/src/apps/zendesk/triggers/new-users/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8610fed0df4f06189aecb9e3ec70091ecff05729 --- /dev/null +++ b/packages/backend/src/apps/zendesk/triggers/new-users/index.js @@ -0,0 +1,83 @@ +import Crypto from 'crypto'; +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'New users', + key: 'newUsers', + type: 'webhook', + description: 'Triggers upon the creation of a new user.', + + async run($) { + const dataItem = { + raw: $.request.body, + meta: { + internalId: Crypto.randomUUID(), + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async testRun($) { + const params = { + query: 'type:user', + sort_by: 'created_at', + sort_order: 'desc', + }; + + const response = await $.http.get('/api/v2/search', { params }); + + const lastUser = response.data.results[0]; + + const computedWebhookEvent = { + id: Crypto.randomUUID(), + time: lastUser.created_at, + type: 'zen:event-type:user.created', + event: {}, + detail: { + id: lastUser.id, + role: lastUser.role, + email: lastUser.email, + created_at: lastUser.created_at, + updated_at: lastUser.updated_at, + external_id: lastUser.external_id, + organization_id: lastUser.organization_id, + default_group_id: lastUser.default_group_id, + }, + subject: `zen:user:${lastUser.id}`, + account_id: '', + zendesk_event_version: '2022-11-06', + }; + + const dataItem = { + raw: computedWebhookEvent, + meta: { + internalId: computedWebhookEvent.id, + }, + }; + + $.pushTriggerItem(dataItem); + }, + + async registerHook($) { + const payload = { + webhook: { + name: `Flow ID: ${$.flow.id}`, + status: 'active', + subscriptions: ['zen:event-type:user.created'], + endpoint: $.webhookUrl, + http_method: 'POST', + request_format: 'json', + }, + }; + + const response = await $.http.post('/api/v2/webhooks', payload); + const id = response.data.webhook.id; + + await $.flow.setRemoteWebhookId(id); + }, + + async unregisterHook($) { + await $.http.delete(`/api/v2/webhooks/${$.flow.remoteWebhookId}`); + }, +}); diff --git a/packages/backend/src/config/app.js b/packages/backend/src/config/app.js new file mode 100644 index 0000000000000000000000000000000000000000..de2d2c0edd558460aa11b8f31df2269857b2ff27 --- /dev/null +++ b/packages/backend/src/config/app.js @@ -0,0 +1,112 @@ +import { URL } from 'node:url'; +import * as dotenv from 'dotenv'; +import path from 'path'; +import process from 'node:process'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +if (process.env.APP_ENV === 'test') { + dotenv.config({ path: path.resolve(__dirname, '../../.env.test') }); +} else { + dotenv.config(); +} + +const host = process.env.HOST || 'localhost'; +const protocol = process.env.PROTOCOL || 'http'; +const port = process.env.PORT || '3000'; +const serveWebAppSeparately = + process.env.SERVE_WEB_APP_SEPARATELY === 'true' ? true : false; + +let apiUrl = new URL( + process.env.API_URL || `${protocol}://${host}:${port}` +).toString(); +apiUrl = apiUrl.substring(0, apiUrl.length - 1); + +// use apiUrl by default, which has less priority over the following cases +let webAppUrl = apiUrl; + +if (process.env.WEB_APP_URL) { + // use env. var. if provided + webAppUrl = new URL(process.env.WEB_APP_URL).toString(); + webAppUrl = webAppUrl.substring(0, webAppUrl.length - 1); +} else if (serveWebAppSeparately) { + // no env. var. and serving separately, sign of development + webAppUrl = 'http://localhost:3001'; +} + +let webhookUrl = new URL(process.env.WEBHOOK_URL || apiUrl).toString(); +webhookUrl = webhookUrl.substring(0, webhookUrl.length - 1); + +const publicDocsUrl = 'https://automatisch.io/docs'; +const docsUrl = process.env.DOCS_URL || publicDocsUrl; + +const appEnv = process.env.APP_ENV || 'development'; + +const appConfig = { + host, + protocol, + port, + appEnv: appEnv, + logLevel: process.env.LOG_LEVEL || 'info', + isDev: appEnv === 'development', + isTest: appEnv === 'test', + isProd: appEnv === 'production', + version: '0.12.0', + postgresDatabase: process.env.POSTGRES_DATABASE || 'automatisch_development', + postgresSchema: process.env.POSTGRES_SCHEMA || 'public', + postgresPort: parseInt(process.env.POSTGRES_PORT || '5432'), + postgresHost: process.env.POSTGRES_HOST || 'localhost', + postgresUsername: + process.env.POSTGRES_USERNAME || 'automatisch_development_user', + postgresPassword: process.env.POSTGRES_PASSWORD, + postgresEnableSsl: process.env.POSTGRES_ENABLE_SSL === 'true', + encryptionKey: process.env.ENCRYPTION_KEY || '', + webhookSecretKey: process.env.WEBHOOK_SECRET_KEY || '', + appSecretKey: process.env.APP_SECRET_KEY || '', + serveWebAppSeparately, + redisHost: process.env.REDIS_HOST || '127.0.0.1', + redisPort: parseInt(process.env.REDIS_PORT || '6379'), + redisUsername: process.env.REDIS_USERNAME, + redisPassword: process.env.REDIS_PASSWORD, + redisTls: process.env.REDIS_TLS === 'true', + enableBullMQDashboard: process.env.ENABLE_BULLMQ_DASHBOARD === 'true', + bullMQDashboardUsername: process.env.BULLMQ_DASHBOARD_USERNAME, + bullMQDashboardPassword: process.env.BULLMQ_DASHBOARD_PASSWORD, + baseUrl: apiUrl, + webAppUrl, + webhookUrl, + docsUrl, + telemetryEnabled: process.env.TELEMETRY_ENABLED === 'false' ? false : true, + requestBodySizeLimit: '1mb', + smtpHost: process.env.SMTP_HOST, + smtpPort: parseInt(process.env.SMTP_PORT || '587'), + smtpSecure: process.env.SMTP_SECURE === 'true', + smtpUser: process.env.SMTP_USER, + smtpPassword: process.env.SMTP_PASSWORD, + fromEmail: process.env.FROM_EMAIL, + isCloud: process.env.AUTOMATISCH_CLOUD === 'true', + isSelfHosted: process.env.AUTOMATISCH_CLOUD !== 'true', + isMation: process.env.MATION === 'true', + paddleVendorId: Number(process.env.PADDLE_VENDOR_ID), + paddleVendorAuthCode: process.env.PADDLE_VENDOR_AUTH_CODE, + paddlePublicKey: process.env.PADDLE_PUBLIC_KEY, + licenseKey: process.env.LICENSE_KEY, + sentryDsn: process.env.SENTRY_DSN, + CI: process.env.CI === 'true', + disableNotificationsPage: process.env.DISABLE_NOTIFICATIONS_PAGE === 'true', + disableFavicon: process.env.DISABLE_FAVICON === 'true', + additionalDrawerLink: process.env.ADDITIONAL_DRAWER_LINK, + additionalDrawerLinkText: process.env.ADDITIONAL_DRAWER_LINK_TEXT, + disableSeedUser: process.env.DISABLE_SEED_USER === 'true', +}; + +if (!appConfig.encryptionKey) { + throw new Error('ENCRYPTION_KEY environment variable needs to be set!'); +} + +if (!appConfig.webhookSecretKey) { + throw new Error('WEBHOOK_SECRET_KEY environment variable needs to be set!'); +} + +export default appConfig; diff --git a/packages/backend/src/config/cors-options.js b/packages/backend/src/config/cors-options.js new file mode 100644 index 0000000000000000000000000000000000000000..b4386b1cf71e776dc4ed6e0054bd208ba8cdb00f --- /dev/null +++ b/packages/backend/src/config/cors-options.js @@ -0,0 +1,10 @@ +import appConfig from './app.js'; + +const corsOptions = { + origin: appConfig.webAppUrl, + methods: 'GET,HEAD,POST,DELETE', + credentials: true, + optionsSuccessStatus: 200, +}; + +export default corsOptions; diff --git a/packages/backend/src/config/database.js b/packages/backend/src/config/database.js new file mode 100644 index 0000000000000000000000000000000000000000..55713b9feee90b4e37ddb47c60d9b9d9c950f28d --- /dev/null +++ b/packages/backend/src/config/database.js @@ -0,0 +1,22 @@ +import process from 'process'; +// The following two lines are required to get count values as number. +// More info: https://github.com/knex/knex/issues/387#issuecomment-51554522 +import pg from 'pg'; +pg.types.setTypeParser(20, 'text', parseInt); +import knex from 'knex'; +import knexConfig from '../../knexfile.js'; +import logger from '../helpers/logger.js'; + +export const client = knex(knexConfig); + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +client.raw('SELECT 1').catch((err) => { + if (err.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed PostgreSQL and it is running.', + err + ); + process.exit(); + } +}); diff --git a/packages/backend/src/config/orm.js b/packages/backend/src/config/orm.js new file mode 100644 index 0000000000000000000000000000000000000000..a2576e719effb3b2a5cbbb305f1ed7a729e2de50 --- /dev/null +++ b/packages/backend/src/config/orm.js @@ -0,0 +1,4 @@ +import { Model } from 'objection'; +import { client } from './database.js'; + +Model.knex(client); diff --git a/packages/backend/src/config/redis.js b/packages/backend/src/config/redis.js new file mode 100644 index 0000000000000000000000000000000000000000..45d007ea5099991b03c78a68552909ccfbae643c --- /dev/null +++ b/packages/backend/src/config/redis.js @@ -0,0 +1,16 @@ +import appConfig from './app.js'; + +const redisConfig = { + host: appConfig.redisHost, + port: appConfig.redisPort, + username: appConfig.redisUsername, + password: appConfig.redisPassword, + enableOfflineQueue: false, + enableReadyCheck: true, +}; + +if (appConfig.redisTls) { + redisConfig.tls = {}; +} + +export default redisConfig; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js new file mode 100644 index 0000000000000000000000000000000000000000..5fe162a9bec2772faf92b6f1b68f3c199f166846 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.js @@ -0,0 +1,13 @@ +import User from '../../../../models/user.js'; +import { renderObject, renderError } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const { email, password } = request.body; + const token = await User.authenticate(email, password); + + if (token) { + return renderObject(response, { token }); + } + + renderError(response, [{ general: ['Incorrect email or password.'] }]); +}; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cc54b0948a1cedfee011671931db86935e22e3e4 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/create-access-token.test.js @@ -0,0 +1,39 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import { createUser } from '../../../../../test/factories/user'; + +describe('POST /api/v1/access-tokens', () => { + beforeEach(async () => { + await createUser({ + email: 'user@automatisch.io', + password: 'password', + }); + }); + + it('should return the token data with correct credentials', async () => { + const response = await request(app) + .post('/api/v1/access-tokens') + .send({ + email: 'user@automatisch.io', + password: 'password', + }) + .expect(200); + + expect(response.body.data.token.length).toBeGreaterThan(0); + }); + + it('should return error with incorrect credentials', async () => { + const response = await request(app) + .post('/api/v1/access-tokens') + .send({ + email: 'incorrect@email.com', + password: 'incorrectpassword', + }) + .expect(422); + + expect(response.body.errors.general).toEqual([ + 'Incorrect email or password.', + ]); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js new file mode 100644 index 0000000000000000000000000000000000000000..95c7ffbc71c17b0385d6dfe23f330e6619444df5 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.js @@ -0,0 +1,15 @@ +export default async (request, response) => { + const token = request.params.token; + + const accessToken = await request.currentUser + .$relatedQuery('accessTokens') + .findOne({ + token, + revoked_at: null, + }) + .throwIfNotFound(); + + await accessToken.revoke(); + + response.status(204).send(); +}; diff --git a/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1651418a3d4b3027a9abb38f444081aa3a67e265 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/access-tokens/revoke-access-token.test.js @@ -0,0 +1,54 @@ +import { expect, describe, it, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user.js'; +import AccessToken from '../../../../models/access-token.js'; + +describe('DELETE /api/v1/access-tokens/:token', () => { + let token; + + beforeEach(async () => { + const currentUser = await createUser({ + email: 'user@automatisch.io', + password: 'password', + }); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should respond with HTTP 204 with correct token', async () => { + await request(app) + .delete(`/api/v1/access-tokens/${token}`) + .set('Authorization', token) + .expect(204); + + const revokedToken = await AccessToken.query().findOne({ token }); + + expect(revokedToken).toBeDefined(); + expect(revokedToken.revokedAt).not.toBeNull(); + }); + + it('should respond with HTTP 401 with incorrect credentials', async () => { + await request(app) + .delete(`/api/v1/access-tokens/${token}`) + .set('Authorization', 'wrong-token') + .expect(401); + + const unrevokedToken = await AccessToken.query().findOne({ token }); + + expect(unrevokedToken).toBeDefined(); + expect(unrevokedToken.revokedAt).toBeNull(); + }); + + it('should respond with HTTP 404 with correct credentials, but non-valid token', async () => { + await request(app) + .delete('/api/v1/access-tokens/wrong-token') + .set('Authorization', token) + .expect(404); + + const unrevokedToken = await AccessToken.query().findOne({ token }); + + expect(unrevokedToken).toBeDefined(); + expect(unrevokedToken.revokedAt).toBeNull(); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..c43ac23e2600fa1e44a6e1c24bb2c1a84ba36595 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import AppAuthClient from '../../../../../models/app-auth-client.js'; + +export default async (request, response) => { + const appAuthClient = await AppAuthClient.query() + .findById(request.params.appAuthClientId) + .where({ app_key: request.params.appKey }) + .throwIfNotFound(); + + renderObject(response, appAuthClient); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..581c49e7e05ed544c19b5cac9023861f672e5f51 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-client.ee.test.js @@ -0,0 +1,55 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import getAppAuthClientMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-client.js'; +import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/apps/:appKey/auth-clients/:appAuthClientId', () => { + let currentUser, adminRole, currentAppAuthClient, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + adminRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: adminRole.id }); + + currentAppAuthClient = await createAppAuthClient({ + appKey: 'deepl', + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified app auth client', async () => { + const response = await request(app) + .get(`/api/v1/admin/apps/deepl/auth-clients/${currentAppAuthClient.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppAuthClientMock(currentAppAuthClient); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing app auth client ID', async () => { + const notExistingAppAuthClientUUID = Crypto.randomUUID(); + + await request(app) + .get( + `/api/v1/admin/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}` + ) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .get('/api/v1/admin/apps/deepl/auth-clients/invalidAppAuthClientUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..257e0dd711c9c5f68b1e679f7b7bdf8d45bfb079 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.js @@ -0,0 +1,10 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import AppAuthClient from '../../../../../models/app-auth-client.js'; + +export default async (request, response) => { + const appAuthClients = await AppAuthClient.query() + .where({ app_key: request.params.appKey }) + .orderBy('created_at', 'desc'); + + renderObject(response, appAuthClients); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0dfd472c4b33f8161455bc907fc575128783b804 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/apps/get-auth-clients.ee.test.js @@ -0,0 +1,44 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import getAuthClientsMock from '../../../../../../test/mocks/rest/api/v1/admin/apps/get-auth-clients.js'; +import { createAppAuthClient } from '../../../../../../test/factories/app-auth-client.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/apps/:appKey/auth-clients', () => { + let currentUser, adminRole, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + adminRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: adminRole.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified app auth client info', async () => { + const appAuthClientOne = await createAppAuthClient({ + appKey: 'deepl', + }); + + const appAuthClientTwo = await createAppAuthClient({ + appKey: 'deepl', + }); + + const response = await request(app) + .get('/api/v1/admin/apps/deepl/auth-clients') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAuthClientsMock([ + appAuthClientTwo, + appAuthClientOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.js b/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..232e33ea29c9dbac866531a513fa7111701397e9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.js @@ -0,0 +1,6 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import permissionCatalog from '../../../../../helpers/permission-catalog.ee.js'; + +export default async (request, response) => { + renderObject(response, permissionCatalog); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.test.js b/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..bbeba16b98c05c627287cc5465b72bceb5df935d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/permissions/get-permissions-catalog.ee.test.js @@ -0,0 +1,32 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import getPermissionsCatalogMock from '../../../../../../test/mocks/rest/api/v1/admin/permissions/get-permissions-catalog.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/permissions/catalog', () => { + let role, currentUser, token; + + beforeEach(async () => { + role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return roles', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/admin/permissions/catalog') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getPermissionsCatalogMock(); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.js b/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..57733ae899a76028cbfd66a0f6599aa282608922 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.js @@ -0,0 +1,16 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import Role from '../../../../../models/role.js'; + +export default async (request, response) => { + const role = await Role.query() + .leftJoinRelated({ + permissions: true, + }) + .withGraphFetched({ + permissions: true, + }) + .findById(request.params.roleId) + .throwIfNotFound(); + + renderObject(response, role); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.test.js b/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..020539f4316936ce54035f300bb3166083fdc343 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/get-role.ee.test.js @@ -0,0 +1,59 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createPermission } from '../../../../../../test/factories/permission.js'; +import getRoleMock from '../../../../../../test/mocks/rest/api/v1/admin/roles/get-role.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/roles/:roleId', () => { + let role, currentUser, token, permissionOne, permissionTwo; + + beforeEach(async () => { + role = await createRole({ key: 'admin' }); + permissionOne = await createPermission({ roleId: role.id }); + permissionTwo = await createPermission({ roleId: role.id }); + currentUser = await createUser({ roleId: role.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return role', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get(`/api/v1/admin/roles/${role.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getRoleMock(role, [ + permissionOne, + permissionTwo, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing role UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const notExistingRoleUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/admin/roles/${notExistingRoleUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + await request(app) + .get('/api/v1/admin/roles/invalidRoleUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.js b/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..3193b3e0691aabef7694251bf5bb2aef4640ea69 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.js @@ -0,0 +1,8 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import Role from '../../../../../models/role.js'; + +export default async (request, response) => { + const roles = await Role.query().orderBy('name'); + + renderObject(response, roles); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.test.js b/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..6f22f50b3cffe42d8a94177a6f361f7cec609904 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/roles/get-roles.ee.test.js @@ -0,0 +1,33 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import getRolesMock from '../../../../../../test/mocks/rest/api/v1/admin/roles/get-roles.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/roles', () => { + let roleOne, roleTwo, currentUser, token; + + beforeEach(async () => { + roleOne = await createRole({ key: 'admin' }); + roleTwo = await createRole({ key: 'user' }); + currentUser = await createUser({ roleId: roleOne.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return roles', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/admin/roles') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getRolesMock([roleOne, roleTwo]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2f7a377f28599de8bfb081655963b233b2e80bcd --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js @@ -0,0 +1,14 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProvider = await SamlAuthProvider.query() + .findById(request.params.samlAuthProviderId) + .throwIfNotFound(); + + const roleMappings = await samlAuthProvider + .$relatedQuery('samlAuthProvidersRoleMappings') + .orderBy('remote_role_name', 'asc'); + + renderObject(response, roleMappings); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..42f37a3a0a85602932983dfb8e756b7083241bfc --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.test.js @@ -0,0 +1,51 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import { createRoleMapping } from '../../../../../../test/factories/role-mapping.js'; +import getRoleMappingsMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/saml-auth-providers/:samlAuthProviderId/role-mappings', () => { + let roleMappingOne, roleMappingTwo, samlAuthProvider, currentUser, token; + + beforeEach(async () => { + const role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + samlAuthProvider = await createSamlAuthProvider(); + + roleMappingOne = await createRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + remoteRoleName: 'Admin', + }); + + roleMappingTwo = await createRoleMapping({ + samlAuthProviderId: samlAuthProvider.id, + remoteRoleName: 'User', + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return role mappings', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get( + `/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}/role-mappings` + ) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getRoleMappingsMock([ + roleMappingOne, + roleMappingTwo, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1fdf633be2060607a0603570538f0f512b98cb72 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js @@ -0,0 +1,12 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProvider = await SamlAuthProvider.query() + .findById(request.params.samlAuthProviderId) + .throwIfNotFound(); + + renderObject(response, samlAuthProvider, { + serializer: 'AdminSamlAuthProvider', + }); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..adddb53db7ba7c2dfd8f6e9d4c39dd0fe1b4ce06 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.test.js @@ -0,0 +1,57 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import getSamlAuthProviderMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/saml-auth-provider/:samlAuthProviderId', () => { + let samlAuthProvider, currentUser, token; + + beforeEach(async () => { + const role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + samlAuthProvider = await createSamlAuthProvider(); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return saml auth provider with specified id', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get(`/api/v1/admin/saml-auth-providers/${samlAuthProvider.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getSamlAuthProviderMock(samlAuthProvider); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing saml auth provider UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const notExistingSamlAuthProviderUUID = Crypto.randomUUID(); + + await request(app) + .get( + `/api/v1/admin/saml-auth-providers/${notExistingSamlAuthProviderUUID}` + ) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + await request(app) + .get('/api/v1/admin/saml-auth-providers/invalidSamlAuthProviderUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..88b3d6332edb47b0e5e062ec698538446a7c77e0 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js @@ -0,0 +1,13 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProviders = await SamlAuthProvider.query().orderBy( + 'created_at', + 'desc' + ); + + renderObject(response, samlAuthProviders, { + serializer: 'AdminSamlAuthProvider', + }); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..ca2106e22bdb9194975b8851a1b298e96ea67219 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.test.js @@ -0,0 +1,39 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id.js'; +import { createRole } from '../../../../../../test/factories/role.js'; +import { createUser } from '../../../../../../test/factories/user.js'; +import { createSamlAuthProvider } from '../../../../../../test/factories/saml-auth-provider.ee.js'; +import getSamlAuthProvidersMock from '../../../../../../test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/saml-auth-providers', () => { + let samlAuthProviderOne, samlAuthProviderTwo, currentUser, token; + + beforeEach(async () => { + const role = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: role.id }); + + samlAuthProviderOne = await createSamlAuthProvider(); + samlAuthProviderTwo = await createSamlAuthProvider(); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return saml auth providers', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/admin/saml-auth-providers') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getSamlAuthProvidersMock([ + samlAuthProviderTwo, + samlAuthProviderOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.js b/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7c63e600f160a9ee25c5b54ed24dc3bdfe0c5530 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.js @@ -0,0 +1,13 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import User from '../../../../../models/user.js'; + +export default async (request, response) => { + const user = await User.query() + .withGraphFetched({ + role: true, + }) + .findById(request.params.userId) + .throwIfNotFound(); + + renderObject(response, user); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.test.js b/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..93ee5053b0a4aa76aed90e6b3d72db15ff390369 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/get-user.ee.test.js @@ -0,0 +1,55 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../../app.js'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../../test/factories/user'; +import { createRole } from '../../../../../../test/factories/role'; +import getUserMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-user.js'; +import * as license from '../../../../../helpers/license.ee.js'; + +describe('GET /api/v1/admin/users/:userId', () => { + let currentUser, currentUserRole, anotherUser, anotherUserRole, token; + + beforeEach(async () => { + currentUserRole = await createRole({ key: 'admin' }); + currentUser = await createUser({ roleId: currentUserRole.id }); + + anotherUser = await createUser(); + anotherUserRole = await anotherUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified user info', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get(`/api/v1/admin/users/${anotherUser.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getUserMock(anotherUser, anotherUserRole); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing user UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const notExistingUserUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/admin/users/${notExistingUserUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + await request(app) + .get('/api/v1/admin/users/invalidUserUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.js b/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1115404e0667ddd3fd7e9a2e5130488ffb6a5959 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.js @@ -0,0 +1,15 @@ +import { renderObject } from '../../../../../helpers/renderer.js'; +import User from '../../../../../models/user.js'; +import paginateRest from '../../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const usersQuery = User.query() + .withGraphFetched({ + role: true, + }) + .orderBy('full_name', 'asc'); + + const users = await paginateRest(usersQuery, request.query.page); + + renderObject(response, users); +}; diff --git a/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.test.js b/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a7528de0e277def0371a8226f8adaacdd0b7706f --- /dev/null +++ b/packages/backend/src/controllers/api/v1/admin/users/get-users.ee.test.js @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app'; +import createAuthTokenByUserId from '../../../../../helpers/create-auth-token-by-user-id'; +import { createRole } from '../../../../../../test/factories/role'; +import { createUser } from '../../../../../../test/factories/user'; +import getUsersMock from '../../../../../../test/mocks/rest/api/v1/admin/users/get-users.js'; + +describe('GET /api/v1/admin/users', () => { + let currentUser, currentUserRole, anotherUser, anotherUserRole, token; + + beforeEach(async () => { + currentUserRole = await createRole({ key: 'admin' }); + + currentUser = await createUser({ + roleId: currentUserRole.id, + fullName: 'Current User', + }); + + anotherUserRole = await createRole({ + key: 'anotherUser', + name: 'Another user role', + }); + + anotherUser = await createUser({ + roleId: anotherUserRole.id, + fullName: 'Another User', + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return users data', async () => { + const response = await request(app) + .get('/api/v1/admin/users') + .set('Authorization', token) + .expect(200); + + const expectedResponsePayload = await getUsersMock( + [anotherUser, currentUser], + [anotherUserRole, currentUserRole] + ); + + expect(response.body).toEqual(expectedResponsePayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-action-substeps.js b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.js new file mode 100644 index 0000000000000000000000000000000000000000..6b985bdc3b9b81891da79c7e2e33ce6a5a3209fe --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.js @@ -0,0 +1,11 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const substeps = await App.findActionSubsteps( + request.params.appKey, + request.params.actionKey + ); + + renderObject(response, substeps); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cc4c01a3c9740b8214856103c3710b749e3b1166 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-action-substeps.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getActionSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-action-substeps.js'; + +describe('GET /api/v1/apps/:appKey/actions/:actionKey/substeps', () => { + let currentUser, exampleApp, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + exampleApp = await App.findOneByKey('github'); + }); + + it('should return the app auth info', async () => { + const actions = await App.findActionsByKey('github'); + const exampleAction = actions.find( + (action) => action.key === 'createIssue' + ); + + const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/${exampleAction.key}/substeps`; + + const response = await request(app) + .get(endpointUrl) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getActionSubstepsMock(exampleAction.substeps); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key/actions/invalid-actions-key/substeps') + .set('Authorization', token) + .expect(404); + }); + + it('should return empty array for invalid action key', async () => { + const endpointUrl = `/api/v1/apps/${exampleApp.key}/actions/invalid-action-key/substeps`; + + const response = await request(app) + .get(endpointUrl) + .set('Authorization', token) + .expect(200); + + expect(response.body.data).toEqual([]); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-actions.js b/packages/backend/src/controllers/api/v1/apps/get-actions.js new file mode 100644 index 0000000000000000000000000000000000000000..78d45fde4e56e84125d5dc243a65590bead91a13 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-actions.js @@ -0,0 +1,8 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const actions = await App.findActionsByKey(request.params.appKey); + + renderObject(response, actions, { serializer: 'Action' }); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-actions.test.js b/packages/backend/src/controllers/api/v1/apps/get-actions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..522191474eff707412c3866861bd11545e9b3f67 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-actions.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getActionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-actions.js'; + +describe('GET /api/v1/apps/:appKey/actions', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the app actions', async () => { + const exampleApp = await App.findOneByKey('github'); + + const response = await request(app) + .get(`/api/v1/apps/${exampleApp.key}/actions`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getActionsMock(exampleApp.actions); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key/actions') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-app.js b/packages/backend/src/controllers/api/v1/apps/get-app.js new file mode 100644 index 0000000000000000000000000000000000000000..2f27d3cdc276d88c1867a07d57fe706260a3b364 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-app.js @@ -0,0 +1,8 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + renderObject(response, app, { serializer: 'App' }); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-app.test.js b/packages/backend/src/controllers/api/v1/apps/get-app.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2cc0d839e636c9a61bfd4d4558ed1e8eb7c15155 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-app.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getAppMock from '../../../../../test/mocks/rest/api/v1/apps/get-app.js'; + +describe('GET /api/v1/apps/:appKey', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the app info', async () => { + const exampleApp = await App.findOneByKey('github'); + + const response = await request(app) + .get(`/api/v1/apps/${exampleApp.key}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppMock(exampleApp); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-apps.js b/packages/backend/src/controllers/api/v1/apps/get-apps.js new file mode 100644 index 0000000000000000000000000000000000000000..be6e112e5bd6d2856cea812b79c708e0a0a9a596 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-apps.js @@ -0,0 +1,16 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let apps = await App.findAll(request.query.name); + + if (request.query.onlyWithTriggers) { + apps = apps.filter((app) => app.triggers?.length); + } + + if (request.query.onlyWithActions) { + apps = apps.filter((app) => app.actions?.length); + } + + renderObject(response, apps, { serializer: 'App' }); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-apps.test.js b/packages/backend/src/controllers/api/v1/apps/get-apps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..87f1702a2b142db4389e4166bbbd9b6e90219982 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-apps.test.js @@ -0,0 +1,63 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getAppsMock from '../../../../../test/mocks/rest/api/v1/apps/get-apps.js'; + +describe('GET /api/v1/apps', () => { + let currentUser, apps, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + apps = await App.findAll(); + }); + + it('should return all apps', async () => { + const response = await request(app) + .get('/api/v1/apps') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(apps); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return all apps filtered by name', async () => { + const appsWithNameGit = apps.filter((app) => app.name.includes('Git')); + + const response = await request(app) + .get('/api/v1/apps?name=Git') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(appsWithNameGit); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return only the apps with triggers', async () => { + const appsWithTriggers = apps.filter((app) => app.triggers?.length > 0); + + const response = await request(app) + .get('/api/v1/apps?onlyWithTriggers=true') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(appsWithTriggers); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return only the apps with actions', async () => { + const appsWithActions = apps.filter((app) => app.actions?.length > 0); + + const response = await request(app) + .get('/api/v1/apps?onlyWithActions=true') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(appsWithActions); + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js b/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..5aceb529e1aa904ed6c6d377674b914b449d5e5b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import AppAuthClient from '../../../../models/app-auth-client.js'; + +export default async (request, response) => { + const appAuthClient = await AppAuthClient.query() + .findById(request.params.appAuthClientId) + .where({ app_key: request.params.appKey, active: true }) + .throwIfNotFound(); + + renderObject(response, appAuthClient); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..050432fff0ff5bf9ce6a23f1cf94643b25f736ec --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth-client.ee.test.js @@ -0,0 +1,50 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getAppAuthClientMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-client.js'; +import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/apps/:appKey/auth-clients/:appAuthClientId', () => { + let currentUser, currentAppAuthClient, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + currentUser = await createUser(); + currentAppAuthClient = await createAppAuthClient({ + appKey: 'deepl', + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified app auth client', async () => { + const response = await request(app) + .get(`/api/v1/apps/deepl/auth-clients/${currentAppAuthClient.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppAuthClientMock(currentAppAuthClient); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing app auth client ID', async () => { + const notExistingAppAuthClientUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/apps/deepl/auth-clients/${notExistingAppAuthClientUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await request(app) + .get('/api/v1/apps/deepl/auth-clients/invalidAppAuthClientUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js b/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..06eceec1be8fc1abc607c1f5eb36079416baf933 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.js @@ -0,0 +1,10 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import AppAuthClient from '../../../../models/app-auth-client.js'; + +export default async (request, response) => { + const appAuthClients = await AppAuthClient.query() + .where({ app_key: request.params.appKey, active: true }) + .orderBy('created_at', 'desc'); + + renderObject(response, appAuthClients); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..8bc479d985f60a43e72a4a931368d0059e805683 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth-clients.ee.test.js @@ -0,0 +1,42 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getAuthClientsMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth-clients.js'; +import { createAppAuthClient } from '../../../../../test/factories/app-auth-client.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/apps/:appKey/auth-clients', () => { + let currentUser, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + currentUser = await createUser(); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified app auth client info', async () => { + const appAuthClientOne = await createAppAuthClient({ + appKey: 'deepl', + }); + + const appAuthClientTwo = await createAppAuthClient({ + appKey: 'deepl', + }); + + const response = await request(app) + .get('/api/v1/apps/deepl/auth-clients') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAuthClientsMock([ + appAuthClientTwo, + appAuthClientOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth.js b/packages/backend/src/controllers/api/v1/apps/get-auth.js new file mode 100644 index 0000000000000000000000000000000000000000..37b307539e1e6170ac70b7f529ac91ddc823e104 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth.js @@ -0,0 +1,8 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const auth = await App.findAuthByKey(request.params.appKey); + + renderObject(response, auth, { serializer: 'Auth' }); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-auth.test.js b/packages/backend/src/controllers/api/v1/apps/get-auth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a6405f70c6323aaf8fbdf7353a50d527882aac3d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-auth.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getAuthMock from '../../../../../test/mocks/rest/api/v1/apps/get-auth.js'; + +describe('GET /api/v1/apps/:appKey/auth', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the app auth info', async () => { + const exampleApp = await App.findOneByKey('github'); + + const response = await request(app) + .get(`/api/v1/apps/${exampleApp.key}/auth`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAuthMock(exampleApp.auth); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key/auth') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-config.ee.js b/packages/backend/src/controllers/api/v1/apps/get-config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..d0837e358e37fc2dbd5b742b56d7023342168743 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-config.ee.js @@ -0,0 +1,15 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import AppConfig from '../../../../models/app-config.js'; + +export default async (request, response) => { + const appConfig = await AppConfig.query() + .withGraphFetched({ + appAuthClients: true, + }) + .findOne({ + key: request.params.appKey, + }) + .throwIfNotFound(); + + renderObject(response, appConfig); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js b/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d10c2bd7bb4626275d98fa059af2a6970c5dbb04 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-config.ee.test.js @@ -0,0 +1,44 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getAppConfigMock from '../../../../../test/mocks/rest/api/v1/apps/get-config.js'; +import { createAppConfig } from '../../../../../test/factories/app-config.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/apps/:appKey/config', () => { + let currentUser, appConfig, token; + + beforeEach(async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + currentUser = await createUser(); + + appConfig = await createAppConfig({ + key: 'deepl', + allowCustomConnection: true, + shared: true, + disabled: false, + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return specified app config info', async () => { + const response = await request(app) + .get(`/api/v1/apps/${appConfig.key}/config`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppConfigMock(appConfig); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing app key', async () => { + await request(app) + .get('/api/v1/apps/not-existing-app-key/config') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-connections.js b/packages/backend/src/controllers/api/v1/apps/get-connections.js new file mode 100644 index 0000000000000000000000000000000000000000..1f5a91ad6b17bd555f5849055e02fef1b0bd4717 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.js @@ -0,0 +1,24 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import App from '../../../../models/app.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + const connections = await request.currentUser.authorizedConnections + .clone() + .select('connections.*') + .withGraphFetched({ + appConfig: true, + appAuthClient: true, + }) + .fullOuterJoinRelated('steps') + .where({ + 'connections.key': app.key, + 'connections.draft': false, + }) + .countDistinct('steps.flow_id as flowCount') + .groupBy('connections.id') + .orderBy('created_at', 'desc'); + + renderObject(response, connections); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-connections.test.js b/packages/backend/src/controllers/api/v1/apps/get-connections.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c95cc467c17d3935c9b192a715cfeadc8d0c57e1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-connections.test.js @@ -0,0 +1,101 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getConnectionsMock from '../../../../../test/mocks/rest/api/v1/apps/get-connections.js'; + +describe('GET /api/v1/apps/:appKey/connections', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the connections data of specified app for current user', async () => { + const currentUserConnectionOne = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + const currentUserConnectionTwo = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/apps/deepl/connections') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionsMock([ + currentUserConnectionTwo, + currentUserConnectionOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the connections data of specified app for another user', async () => { + const anotherUser = await createUser(); + + const anotherUserConnectionOne = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + draft: false, + }); + + const anotherUserConnectionTwo = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + draft: false, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/apps/deepl/connections') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionsMock([ + anotherUserConnectionTwo, + anotherUserConnectionOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid connection UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .get('/api/v1/connections/invalid-connection-id/connections') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-flows.js b/packages/backend/src/controllers/api/v1/apps/get-flows.js new file mode 100644 index 0000000000000000000000000000000000000000..6554365e0cc0f70620294a4abe466e66add71d8e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-flows.js @@ -0,0 +1,23 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import App from '../../../../models/app.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const app = await App.findOneByKey(request.params.appKey); + + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .joinRelated({ + steps: true, + }) + .withGraphFetched({ + steps: true, + }) + .where('steps.app_key', app.key) + .orderBy('active', 'desc') + .orderBy('updated_at', 'desc'); + + const flows = await paginateRest(flowsQuery, request.query.page); + + renderObject(response, flows); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-flows.test.js b/packages/backend/src/controllers/api/v1/apps/get-flows.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b53d70cad33d09d82d68186a5d9d98531196f37a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-flows.test.js @@ -0,0 +1,129 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; + +describe('GET /api/v1/apps/:appKey/flows', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flows data of specified app for current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + }); + + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const currentUserFlowTwo = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/apps/webhook/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flows data of specified app for another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + }); + + const actionStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'action', + }); + + const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id }); + + await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/apps/webhook/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .get('/api/v1/apps/invalid-app-key/flows') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.js b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.js new file mode 100644 index 0000000000000000000000000000000000000000..96e542f41ac3eb44191e487c0b8e2097533fc529 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.js @@ -0,0 +1,11 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const substeps = await App.findTriggerSubsteps( + request.params.appKey, + request.params.triggerKey + ); + + renderObject(response, substeps); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..840cb85eba20affeff7c90a7cd6aa8de3c56360a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-trigger-substeps.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getTriggerSubstepsMock from '../../../../../test/mocks/rest/api/v1/apps/get-trigger-substeps.js'; + +describe('GET /api/v1/apps/:appKey/triggers/:triggerKey/substeps', () => { + let currentUser, exampleApp, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + exampleApp = await App.findOneByKey('github'); + }); + + it('should return the app auth info', async () => { + const triggers = await App.findTriggersByKey('github'); + const exampleTrigger = triggers.find( + (trigger) => trigger.key === 'newIssues' + ); + + const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/${exampleTrigger.key}/substeps`; + + const response = await request(app) + .get(endpointUrl) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getTriggerSubstepsMock(exampleTrigger.substeps); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key/triggers/invalid-trigger-key/substeps') + .set('Authorization', token) + .expect(404); + }); + + it('should return empty array for invalid trigger key', async () => { + const endpointUrl = `/api/v1/apps/${exampleApp.key}/triggers/invalid-trigger-key/substeps`; + + const response = await request(app) + .get(endpointUrl) + .set('Authorization', token) + .expect(200); + + expect(response.body.data).toEqual([]); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/apps/get-triggers.js b/packages/backend/src/controllers/api/v1/apps/get-triggers.js new file mode 100644 index 0000000000000000000000000000000000000000..19cd9095027618f3463091c18928e475c72ded6b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-triggers.js @@ -0,0 +1,8 @@ +import App from '../../../../models/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const triggers = await App.findTriggersByKey(request.params.appKey); + + renderObject(response, triggers, { serializer: 'Trigger' }); +}; diff --git a/packages/backend/src/controllers/api/v1/apps/get-triggers.test.js b/packages/backend/src/controllers/api/v1/apps/get-triggers.test.js new file mode 100644 index 0000000000000000000000000000000000000000..ff825a71203590c39582c2081dc1a80fdac5721b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/apps/get-triggers.test.js @@ -0,0 +1,35 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import App from '../../../../models/app'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import getTriggersMock from '../../../../../test/mocks/rest/api/v1/apps/get-triggers.js'; + +describe('GET /api/v1/apps/:appKey/triggers', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the app triggers', async () => { + const exampleApp = await App.findOneByKey('github'); + + const response = await request(app) + .get(`/api/v1/apps/${exampleApp.key}/triggers`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getTriggersMock(exampleApp.triggers); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for invalid app key', async () => { + await request(app) + .get('/api/v1/apps/invalid-app-key/triggers') + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/automatisch/config.ee.js b/packages/backend/src/controllers/api/v1/automatisch/config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..44fd957cc381c63fc3cc5a4bb6a36616aac35ce3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/config.ee.js @@ -0,0 +1,24 @@ +import appConfig from '../../../../config/app.js'; +import Config from '../../../../models/config.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const defaultConfig = { + disableNotificationsPage: appConfig.disableNotificationsPage, + disableFavicon: appConfig.disableFavicon, + additionalDrawerLink: appConfig.additionalDrawerLink, + additionalDrawerLinkText: appConfig.additionalDrawerLinkText, + }; + + let config = await Config.query().orderBy('key', 'asc'); + + config = config.reduce((computedConfig, configEntry) => { + const { key, value } = configEntry; + + computedConfig[key] = value?.data; + + return computedConfig; + }, defaultConfig); + + renderObject(response, config); +}; diff --git a/packages/backend/src/controllers/api/v1/automatisch/config.ee.test.js b/packages/backend/src/controllers/api/v1/automatisch/config.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..438d39c56fdd6e491576144d0f836e67a15df03a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/config.ee.test.js @@ -0,0 +1,51 @@ +import { vi, expect, describe, it } from 'vitest'; +import request from 'supertest'; +import { createConfig } from '../../../../../test/factories/config.js'; +import app from '../../../../app.js'; +import configMock from '../../../../../test/mocks/rest/api/v1/automatisch/config.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/automatisch/config', () => { + it('should return Automatisch config', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const logoConfig = await createConfig({ + key: 'logo.svgData', + value: { data: 'Sample' }, + }); + + const primaryDarkConfig = await createConfig({ + key: 'palette.primary.dark', + value: { data: '#001F52' }, + }); + + const primaryLightConfig = await createConfig({ + key: 'palette.primary.light', + value: { data: '#4286FF' }, + }); + + const primaryMainConfig = await createConfig({ + key: 'palette.primary.main', + value: { data: '#0059F7' }, + }); + + const titleConfig = await createConfig({ + key: 'title', + value: { data: 'Sample Title' }, + }); + + const response = await request(app) + .get('/api/v1/automatisch/config') + .expect(200); + + const expectedPayload = configMock( + logoConfig, + primaryDarkConfig, + primaryLightConfig, + primaryMainConfig, + titleConfig + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/automatisch/info.js b/packages/backend/src/controllers/api/v1/automatisch/info.js new file mode 100644 index 0000000000000000000000000000000000000000..5a37b53913f8e4934baf3c7b84d758df487bb4bd --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/info.js @@ -0,0 +1,13 @@ +import appConfig from '../../../../config/app.js'; +import { hasValidLicense } from '../../../../helpers/license.ee.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const info = { + isCloud: appConfig.isCloud, + isMation: appConfig.isMation, + isEnterprise: await hasValidLicense(), + }; + + renderObject(response, info); +}; diff --git a/packages/backend/src/controllers/api/v1/automatisch/info.test.js b/packages/backend/src/controllers/api/v1/automatisch/info.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1853346f7e517580e358286d7fa43770bbc746f1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/info.test.js @@ -0,0 +1,22 @@ +import { vi, expect, describe, it } from 'vitest'; +import request from 'supertest'; +import appConfig from '../../../../config/app.js'; +import app from '../../../../app.js'; +import infoMock from '../../../../../test/mocks/rest/api/v1/automatisch/info.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/automatisch/info', () => { + it('should return Automatisch info', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); + vi.spyOn(appConfig, 'isMation', 'get').mockReturnValue(false); + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/automatisch/info') + .expect(200); + + const expectedPayload = infoMock(); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/automatisch/license.js b/packages/backend/src/controllers/api/v1/automatisch/license.js new file mode 100644 index 0000000000000000000000000000000000000000..65f6f4b6a68aa45bd5da2ee2dd91986396e7b2cc --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/license.js @@ -0,0 +1,15 @@ +import { getLicense } from '../../../../helpers/license.ee.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const license = await getLicense(); + + const computedLicense = { + id: license ? license.id : null, + name: license ? license.name : null, + expireAt: license ? license.expireAt : null, + verified: license ? true : false, + }; + + renderObject(response, computedLicense); +}; diff --git a/packages/backend/src/controllers/api/v1/automatisch/license.test.js b/packages/backend/src/controllers/api/v1/automatisch/license.test.js new file mode 100644 index 0000000000000000000000000000000000000000..211ef630c83ac8834f4bbbb787033b2fe6463a92 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/license.test.js @@ -0,0 +1,23 @@ +import { vi, expect, describe, it } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import licenseMock from '../../../../../test/mocks/rest/api/v1/automatisch/license.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/automatisch/license', () => { + it('should return Automatisch license info', async () => { + vi.spyOn(license, 'getLicense').mockResolvedValue({ + id: '123', + name: 'license-name', + expireAt: '2025-12-31T23:59:59Z', + }); + + const response = await request(app) + .get('/api/v1/automatisch/license') + .expect(200); + + const expectedPayload = licenseMock(); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/automatisch/notifications.js b/packages/backend/src/controllers/api/v1/automatisch/notifications.js new file mode 100644 index 0000000000000000000000000000000000000000..e42d65f9d6d89379c4a780a8d97a42c01e41f6f8 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/notifications.js @@ -0,0 +1,19 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import axios from '../../../../helpers/axios-with-proxy.js'; +import logger from '../../../../helpers/logger.js'; + +const NOTIFICATIONS_URL = + 'https://notifications.automatisch.io/notifications.json'; + +export default async (request, response) => { + let notifications = []; + + try { + const response = await axios.get(NOTIFICATIONS_URL); + notifications = response.data; + } catch (error) { + logger.error('Error fetching notifications API endpoint!', error); + } + + renderObject(response, notifications); +}; diff --git a/packages/backend/src/controllers/api/v1/automatisch/notifications.test.js b/packages/backend/src/controllers/api/v1/automatisch/notifications.test.js new file mode 100644 index 0000000000000000000000000000000000000000..ddfdd90e6e783f3a06e7103269c3e41102b56575 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/notifications.test.js @@ -0,0 +1,9 @@ +import { describe, it } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; + +describe('GET /api/v1/automatisch/notifications', () => { + it('should return Automatisch notifications', async () => { + await request(app).get('/api/v1/automatisch/notifications').expect(200); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/automatisch/version.js b/packages/backend/src/controllers/api/v1/automatisch/version.js new file mode 100644 index 0000000000000000000000000000000000000000..1f8915f49ad54bd39444c1ab5bce3048d2c5c98c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/version.js @@ -0,0 +1,6 @@ +import appConfig from '../../../../config/app.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + renderObject(response, { version: appConfig.version }); +}; diff --git a/packages/backend/src/controllers/api/v1/automatisch/version.test.js b/packages/backend/src/controllers/api/v1/automatisch/version.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a30fe7f394e60b7c0d0452a6d1f076affbbc410b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/automatisch/version.test.js @@ -0,0 +1,26 @@ +import { describe, it, expect } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; + +describe('GET /api/v1/automatisch/version', () => { + it('should return Automatisch version', async () => { + const response = await request(app) + .get('/api/v1/automatisch/version') + .expect(200); + + const expectedPayload = { + data: { + version: '0.12.0', + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/connections/create-test.js b/packages/backend/src/controllers/api/v1/connections/create-test.js new file mode 100644 index 0000000000000000000000000000000000000000..32132582fb986495a5a9f836e2a64ef9c94c1b8c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/create-test.js @@ -0,0 +1,14 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + let connection = await request.currentUser.authorizedConnections + .clone() + .findOne({ + id: request.params.connectionId, + }) + .throwIfNotFound(); + + connection = await connection.testAndUpdateConnection(); + + renderObject(response, connection); +}; diff --git a/packages/backend/src/controllers/api/v1/connections/create-test.test.js b/packages/backend/src/controllers/api/v1/connections/create-test.test.js new file mode 100644 index 0000000000000000000000000000000000000000..152b404a170ff5b6b0811651c0a79a829e13b64a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/create-test.test.js @@ -0,0 +1,123 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; + +describe('POST /api/v1/connections/:connectionId/test', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should update the connection as not verified for current user', async () => { + const currentUserConnection = await createConnection({ + userId: currentUser.id, + key: 'deepl', + verified: true, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/connections/${currentUserConnection.id}/test`) + .set('Authorization', token) + .expect(200); + + expect(response.body.data.verified).toEqual(false); + }); + + it('should update the connection as not verified for another user', async () => { + const anotherUser = await createUser(); + + const anotherUserConnection = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + verified: true, + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/connections/${anotherUserConnection.id}/test`) + .set('Authorization', token) + .expect(200); + + expect(response.body.data.verified).toEqual(false); + }); + + it('should return not found response for not existing connection UUID', async () => { + const notExistingConnectionUUID = Crypto.randomUUID(); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post(`/api/v1/connections/${notExistingConnectionUUID}/test`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await request(app) + .post('/api/v1/connections/invalidConnectionUUID/test') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/connections/get-flows.js b/packages/backend/src/controllers/api/v1/connections/get-flows.js new file mode 100644 index 0000000000000000000000000000000000000000..ade34b23aa0b4bcbd2829b4aaea524fb1024fd84 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/get-flows.js @@ -0,0 +1,20 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .joinRelated({ + steps: true, + }) + .withGraphFetched({ + steps: true, + }) + .where('steps.connection_id', request.params.connectionId) + .orderBy('active', 'desc') + .orderBy('updated_at', 'desc'); + + const flows = await paginateRest(flowsQuery, request.query.page); + + renderObject(response, flows); +}; diff --git a/packages/backend/src/controllers/api/v1/connections/get-flows.test.js b/packages/backend/src/controllers/api/v1/connections/get-flows.test.js new file mode 100644 index 0000000000000000000000000000000000000000..bc9088d66272cd4edc74cac72a29835d5a2a7c1b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/connections/get-flows.test.js @@ -0,0 +1,128 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; + +describe('GET /api/v1/connections/:connectionId/flows', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flows data of specified connection for current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const currentUserConnection = await createConnection({ + userId: currentUser.id, + key: 'webhook', + }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + connectionId: currentUserConnection.id, + }); + + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const currentUserFlowTwo = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/connections/${currentUserConnection.id}/flows`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flows data of specified connection for another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const anotherUserConnection = await createConnection({ + userId: anotherUser.id, + key: 'webhook', + }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + appKey: 'webhook', + connectionId: anotherUserConnection.id, + }); + + const actionStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'action', + }); + + const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id }); + + await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'trigger', + appKey: 'github', + }); + + await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/connections/${anotherUserConnection.id}/flows`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowOne], + [triggerStepFlowOne, actionStepFlowOne] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..f90d243b7b06afec65bf5877b3b55dd067f75f3c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.js @@ -0,0 +1,23 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const execution = await request.currentUser.authorizedExecutions + .clone() + .withSoftDeleted() + .findById(request.params.executionId) + .throwIfNotFound(); + + const executionStepsQuery = execution + .$relatedQuery('executionSteps') + .withSoftDeleted() + .withGraphFetched('step') + .orderBy('created_at', 'asc'); + + const executionSteps = await paginateRest( + executionStepsQuery, + request.query.page + ); + + renderObject(response, executionSteps); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d93fbb11c58dd34fb5dc699fe36a1d89e388c717 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution-steps.test.js @@ -0,0 +1,153 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createExecution } from '../../../../../test/factories/execution.js'; +import { createExecutionStep } from '../../../../../test/factories/execution-step.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getExecutionStepsMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution-steps'; + +describe('GET /api/v1/executions/:executionId/execution-steps', () => { + let currentUser, currentUserRole, anotherUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + anotherUser = await createUser(); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the execution steps of current user execution', async () => { + const currentUserFlow = await createFlow({ + userId: currentUser.id, + }); + + const stepOne = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + }); + + const currentUserExecution = await createExecution({ + flowId: currentUserFlow.id, + }); + + const currentUserExecutionStepOne = await createExecutionStep({ + executionId: currentUserExecution.id, + stepId: stepOne.id, + }); + + const currentUserExecutionStepTwo = await createExecutionStep({ + executionId: currentUserExecution.id, + stepId: stepTwo.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/executions/${currentUserExecution.id}/execution-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionStepsMock( + [currentUserExecutionStepOne, currentUserExecutionStepTwo], + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the execution steps of another user execution', async () => { + const anotherUserFlow = await createFlow({ + userId: anotherUser.id, + }); + + const stepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const anotherUserExecution = await createExecution({ + flowId: anotherUserFlow.id, + }); + + const anotherUserExecutionStepOne = await createExecutionStep({ + executionId: anotherUserExecution.id, + stepId: stepOne.id, + }); + + const anotherUserExecutionStepTwo = await createExecutionStep({ + executionId: anotherUserExecution.id, + stepId: stepTwo.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/executions/${anotherUserExecution.id}/execution-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionStepsMock( + [anotherUserExecutionStepOne, anotherUserExecutionStepTwo], + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing execution step UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingExcecutionUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/executions/${notExistingExcecutionUUID}/execution-steps`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/executions/invalidExecutionUUID/execution-steps') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution.js b/packages/backend/src/controllers/api/v1/executions/get-execution.js new file mode 100644 index 0000000000000000000000000000000000000000..fe6d599c7916ef8b6e47ad7cc7ecd4e969491f07 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution.js @@ -0,0 +1,16 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const execution = await request.currentUser.authorizedExecutions + .clone() + .withGraphFetched({ + flow: { + steps: true, + }, + }) + .withSoftDeleted() + .findById(request.params.executionId) + .throwIfNotFound(); + + renderObject(response, execution); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-execution.test.js b/packages/backend/src/controllers/api/v1/executions/get-execution.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a6a86ce93e44d0d3663c91a4a1043301c08933e3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-execution.test.js @@ -0,0 +1,134 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createExecution } from '../../../../../test/factories/execution.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getExecutionMock from '../../../../../test/mocks/rest/api/v1/executions/get-execution'; + +describe('GET /api/v1/executions/:executionId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the execution data of current user', async () => { + const currentUserFlow = await createFlow({ + userId: currentUser.id, + }); + + const stepOne = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + }); + + const currentUserExecution = await createExecution({ + flowId: currentUserFlow.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/executions/${currentUserExecution.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionMock( + currentUserExecution, + currentUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the execution data of another user', async () => { + const anotherUser = await createUser(); + + const anotherUserFlow = await createFlow({ + userId: anotherUser.id, + }); + + const stepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const anotherUserExecution = await createExecution({ + flowId: anotherUserFlow.id, + }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/executions/${anotherUserExecution.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionMock( + anotherUserExecution, + anotherUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing execution UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingExcecutionUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/executions/${notExistingExcecutionUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/executions/invalidExecutionUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/executions/get-executions.js b/packages/backend/src/controllers/api/v1/executions/get-executions.js new file mode 100644 index 0000000000000000000000000000000000000000..bb4ca70ec4e3c9ec7f7a283e4ef132fc936d6f5a --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-executions.js @@ -0,0 +1,27 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const executionsQuery = request.currentUser.authorizedExecutions + .clone() + .withSoftDeleted() + .orderBy('created_at', 'desc') + .withGraphFetched({ + flow: { + steps: true, + }, + }); + + const executions = await paginateRest(executionsQuery, request.query.page); + + for (const execution of executions.records) { + const executionSteps = await execution.$relatedQuery('executionSteps'); + const status = executionSteps.some((step) => step.status === 'failure') + ? 'failure' + : 'success'; + + execution.status = status; + } + + renderObject(response, executions); +}; diff --git a/packages/backend/src/controllers/api/v1/executions/get-executions.test.js b/packages/backend/src/controllers/api/v1/executions/get-executions.test.js new file mode 100644 index 0000000000000000000000000000000000000000..49f29c9f001f0433e9b8b2668addb0ff396aac2e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/executions/get-executions.test.js @@ -0,0 +1,119 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createExecution } from '../../../../../test/factories/execution.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getExecutionsMock from '../../../../../test/mocks/rest/api/v1/executions/get-executions'; + +describe('GET /api/v1/executions', () => { + let currentUser, currentUserRole, anotherUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + anotherUser = await createUser(); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the executions of current user', async () => { + const currentUserFlow = await createFlow({ + userId: currentUser.id, + }); + + const stepOne = await createStep({ + flowId: currentUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: currentUserFlow.id, + type: 'action', + }); + + const currentUserExecutionOne = await createExecution({ + flowId: currentUserFlow.id, + }); + + const currentUserExecutionTwo = await createExecution({ + flowId: currentUserFlow.id, + }); + + await currentUserExecutionTwo + .$query() + .patchAndFetch({ deletedAt: new Date().toISOString() }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/executions') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionsMock( + [currentUserExecutionTwo, currentUserExecutionOne], + currentUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the executions of another user', async () => { + const anotherUserFlow = await createFlow({ + userId: anotherUser.id, + }); + + const stepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const stepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const anotherUserExecutionOne = await createExecution({ + flowId: anotherUserFlow.id, + }); + + const anotherUserExecutionTwo = await createExecution({ + flowId: anotherUserFlow.id, + }); + + await anotherUserExecutionTwo + .$query() + .patchAndFetch({ deletedAt: new Date().toISOString() }); + + await createPermission({ + action: 'read', + subject: 'Execution', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/executions') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getExecutionsMock( + [anotherUserExecutionTwo, anotherUserExecutionOne], + anotherUserFlow, + [stepOne, stepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/flows/get-flow.js b/packages/backend/src/controllers/api/v1/flows/get-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..004746967dfb036521f8e269ba3d12472466982c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flow.js @@ -0,0 +1,12 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const flow = await request.currentUser.authorizedFlows + .clone() + .withGraphJoined({ steps: true }) + .orderBy('steps.position', 'asc') + .findOne({ 'flows.id': request.params.flowId }) + .throwIfNotFound(); + + renderObject(response, flow); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/get-flow.test.js b/packages/backend/src/controllers/api/v1/flows/get-flow.test.js new file mode 100644 index 0000000000000000000000000000000000000000..cd150f5a478f730a338515795a4c30a42f64f69e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flow.test.js @@ -0,0 +1,102 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getFlowMock from '../../../../../test/mocks/rest/api/v1/flows/get-flow'; + +describe('GET /api/v1/flows/:flowId', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flow data of current user', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + const triggerStep = await createStep({ flowId: currentUserflow.id }); + const actionStep = await createStep({ flowId: currentUserflow.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/flows/${currentUserflow.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowMock(currentUserflow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flow data of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + const triggerStep = await createStep({ flowId: anotherUserFlow.id }); + const actionStep = await createStep({ flowId: anotherUserFlow.id }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/flows/${anotherUserFlow.id}`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowMock(anotherUserFlow, [ + triggerStep, + actionStep, + ]); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing flow UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/flows/${notExistingFlowUUID}`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/flows/invalidFlowUUID') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/flows/get-flows.js b/packages/backend/src/controllers/api/v1/flows/get-flows.js new file mode 100644 index 0000000000000000000000000000000000000000..92e79fbedf32ec66899fbcd2c4ab8e880b9415c9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.js @@ -0,0 +1,21 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import paginateRest from '../../../../helpers/pagination-rest.js'; + +export default async (request, response) => { + const flowsQuery = request.currentUser.authorizedFlows + .clone() + .withGraphFetched({ + steps: true, + }) + .where((builder) => { + if (request.query.name) { + builder.where('flows.name', 'ilike', `%${request.query.name}%`); + } + }) + .orderBy('active', 'desc') + .orderBy('updated_at', 'desc'); + + const flows = await paginateRest(flowsQuery, request.query.page); + + renderObject(response, flows); +}; diff --git a/packages/backend/src/controllers/api/v1/flows/get-flows.test.js b/packages/backend/src/controllers/api/v1/flows/get-flows.test.js new file mode 100644 index 0000000000000000000000000000000000000000..3df8a2bb25faa6824c0a81d3ef7bae1f429c7297 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/flows/get-flows.test.js @@ -0,0 +1,118 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getFlowsMock from '../../../../../test/mocks/rest/api/v1/flows/get-flows.js'; + +describe('GET /api/v1/flows', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the flows data of current user', async () => { + const currentUserFlowOne = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'trigger', + }); + const actionStepFlowOne = await createStep({ + flowId: currentUserFlowOne.id, + type: 'action', + }); + + const currentUserFlowTwo = await createFlow({ userId: currentUser.id }); + + const triggerStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'trigger', + }); + const actionStepFlowTwo = await createStep({ + flowId: currentUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get('/api/v1/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [currentUserFlowTwo, currentUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + ] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the flows data of another user', async () => { + const anotherUser = await createUser(); + + const anotherUserFlowOne = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'trigger', + }); + const actionStepFlowOne = await createStep({ + flowId: anotherUserFlowOne.id, + type: 'action', + }); + + const anotherUserFlowTwo = await createFlow({ userId: anotherUser.id }); + + const triggerStepFlowTwo = await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'trigger', + }); + const actionStepFlowTwo = await createStep({ + flowId: anotherUserFlowTwo.id, + type: 'action', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get('/api/v1/flows') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getFlowsMock( + [anotherUserFlowTwo, anotherUserFlowOne], + [ + triggerStepFlowOne, + actionStepFlowOne, + triggerStepFlowTwo, + actionStepFlowTwo, + ] + ); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.js new file mode 100644 index 0000000000000000000000000000000000000000..84172310299be157f9061c0d32c28dabe1627992 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.js @@ -0,0 +1,9 @@ +import User from '../../../../../models/user.js'; + +export default async (request, response) => { + const { email, password, fullName } = request.body; + + await User.createAdmin({ email, password, fullName }); + + response.status(204).end(); +}; diff --git a/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a157dbb3d7d73042bd79480227dff16a65bd2c8d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/installation/users/create-user.test.js @@ -0,0 +1,84 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../../app.js'; +import Config from '../../../../../models/config.js'; +import User from '../../../../../models/user.js'; +import { createRole } from '../../../../../../test/factories/role'; +import { createUser } from '../../../../../../test/factories/user'; +import { createInstallationCompletedConfig } from '../../../../../../test/factories/config'; + +describe('POST /api/v1/installation/users', () => { + let adminRole; + + beforeEach(async () => { + adminRole = await createRole({ + name: 'Admin', + key: 'admin', + }) + }); + + describe('for incomplete installations', () => { + it('should respond with HTTP 204 with correct payload when no user', async () => { + expect(await Config.isInstallationCompleted()).toBe(false); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(204); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user.roleId).toBe(adminRole.id); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + + it('should respond with HTTP 403 with correct payload when one user exists at least', async () => { + expect(await Config.isInstallationCompleted()).toBe(false); + + await createUser(); + + const usersCountBefore = await User.query().resultSize(); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(403); + + const usersCountAfter = await User.query().resultSize(); + + expect(usersCountBefore).toEqual(usersCountAfter); + }); + }); + + describe('for completed installations', () => { + beforeEach(async () => { + await createInstallationCompletedConfig(); + }); + + it('should respond with HTTP 403 when installation completed', async () => { + expect(await Config.isInstallationCompleted()).toBe(true); + + await request(app) + .post('/api/v1/installation/users') + .send({ + email: 'user@automatisch.io', + password: 'password', + fullName: 'Initial admin' + }) + .expect(403); + + const user = await User.query().findOne({ email: 'user@automatisch.io' }); + + expect(user).toBeUndefined(); + expect(await Config.isInstallationCompleted()).toBe(true); + }); + }) +}); diff --git a/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.js b/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..ca4fefe4bfd80d6b3403b82ee3875db4a9da1634 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.js @@ -0,0 +1,8 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import Billing from '../../../../helpers/billing/index.ee.js'; + +export default async (request, response) => { + const paddleInfo = Billing.paddleInfo; + + renderObject(response, paddleInfo); +}; diff --git a/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.test.js b/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7fccd60f248469e736e8e7fab22b6893faac67b1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/payment/get-paddle-info.ee.test.js @@ -0,0 +1,33 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getPaddleInfoMock from '../../../../../test/mocks/rest/api/v1/payment/get-paddle-info.js'; +import appConfig from '../../../../config/app.js'; +import billing from '../../../../helpers/billing/index.ee.js'; + +describe('GET /api/v1/payment/paddle-info', () => { + let user, token; + + beforeEach(async () => { + user = await createUser(); + token = await createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + vi.spyOn(billing.paddleInfo, 'vendorId', 'get').mockReturnValue( + 'sampleVendorId' + ); + }); + + it('should return payment plans', async () => { + const response = await request(app) + .get('/api/v1/payment/paddle-info') + .set('Authorization', token) + .expect(200); + + const expectedResponsePayload = await getPaddleInfoMock(); + + expect(response.body).toEqual(expectedResponsePayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/payment/get-plans.ee.js b/packages/backend/src/controllers/api/v1/payment/get-plans.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..32c0bf8c4b7c2ce165b253d6c441c5b8465aad93 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/payment/get-plans.ee.js @@ -0,0 +1,8 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import Billing from '../../../../helpers/billing/index.ee.js'; + +export default async (request, response) => { + const paymentPlans = Billing.paddlePlans; + + renderObject(response, paymentPlans); +}; diff --git a/packages/backend/src/controllers/api/v1/payment/get-plans.ee.test.js b/packages/backend/src/controllers/api/v1/payment/get-plans.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..4f7665aa5b0c96e47a11eff5acd3a6eb72cb45e9 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/payment/get-plans.ee.test.js @@ -0,0 +1,29 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getPaymentPlansMock from '../../../../../test/mocks/rest/api/v1/payment/get-plans.js'; +import appConfig from '../../../../config/app.js'; + +describe('GET /api/v1/payment/plans', () => { + let user, token; + + beforeEach(async () => { + user = await createUser(); + token = await createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + }); + + it('should return payment plans', async () => { + const response = await request(app) + .get('/api/v1/payment/plans') + .set('Authorization', token) + .expect(200); + + const expectedResponsePayload = await getPaymentPlansMock(); + + expect(response.body).toEqual(expectedResponsePayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js b/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..3b066438e50453feb52add2e74494371f911e1ba --- /dev/null +++ b/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js @@ -0,0 +1,12 @@ +import { renderObject } from '../../../../helpers/renderer.js'; +import SamlAuthProvider from '../../../../models/saml-auth-provider.ee.js'; + +export default async (request, response) => { + const samlAuthProviders = await SamlAuthProvider.query() + .where({ + active: true, + }) + .orderBy('created_at', 'desc'); + + renderObject(response, samlAuthProviders); +}; diff --git a/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.test.js b/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f2f21b7dc9aae980ef14c38795e2a48330266168 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.test.js @@ -0,0 +1,30 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import { createSamlAuthProvider } from '../../../../../test/factories/saml-auth-provider.ee.js'; +import getSamlAuthProvidersMock from '../../../../../test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.js'; +import * as license from '../../../../helpers/license.ee.js'; + +describe('GET /api/v1/saml-auth-providers', () => { + let samlAuthProviderOne, samlAuthProviderTwo; + + beforeEach(async () => { + samlAuthProviderOne = await createSamlAuthProvider(); + samlAuthProviderTwo = await createSamlAuthProvider(); + }); + + it('should return saml auth providers', async () => { + vi.spyOn(license, 'hasValidLicense').mockResolvedValue(true); + + const response = await request(app) + .get('/api/v1/saml-auth-providers') + .expect(200); + + const expectedPayload = await getSamlAuthProvidersMock([ + samlAuthProviderTwo, + samlAuthProviderOne, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.js new file mode 100644 index 0000000000000000000000000000000000000000..90cb4d8f1162c66ef4b079a2af9125804e2bf317 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.js @@ -0,0 +1,18 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .clone() + .where('steps.id', request.params.stepId) + .whereNotNull('steps.app_key') + .whereNotNull('steps.connection_id') + .first() + .throwIfNotFound(); + + const dynamicData = await step.createDynamicData( + request.body.dynamicDataKey, + request.body.parameters + ); + + renderObject(response, dynamicData); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a83ac23bbdfb4e53bf8b38e52c2639135dbda5c1 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-data.test.js @@ -0,0 +1,244 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createConnection } from '../../../../../test/factories/connection'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import listRepos from '../../../../apps/github/dynamic-data/list-repos/index.js'; +import HttpError from '../../../../errors/http.js'; + +describe('POST /api/v1/steps/:stepId/dynamic-data', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + describe('should return dynamically created data', () => { + let repositories; + + beforeEach(async () => { + repositories = [ + { + value: 'automatisch/automatisch', + name: 'automatisch/automatisch', + }, + { + value: 'automatisch/sample', + name: 'automatisch/sample', + }, + ]; + + vi.spyOn(listRepos, 'run').mockImplementation(async () => { + return { + data: repositories, + }; + }); + }); + + it('of the current users step', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + const connection = await createConnection({ userId: currentUser.id }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + connectionId: connection.id, + type: 'action', + appKey: 'github', + key: 'createIssue', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-data`) + .set('Authorization', token) + .send({ + dynamicDataKey: 'listRepos', + parameters: {}, + }) + .expect(200); + + expect(response.body.data).toEqual(repositories); + }); + + it('of the another users step', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + const connection = await createConnection({ userId: anotherUser.id }); + + const actionStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: connection.id, + type: 'action', + appKey: 'github', + key: 'createIssue', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-data`) + .set('Authorization', token) + .send({ + dynamicDataKey: 'listRepos', + parameters: {}, + }) + .expect(200); + + expect(response.body.data).toEqual(repositories); + }); + }); + + describe('should return error for dynamically created data', () => { + let errors; + + beforeEach(async () => { + errors = { + message: 'Not Found', + documentation_url: + 'https://docs.github.com/rest/users/users#get-a-user', + }; + + vi.spyOn(listRepos, 'run').mockImplementation(async () => { + throw new HttpError({ message: errors }); + }); + }); + + it('of the current users step', async () => { + const currentUserFlow = await createFlow({ userId: currentUser.id }); + const connection = await createConnection({ userId: currentUser.id }); + + const actionStep = await createStep({ + flowId: currentUserFlow.id, + connectionId: connection.id, + type: 'action', + appKey: 'github', + key: 'createIssue', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-data`) + .set('Authorization', token) + .send({ + dynamicDataKey: 'listRepos', + parameters: {}, + }) + .expect(200); + + expect(response.body.errors).toEqual(errors); + }); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingStepUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-data`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for existing step UUID without app key', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const step = await createStep({ appKey: null }); + + await request(app) + .get(`/api/v1/steps/${step.id}/dynamic-data`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .post('/api/v1/steps/invalidStepUUID/dynamic-fields') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js new file mode 100644 index 0000000000000000000000000000000000000000..f1315dfaf21aa5a66f89268ed4adc119d2772bd2 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.js @@ -0,0 +1,17 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .clone() + .where('steps.id', request.params.stepId) + .whereNotNull('steps.app_key') + .first() + .throwIfNotFound(); + + const dynamicFields = await step.createDynamicFields( + request.body.dynamicFieldsKey, + request.body.parameters + ); + + renderObject(response, dynamicFields); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7ae163b52d78dab0dfe35e8ce28dc68a75a8631b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/create-dynamic-fields.test.js @@ -0,0 +1,169 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import createDynamicFieldsMock from '../../../../../test/mocks/rest/api/v1/steps/create-dynamic-fields'; + +describe('POST /api/v1/steps/:stepId/dynamic-fields', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return dynamically created fields of the current users step', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const actionStep = await createStep({ + flowId: currentUserflow.id, + type: 'action', + appKey: 'slack', + key: 'sendMessageToChannel', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-fields`) + .set('Authorization', token) + .send({ + dynamicFieldsKey: 'listFieldsAfterSendAsBot', + parameters: { + sendAsBot: true, + }, + }) + .expect(200); + + const expectedPayload = await createDynamicFieldsMock(); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return dynamically created fields of the another users step', async () => { + const anotherUser = await createUser(); + const anotherUserflow = await createFlow({ userId: anotherUser.id }); + + const actionStep = await createStep({ + flowId: anotherUserflow.id, + type: 'action', + appKey: 'slack', + key: 'sendMessageToChannel', + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .post(`/api/v1/steps/${actionStep.id}/dynamic-fields`) + .set('Authorization', token) + .send({ + dynamicFieldsKey: 'listFieldsAfterSendAsBot', + parameters: { + sendAsBot: true, + }, + }) + .expect(200); + + const expectedPayload = await createDynamicFieldsMock(); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingStepUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingStepUUID}/dynamic-fields`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for existing step UUID without app key', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const step = await createStep({ appKey: null }); + + await request(app) + .get(`/api/v1/steps/${step.id}/dynamic-fields`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .post('/api/v1/steps/invalidStepUUID/dynamic-fields') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.js b/packages/backend/src/controllers/api/v1/steps/get-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..ab1a403e77b28c3580067963af93d451fc7481de --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.js @@ -0,0 +1,11 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .findById(request.params.stepId) + .throwIfNotFound(); + + const connection = await step.$relatedQuery('connection').throwIfNotFound(); + + renderObject(response, connection); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/get-connection.test.js b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js new file mode 100644 index 0000000000000000000000000000000000000000..27e6151e4c57557806461613e56716a7112e1755 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-connection.test.js @@ -0,0 +1,121 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createConnection } from '../../../../../test/factories/connection'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createPermission } from '../../../../../test/factories/permission'; +import getConnectionMock from '../../../../../test/mocks/rest/api/v1/steps/get-connection'; + +describe('GET /api/v1/steps/:stepId/connection', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the current user connection data of specified step', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const currentUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: currentUserflow.id, + connectionId: currentUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(currentUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the current user connection data of specified step', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const anotherUserConnection = await createConnection(); + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + connectionId: anotherUserConnection.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/steps/${triggerStep.id}/connection`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getConnectionMock(anotherUserConnection); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step without connection', async () => { + const stepWithoutConnection = await createStep(); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get(`/api/v1/steps/${stepWithoutConnection.id}/connection`) + .set('Authorization', token) + .expect(404); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingFlowUUID}/connection`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/steps/invalidFlowUUID/connection') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..e9e865a22d54fa6f0e1f7b1fa2dc2467e8e3bab8 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.js @@ -0,0 +1,27 @@ +import { ref } from 'objection'; +import ExecutionStep from '../../../../models/execution-step.js'; +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const step = await request.currentUser.authorizedSteps + .clone() + .findOne({ 'steps.id': request.params.stepId }) + .throwIfNotFound(); + + const previousSteps = await request.currentUser.authorizedSteps + .clone() + .withGraphJoined('executionSteps') + .where('flow_id', '=', step.flowId) + .andWhere('position', '<', step.position) + .andWhere( + 'executionSteps.created_at', + '=', + ExecutionStep.query() + .max('created_at') + .where('step_id', '=', ref('steps.id')) + .andWhere('status', 'success') + ) + .orderBy('steps.position', 'asc'); + + renderObject(response, previousSteps); +}; diff --git a/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f9205295329dd6490392c43c9bcafe48a895a49c --- /dev/null +++ b/packages/backend/src/controllers/api/v1/steps/get-previous-steps.test.js @@ -0,0 +1,173 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import Crypto from 'crypto'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import { createFlow } from '../../../../../test/factories/flow'; +import { createStep } from '../../../../../test/factories/step'; +import { createExecutionStep } from '../../../../../test/factories/execution-step.js'; +import { createPermission } from '../../../../../test/factories/permission'; +import getPreviousStepsMock from '../../../../../test/mocks/rest/api/v1/steps/get-previous-steps'; + +describe('GET /api/v1/steps/:stepId/previous-steps', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUser = await createUser(); + currentUserRole = await currentUser.$relatedQuery('role'); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return the previous steps of the specified step of the current user', async () => { + const currentUserflow = await createFlow({ userId: currentUser.id }); + + const triggerStep = await createStep({ + flowId: currentUserflow.id, + type: 'trigger', + }); + + const actionStepOne = await createStep({ + flowId: currentUserflow.id, + type: 'action', + }); + + const actionStepTwo = await createStep({ + flowId: currentUserflow.id, + type: 'action', + }); + + const executionStepOne = await createExecutionStep({ + stepId: triggerStep.id, + }); + + const executionStepTwo = await createExecutionStep({ + stepId: actionStepOne.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const response = await request(app) + .get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getPreviousStepsMock( + [triggerStep, actionStepOne], + [executionStepOne, executionStepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return the previous steps of the specified step of another user', async () => { + const anotherUser = await createUser(); + const anotherUserFlow = await createFlow({ userId: anotherUser.id }); + + const triggerStep = await createStep({ + flowId: anotherUserFlow.id, + type: 'trigger', + }); + + const actionStepOne = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const actionStepTwo = await createStep({ + flowId: anotherUserFlow.id, + type: 'action', + }); + + const executionStepOne = await createExecutionStep({ + stepId: triggerStep.id, + }); + + const executionStepTwo = await createExecutionStep({ + stepId: actionStepOne.id, + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const response = await request(app) + .get(`/api/v1/steps/${actionStepTwo.id}/previous-steps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getPreviousStepsMock( + [triggerStep, actionStepOne], + [executionStepOne, executionStepTwo] + ); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response for not existing step UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + const notExistingFlowUUID = Crypto.randomUUID(); + + await request(app) + .get(`/api/v1/steps/${notExistingFlowUUID}/previous-steps`) + .set('Authorization', token) + .expect(404); + }); + + it('should return bad request response for invalid UUID', async () => { + await createPermission({ + action: 'update', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await request(app) + .get('/api/v1/steps/invalidFlowUUID/previous-steps') + .set('Authorization', token) + .expect(400); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-apps.js b/packages/backend/src/controllers/api/v1/users/get-apps.js new file mode 100644 index 0000000000000000000000000000000000000000..801fbc7145d69d109c40efc46ada3922f47a3f8b --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-apps.js @@ -0,0 +1,7 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const apps = await request.currentUser.getApps(request.query.name); + + renderObject(response, apps, { serializer: 'App' }); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-apps.test.js b/packages/backend/src/controllers/api/v1/users/get-apps.test.js new file mode 100644 index 0000000000000000000000000000000000000000..eb24b66aa7db7d8dda13f64dc3112fe45641b531 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-apps.test.js @@ -0,0 +1,210 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createRole } from '../../../../../test/factories/role'; +import { createUser } from '../../../../../test/factories/user'; +import { createPermission } from '../../../../../test/factories/permission.js'; +import { createFlow } from '../../../../../test/factories/flow.js'; +import { createStep } from '../../../../../test/factories/step.js'; +import { createConnection } from '../../../../../test/factories/connection.js'; +import getAppsMock from '../../../../../test/mocks/rest/api/v1/users/get-apps.js'; + +describe('GET /api/v1/users/:userId/apps', () => { + let currentUser, currentUserRole, token; + + beforeEach(async () => { + currentUserRole = await createRole(); + currentUser = await createUser({ roleId: currentUserRole.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return all apps of the current user', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const flowOne = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: flowOne.id, + appKey: 'webhook', + }); + + const flowOneActionStepConnection = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + await createStep({ + connectionId: flowOneActionStepConnection.id, + flowId: flowOne.id, + appKey: 'deepl', + }); + + const flowTwo = await createFlow({ userId: currentUser.id }); + + const flowTwoTriggerStepConnection = await createConnection({ + userId: currentUser.id, + key: 'github', + draft: false, + }); + + await createStep({ + connectionId: flowTwoTriggerStepConnection.id, + flowId: flowTwo.id, + appKey: 'github', + }); + + await createStep({ + flowId: flowTwo.id, + appKey: 'slack', + }); + + const response = await request(app) + .get(`/api/v1/users/${currentUser.id}/apps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return all apps of the another user', async () => { + const anotherUser = await createUser(); + + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: [], + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: [], + }); + + const flowOne = await createFlow({ userId: anotherUser.id }); + + await createStep({ + flowId: flowOne.id, + appKey: 'webhook', + }); + + const flowOneActionStepConnection = await createConnection({ + userId: anotherUser.id, + key: 'deepl', + draft: false, + }); + + await createStep({ + connectionId: flowOneActionStepConnection.id, + flowId: flowOne.id, + appKey: 'deepl', + }); + + const flowTwo = await createFlow({ userId: anotherUser.id }); + + const flowTwoTriggerStepConnection = await createConnection({ + userId: anotherUser.id, + key: 'github', + draft: false, + }); + + await createStep({ + connectionId: flowTwoTriggerStepConnection.id, + flowId: flowTwo.id, + appKey: 'github', + }); + + await createStep({ + flowId: flowTwo.id, + appKey: 'slack', + }); + + const response = await request(app) + .get(`/api/v1/users/${currentUser.id}/apps`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getAppsMock(); + expect(response.body).toEqual(expectedPayload); + }); + + it('should return specified app of the current user', async () => { + await createPermission({ + action: 'read', + subject: 'Flow', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + await createPermission({ + action: 'read', + subject: 'Connection', + roleId: currentUserRole.id, + conditions: ['isCreator'], + }); + + const flowOne = await createFlow({ userId: currentUser.id }); + + await createStep({ + flowId: flowOne.id, + appKey: 'webhook', + }); + + const flowOneActionStepConnection = await createConnection({ + userId: currentUser.id, + key: 'deepl', + draft: false, + }); + + await createStep({ + connectionId: flowOneActionStepConnection.id, + flowId: flowOne.id, + appKey: 'deepl', + }); + + const flowTwo = await createFlow({ userId: currentUser.id }); + + const flowTwoTriggerStepConnection = await createConnection({ + userId: currentUser.id, + key: 'github', + draft: false, + }); + + await createStep({ + connectionId: flowTwoTriggerStepConnection.id, + flowId: flowTwo.id, + appKey: 'github', + }); + + await createStep({ + flowId: flowTwo.id, + appKey: 'slack', + }); + + const response = await request(app) + .get(`/api/v1/users/${currentUser.id}/apps?name=deepl`) + .set('Authorization', token) + .expect(200); + + expect(response.body.data.length).toEqual(1); + expect(response.body.data[0].key).toEqual('deepl'); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-current-user.js b/packages/backend/src/controllers/api/v1/users/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..7008168842d8d14b47004500c8d58ca5983513d5 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-current-user.js @@ -0,0 +1,5 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + renderObject(response, request.currentUser); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-current-user.test.js b/packages/backend/src/controllers/api/v1/users/get-current-user.test.js new file mode 100644 index 0000000000000000000000000000000000000000..362c9815d33ce7847447ef2f98682f2f67a8d07e --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-current-user.test.js @@ -0,0 +1,44 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createPermission } from '../../../../../test/factories/permission'; +import { createRole } from '../../../../../test/factories/role'; +import { createUser } from '../../../../../test/factories/user'; +import getCurrentUserMock from '../../../../../test/mocks/rest/api/v1/users/get-current-user'; + +describe('GET /api/v1/users/me', () => { + let role, permissionOne, permissionTwo, currentUser, token; + + beforeEach(async () => { + role = await createRole(); + + permissionOne = await createPermission({ + roleId: role.id, + }); + + permissionTwo = await createPermission({ + roleId: role.id, + }); + + currentUser = await createUser({ + roleId: role.id, + }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return current user info', async () => { + const response = await request(app) + .get('/api/v1/users/me') + .set('Authorization', token) + .expect(200); + + const expectedPayload = getCurrentUserMock(currentUser, role, [ + permissionOne, + permissionTwo, + ]); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-invoices.ee.js b/packages/backend/src/controllers/api/v1/users/get-invoices.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..ec6e2efd37a2f1715e7fa6c9e79d5a2296e53a44 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-invoices.ee.js @@ -0,0 +1,7 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const invoices = await request.currentUser.getInvoices(); + + renderObject(response, invoices); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-invoices.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-invoices.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..1b0e87327036b74e8e890cb056fea9aa8160a769 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-invoices.ee.test.js @@ -0,0 +1,34 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createUser } from '../../../../../test/factories/user'; +import User from '../../../../models/user'; +import getInvoicesMock from '../../../../../test/mocks/rest/api/v1/users/get-invoices.ee'; + +describe('GET /api/v1/user/invoices', () => { + let currentUser, token; + + beforeEach(async () => { + currentUser = await createUser(); + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return current user invoices', async () => { + const invoices = [ + { id: 1, amount: 100, description: 'Invoice 1' }, + { id: 2, amount: 200, description: 'Invoice 2' }, + ]; + + vi.spyOn(User.prototype, 'getInvoices').mockResolvedValue(invoices); + + const response = await request(app) + .get('/api/v1/users/invoices') + .set('Authorization', token) + .expect(200); + + const expectedPayload = await getInvoicesMock(invoices); + + expect(response.body).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..bda4c4f14eed9bf3ab75dd325fa07675b6b798d3 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.js @@ -0,0 +1,7 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const planAndUsage = await request.currentUser.getPlanAndUsage(); + + renderObject(response, planAndUsage); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..4986ad2be277071a686e4bb22e72025539a83e35 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-plan-and-usage.ee.test.js @@ -0,0 +1,68 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import { createSubscription } from '../../../../../test/factories/subscription.js'; +import { createUsageData } from '../../../../../test/factories/usage-data.js'; +import appConfig from '../../../../config/app.js'; +import { DateTime } from 'luxon'; + +describe('GET /api/v1/users/:userId/plan-and-usage', () => { + let user, token; + + beforeEach(async () => { + const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); + user = await createUser({ trialExpiryDate }); + token = await createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + }); + + it('should return free trial plan and usage data', async () => { + const response = await request(app) + .get(`/api/v1/users/${user.id}/plan-and-usage`) + .set('Authorization', token) + .expect(200); + + const expectedResponseData = { + plan: { + id: null, + limit: null, + name: 'Free Trial', + }, + usage: { + task: 0, + }, + }; + + expect(response.body.data).toEqual(expectedResponseData); + }); + + it('should return current plan and usage data', async () => { + await createSubscription({ userId: user.id }); + + await createUsageData({ + userId: user.id, + consumedTaskCount: 1234, + }); + + const response = await request(app) + .get(`/api/v1/users/${user.id}/plan-and-usage`) + .set('Authorization', token) + .expect(200); + + const expectedResponseData = { + plan: { + id: '47384', + limit: '10,000', + name: '10k - monthly', + }, + usage: { + task: 1234, + }, + }; + + expect(response.body.data).toEqual(expectedResponseData); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..afecacc7367a5066856579bb0f7d182c4123a179 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.js @@ -0,0 +1,9 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const subscription = await request.currentUser + .$relatedQuery('currentSubscription') + .throwIfNotFound(); + + renderObject(response, subscription); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..24c6db47f52eb3eaf6f3821205db49ab4a7a3252 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-subscription.ee.test.js @@ -0,0 +1,51 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import appConfig from '../../../../config/app.js'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id'; +import { createRole } from '../../../../../test/factories/role'; +import { createUser } from '../../../../../test/factories/user'; +import { createSubscription } from '../../../../../test/factories/subscription.js'; +import getSubscriptionMock from '../../../../../test/mocks/rest/api/v1/users/get-subscription.js'; + +describe('GET /api/v1/users/:userId/subscription', () => { + let currentUser, role, subscription, token; + + beforeEach(async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + role = await createRole(); + + currentUser = await createUser({ + roleId: role.id, + }); + + subscription = await createSubscription({ userId: currentUser.id }); + + token = await createAuthTokenByUserId(currentUser.id); + }); + + it('should return subscription info of the current user', async () => { + const response = await request(app) + .get(`/api/v1/users/${currentUser.id}/subscription`) + .set('Authorization', token) + .expect(200); + + const expectedPayload = getSubscriptionMock(subscription); + + expect(response.body).toEqual(expectedPayload); + }); + + it('should return not found response if there is no current subscription', async () => { + const userWithoutSubscription = await createUser({ + roleId: role.id, + }); + + const token = await createAuthTokenByUserId(userWithoutSubscription.id); + + await request(app) + .get(`/api/v1/users/${userWithoutSubscription.id}/subscription`) + .set('Authorization', token) + .expect(404); + }); +}); diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1c4575bb3921c79aa61eef780e6505ce2f3c8a72 --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.js @@ -0,0 +1,12 @@ +import { renderObject } from '../../../../helpers/renderer.js'; + +export default async (request, response) => { + const inTrial = await request.currentUser.inTrial(); + + const trialInfo = { + inTrial, + expireAt: request.currentUser.trialExpiryDate, + }; + + renderObject(response, trialInfo); +}; diff --git a/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..f560554b296cb6b1037316253c1bed32de24b40d --- /dev/null +++ b/packages/backend/src/controllers/api/v1/users/get-user-trial.ee.test.js @@ -0,0 +1,38 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import request from 'supertest'; +import app from '../../../../app.js'; +import createAuthTokenByUserId from '../../../../helpers/create-auth-token-by-user-id.js'; +import { createUser } from '../../../../../test/factories/user.js'; +import getUserTrialMock from '../../../../../test/mocks/rest/api/v1/users/get-user-trial.js'; +import appConfig from '../../../../config/app.js'; +import { DateTime } from 'luxon'; +import User from '../../../../models/user.js'; + +describe('GET /api/v1/users/:userId/trial', () => { + let user, token; + + beforeEach(async () => { + const trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); + user = await createUser({ trialExpiryDate }); + token = await createAuthTokenByUserId(user.id); + + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + }); + + describe('should return in trial, active subscription and expire at info', () => { + beforeEach(async () => { + vi.spyOn(User.prototype, 'inTrial').mockResolvedValue(false); + vi.spyOn(User.prototype, 'hasActiveSubscription').mockResolvedValue(true); + }); + + it('should return null', async () => { + const response = await request(app) + .get(`/api/v1/users/${user.id}/trial`) + .set('Authorization', token) + .expect(200); + + const expectedResponsePayload = await getUserTrialMock(user); + expect(response.body).toEqual(expectedResponsePayload); + }); + }); +}); diff --git a/packages/backend/src/controllers/healthcheck/index.js b/packages/backend/src/controllers/healthcheck/index.js new file mode 100644 index 0000000000000000000000000000000000000000..6305ab5ed6f47c2b4ce579fe8faff725f164da66 --- /dev/null +++ b/packages/backend/src/controllers/healthcheck/index.js @@ -0,0 +1,3 @@ +export default async (request, response) => { + response.status(200).end(); +}; diff --git a/packages/backend/src/controllers/healthcheck/index.test.js b/packages/backend/src/controllers/healthcheck/index.test.js new file mode 100644 index 0000000000000000000000000000000000000000..feb49281f2d142a13137fe2cd288a88be449bc36 --- /dev/null +++ b/packages/backend/src/controllers/healthcheck/index.test.js @@ -0,0 +1,9 @@ +import { describe, it } from 'vitest'; +import request from 'supertest'; +import app from '../../app.js'; + +describe('GET /healthcheck', () => { + it('should return 200 response with version data', async () => { + await request(app).get('/healthcheck').expect(200); + }); +}); diff --git a/packages/backend/src/controllers/paddle/webhooks.ee.js b/packages/backend/src/controllers/paddle/webhooks.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..b2d3e509892d1315943bd5d7c7b146b49f8eb837 --- /dev/null +++ b/packages/backend/src/controllers/paddle/webhooks.ee.js @@ -0,0 +1,47 @@ +import crypto from 'crypto'; +import { serialize } from 'php-serialize'; +import Billing from '../../helpers/billing/index.ee.js'; +import appConfig from '../../config/app.js'; + +export default async (request, response) => { + if (!verifyWebhook(request)) { + return response.sendStatus(401); + } + + if (request.body.alert_name === 'subscription_created') { + await Billing.webhooks.handleSubscriptionCreated(request); + } else if (request.body.alert_name === 'subscription_updated') { + await Billing.webhooks.handleSubscriptionUpdated(request); + } else if (request.body.alert_name === 'subscription_cancelled') { + await Billing.webhooks.handleSubscriptionCancelled(request); + } else if (request.body.alert_name === 'subscription_payment_succeeded') { + await Billing.webhooks.handleSubscriptionPaymentSucceeded(request); + } + + return response.sendStatus(200); +}; + +const verifyWebhook = (request) => { + const signature = request.body.p_signature; + + const keys = Object.keys(request.body) + .filter((key) => key !== 'p_signature') + .sort(); + + const sorted = {}; + keys.forEach((key) => { + sorted[key] = request.body[key]; + }); + + const serialized = serialize(sorted); + + try { + const verifier = crypto.createVerify('sha1'); + verifier.write(serialized); + verifier.end(); + + return verifier.verify(appConfig.paddlePublicKey, signature, 'base64'); + } catch (err) { + return false; + } +}; diff --git a/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js b/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js new file mode 100644 index 0000000000000000000000000000000000000000..2f5c611f00dfc70f206899d487bbe3f7314f31c3 --- /dev/null +++ b/packages/backend/src/controllers/webhooks/handler-by-connection-id-and-ref-value.js @@ -0,0 +1,38 @@ +import path from 'node:path'; + +import Connection from '../../models/connection.js'; +import logger from '../../helpers/logger.js'; +import handler from '../../helpers/webhook-handler.js'; + +export default async (request, response) => { + const computedRequestPayload = { + headers: request.headers, + body: request.body, + query: request.query, + params: request.params, + }; + logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`); + logger.debug(JSON.stringify(computedRequestPayload, null, 2)); + + const { connectionId } = request.params; + + const connection = await Connection.query() + .findById(connectionId) + .throwIfNotFound(); + + if (!(await connection.verifyWebhook(request))) { + return response.sendStatus(401); + } + + const triggerSteps = await connection + .$relatedQuery('triggerSteps') + .where('webhook_path', path.join(request.baseUrl, request.path)); + + if (triggerSteps.length === 0) return response.sendStatus(404); + + for (const triggerStep of triggerSteps) { + await handler(triggerStep.flowId, request, response); + } + + response.sendStatus(204); +}; diff --git a/packages/backend/src/controllers/webhooks/handler-by-flow-id.js b/packages/backend/src/controllers/webhooks/handler-by-flow-id.js new file mode 100644 index 0000000000000000000000000000000000000000..5ed0fd672f493b86aa0580e623ae5d35809a29a3 --- /dev/null +++ b/packages/backend/src/controllers/webhooks/handler-by-flow-id.js @@ -0,0 +1,31 @@ +import Flow from '../../models/flow.js'; +import logger from '../../helpers/logger.js'; +import handler from '../../helpers/webhook-handler.js'; + +export default async (request, response) => { + const computedRequestPayload = { + headers: request.headers, + body: request.body, + query: request.query, + params: request.params, + }; + + logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`); + logger.debug(JSON.stringify(computedRequestPayload, null, 2)); + + const flowId = request.params.flowId; + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const triggerStep = await flow.getTriggerStep(); + + if (triggerStep.appKey !== 'webhook') { + const connection = await triggerStep.$relatedQuery('connection'); + + if (!(await connection.verifyWebhook(request))) { + return response.sendStatus(401); + } + } + + await handler(flowId, request, response); + + response.sendStatus(204); +}; diff --git a/packages/backend/src/controllers/webhooks/handler-sync-by-flow-id.js b/packages/backend/src/controllers/webhooks/handler-sync-by-flow-id.js new file mode 100644 index 0000000000000000000000000000000000000000..64c934e88c27daa31d220a77c56e8dc159132131 --- /dev/null +++ b/packages/backend/src/controllers/webhooks/handler-sync-by-flow-id.js @@ -0,0 +1,29 @@ +import Flow from '../../models/flow.js'; +import logger from '../../helpers/logger.js'; +import handlerSync from '../../helpers/webhook-handler-sync.js'; + +export default async (request, response) => { + const computedRequestPayload = { + headers: request.headers, + body: request.body, + query: request.query, + params: request.params, + }; + + logger.debug(`Handling incoming webhook request at ${request.originalUrl}.`); + logger.debug(JSON.stringify(computedRequestPayload, null, 2)); + + const flowId = request.params.flowId; + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const triggerStep = await flow.getTriggerStep(); + + if (triggerStep.appKey !== 'webhook') { + const connection = await triggerStep.$relatedQuery('connection'); + + if (!(await connection.verifyWebhook(request))) { + return response.sendStatus(401); + } + } + + await handlerSync(flowId, request, response); +}; diff --git a/packages/backend/src/db/migrations/20211005151457_create_users.js b/packages/backend/src/db/migrations/20211005151457_create_users.js new file mode 100644 index 0000000000000000000000000000000000000000..b1699c9dc654c8bdf1c5882e4f3e7d1b8a94f619 --- /dev/null +++ b/packages/backend/src/db/migrations/20211005151457_create_users.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.createTable('users', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('email').unique().notNullable(); + table.string('password').notNullable(); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('users'); +} diff --git a/packages/backend/src/db/migrations/20211011120732_create_credentials.js b/packages/backend/src/db/migrations/20211011120732_create_credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..842f5ea70c4854f3971d5735642de463495a7abe --- /dev/null +++ b/packages/backend/src/db/migrations/20211011120732_create_credentials.js @@ -0,0 +1,16 @@ +export async function up(knex) { + return knex.schema.createTable('credentials', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('key').notNullable(); + table.string('display_name').notNullable(); + table.text('data').notNullable(); + table.uuid('user_id').references('id').inTable('users'); + table.boolean('verified').defaultTo(false); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('credentials'); +} diff --git a/packages/backend/src/db/migrations/20211014144855_remove_display_name_from_credentials.js b/packages/backend/src/db/migrations/20211014144855_remove_display_name_from_credentials.js new file mode 100644 index 0000000000000000000000000000000000000000..5e746dee61f1749099770f637b490d88a3232e92 --- /dev/null +++ b/packages/backend/src/db/migrations/20211014144855_remove_display_name_from_credentials.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('credentials', (table) => { + table.dropColumn('display_name'); + }); +} + +export async function down(knex) { + return knex.schema.table('credentials', (table) => { + table.string('display_name'); + }); +} diff --git a/packages/backend/src/db/migrations/20211017104154_rename_credentials_as_connections.js b/packages/backend/src/db/migrations/20211017104154_rename_credentials_as_connections.js new file mode 100644 index 0000000000000000000000000000000000000000..78f0740758f3022aef508240979f5c8de85a5aba --- /dev/null +++ b/packages/backend/src/db/migrations/20211017104154_rename_credentials_as_connections.js @@ -0,0 +1,7 @@ +export async function up(knex) { + return knex.schema.renameTable('credentials', 'connections'); +} + +export async function down(knex) { + return knex.schema.renameTable('connections', 'credentials'); +} diff --git a/packages/backend/src/db/migrations/20211106214730_create_steps.js b/packages/backend/src/db/migrations/20211106214730_create_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..c84a1d3ca482d605e7d2d2dfbd4607a94db55519 --- /dev/null +++ b/packages/backend/src/db/migrations/20211106214730_create_steps.js @@ -0,0 +1,16 @@ +export async function up(knex) { + return knex.schema.createTable('steps', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('key').notNullable(); + table.string('app_key').notNullable(); + table.string('type').notNullable(); + table.uuid('connection_id').references('id').inTable('connections'); + table.text('parameters'); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('steps'); +} diff --git a/packages/backend/src/db/migrations/20211122140336_create_flows.js b/packages/backend/src/db/migrations/20211122140336_create_flows.js new file mode 100644 index 0000000000000000000000000000000000000000..00e81c9c9be027ac439decde2322842270c3b0e1 --- /dev/null +++ b/packages/backend/src/db/migrations/20211122140336_create_flows.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.createTable('flows', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name'); + table.uuid('user_id').references('id').inTable('users'); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('flows'); +} diff --git a/packages/backend/src/db/migrations/20211122140612_add_flow_id_to_steps.js b/packages/backend/src/db/migrations/20211122140612_add_flow_id_to_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..e1326ccffd0e79dd3ff6517459f5e76a9ee29dd5 --- /dev/null +++ b/packages/backend/src/db/migrations/20211122140612_add_flow_id_to_steps.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('steps', (table) => { + table.uuid('flow_id').references('id').inTable('flows'); + }); +} + +export async function down(knex) { + return knex.schema.table('steps', (table) => { + table.dropColumn('flow_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20220105151725_remove_constraints_from_steps.js b/packages/backend/src/db/migrations/20220105151725_remove_constraints_from_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..f4812f2271dc95c0696dc62ba53295a2337eeab7 --- /dev/null +++ b/packages/backend/src/db/migrations/20220105151725_remove_constraints_from_steps.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.alterTable('steps', (table) => { + table.string('key').nullable().alter(); + table.string('app_key').nullable().alter(); + }); +} + +export async function down(knex) { + return knex.schema.alterTable('steps', (table) => { + table.string('key').notNullable().alter(); + table.string('app_key').notNullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20220108141045_add_active_column_to_flows.js b/packages/backend/src/db/migrations/20220108141045_add_active_column_to_flows.js new file mode 100644 index 0000000000000000000000000000000000000000..5da6273e8235145b2c67c09e2f28a3e27d5761db --- /dev/null +++ b/packages/backend/src/db/migrations/20220108141045_add_active_column_to_flows.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('flows', (table) => { + table.boolean('active').defaultTo(false); + }); +} + +export async function down(knex) { + return knex.schema.table('flows', (table) => { + table.dropColumn('active'); + }); +} diff --git a/packages/backend/src/db/migrations/20220127141941_add_position_to_steps.js b/packages/backend/src/db/migrations/20220127141941_add_position_to_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..bf1f567fbe48d2deba04a857aa7fdb655bf941b4 --- /dev/null +++ b/packages/backend/src/db/migrations/20220127141941_add_position_to_steps.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('steps', (table) => { + table.integer('position').notNullable(); + }); +} + +export async function down(knex) { + return knex.schema.table('steps', (table) => { + table.dropColumn('position'); + }); +} diff --git a/packages/backend/src/db/migrations/20220205145128_add_status_to_steps.js b/packages/backend/src/db/migrations/20220205145128_add_status_to_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..b7629f7609e4735c61af449b5ce0aecb5f60f284 --- /dev/null +++ b/packages/backend/src/db/migrations/20220205145128_add_status_to_steps.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('steps', (table) => { + table.string('status').notNullable().defaultTo('incomplete'); + }); +} + +export async function down(knex) { + return knex.schema.table('steps', (table) => { + table.dropColumn('status'); + }); +} diff --git a/packages/backend/src/db/migrations/20220219093113_create_executions.js b/packages/backend/src/db/migrations/20220219093113_create_executions.js new file mode 100644 index 0000000000000000000000000000000000000000..1ce2e5c0d9032a12d83932beefa777e0bacc9efe --- /dev/null +++ b/packages/backend/src/db/migrations/20220219093113_create_executions.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.createTable('executions', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('flow_id').references('id').inTable('flows'); + table.boolean('test_run').notNullable().defaultTo(false); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('executions'); +} diff --git a/packages/backend/src/db/migrations/20220219100800_create_execution_steps.js b/packages/backend/src/db/migrations/20220219100800_create_execution_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..9fe452060f93cd315582413797a011cabce51ee0 --- /dev/null +++ b/packages/backend/src/db/migrations/20220219100800_create_execution_steps.js @@ -0,0 +1,16 @@ +export async function up(knex) { + return knex.schema.createTable('execution_steps', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('execution_id').references('id').inTable('executions'); + table.uuid('step_id').references('id').inTable('steps'); + table.string('status'); + table.text('data_in'); + table.text('data_out'); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('execution_steps'); +} diff --git a/packages/backend/src/db/migrations/20220221225128_alter_columns_of_execution_steps.js b/packages/backend/src/db/migrations/20220221225128_alter_columns_of_execution_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..04bc60dfde1e9a0e5bdd6de0dcc7795648929544 --- /dev/null +++ b/packages/backend/src/db/migrations/20220221225128_alter_columns_of_execution_steps.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.alterTable('execution_steps', (table) => { + table.jsonb('data_in').alter(); + table.jsonb('data_out').alter(); + }); +} + +export async function down(knex) { + return knex.schema.alterTable('execution_steps', (table) => { + table.text('data_in').alter(); + table.text('data_out').alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20220221225537_alter_parameters_column_of_steps.js b/packages/backend/src/db/migrations/20220221225537_alter_parameters_column_of_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..6bfd231b2db6ced20dc2f13010d3b715100210bb --- /dev/null +++ b/packages/backend/src/db/migrations/20220221225537_alter_parameters_column_of_steps.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.alterTable('steps', (table) => { + table.jsonb('parameters').defaultTo('{}').alter(); + }); +} + +export async function down(knex) { + return knex.schema.alterTable('steps', (table) => { + table.text('parameters').alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20220727104324_add_draft_column_to_connections.js b/packages/backend/src/db/migrations/20220727104324_add_draft_column_to_connections.js new file mode 100644 index 0000000000000000000000000000000000000000..12f8e8d5b08bf063546f921b833ceea02dd85a77 --- /dev/null +++ b/packages/backend/src/db/migrations/20220727104324_add_draft_column_to_connections.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('connections', (table) => { + table.boolean('draft').defaultTo(true); + }); +} + +export async function down(knex) { + return knex.schema.table('connections', (table) => { + table.dropColumn('draft'); + }); +} diff --git a/packages/backend/src/db/migrations/20220817121241_add_published_at_column_to_flows.js b/packages/backend/src/db/migrations/20220817121241_add_published_at_column_to_flows.js new file mode 100644 index 0000000000000000000000000000000000000000..fd436f358fff75f6f2d47096ef6fcb674748ad12 --- /dev/null +++ b/packages/backend/src/db/migrations/20220817121241_add_published_at_column_to_flows.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('flows', (table) => { + table.timestamp('published_at').nullable(); + }); +} + +export async function down(knex) { + return knex.schema.table('flows', (table) => { + table.dropColumn('published_at'); + }); +} diff --git a/packages/backend/src/db/migrations/20220823171017_add_internal_id_to_executions.js b/packages/backend/src/db/migrations/20220823171017_add_internal_id_to_executions.js new file mode 100644 index 0000000000000000000000000000000000000000..a833571ca54990fb722adab7b28e2b55748172c6 --- /dev/null +++ b/packages/backend/src/db/migrations/20220823171017_add_internal_id_to_executions.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('executions', (table) => { + table.string('internal_id'); + }); +} + +export async function down(knex) { + return knex.schema.table('executions', (table) => { + table.dropColumn('internal_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20220904160521_add_raw_error_to_execution_steps.js b/packages/backend/src/db/migrations/20220904160521_add_raw_error_to_execution_steps.js new file mode 100644 index 0000000000000000000000000000000000000000..ccef7ec783f096d31c68331d956be59b0f1ed2ac --- /dev/null +++ b/packages/backend/src/db/migrations/20220904160521_add_raw_error_to_execution_steps.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('execution_steps', (table) => { + table.jsonb('error_details'); + }); +} + +export async function down(knex) { + return knex.schema.table('execution_steps', (table) => { + table.dropColumn('error_details'); + }); +} diff --git a/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.js b/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.js new file mode 100644 index 0000000000000000000000000000000000000000..b584d6a20e2e41e4eb5579e6112ac4915f1d4452 --- /dev/null +++ b/packages/backend/src/db/migrations/20220928162525_soft-delete-base-model.js @@ -0,0 +1,29 @@ +async function addDeletedColumn(knex, tableName) { + return await knex.schema.table(tableName, (table) => { + table.timestamp('deleted_at').nullable(); + }); +} + +async function dropDeletedColumn(knex, tableName) { + return await knex.schema.table(tableName, (table) => { + table.dropColumn('deleted_at'); + }); +} + +export async function up(knex) { + await addDeletedColumn(knex, 'steps'); + await addDeletedColumn(knex, 'flows'); + await addDeletedColumn(knex, 'executions'); + await addDeletedColumn(knex, 'execution_steps'); + await addDeletedColumn(knex, 'users'); + await addDeletedColumn(knex, 'connections'); +} + +export async function down(knex) { + await dropDeletedColumn(knex, 'steps'); + await dropDeletedColumn(knex, 'flows'); + await dropDeletedColumn(knex, 'executions'); + await dropDeletedColumn(knex, 'execution_steps'); + await dropDeletedColumn(knex, 'users'); + await dropDeletedColumn(knex, 'connections'); +} diff --git a/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.js b/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.js new file mode 100644 index 0000000000000000000000000000000000000000..3a39da2d8c7a846a3e4064e5f807e52ac5d78b9f --- /dev/null +++ b/packages/backend/src/db/migrations/20221214184855_add_remote_webhook_id_in_flow.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('flows', (table) => { + table.string('remote_webhook_id'); + }); +} + +export async function down(knex) { + return knex.schema.table('flows', (table) => { + table.dropColumn('remote_webhook_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20230218110748_add_role_to_users.js b/packages/backend/src/db/migrations/20230218110748_add_role_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..c4b1af63af9140242dd0e2bf330f2f9b7b393da0 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218110748_add_role_to_users.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.table('users', async (table) => { + table.string('role'); + + await knex('users').update({ role: 'admin' }); + }); +} + +export async function down(knex) { + return knex.schema.table('users', (table) => { + table.dropColumn('role'); + }); +} diff --git a/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.js b/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.js new file mode 100644 index 0000000000000000000000000000000000000000..90e4e30a3e3e4b1933cb23ad71ab474f13f5f669 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218131824_alter_role_to_not_nullable_for_users.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.alterTable('users', (table) => { + table.string('role').notNullable().alter(); + }); +} + +export async function down(knex) { + return knex.schema.alterTable('users', (table) => { + table.string('role').nullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.js b/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..ddd5bbc3fc67e25e5498433ef3d4ca2f9a882f45 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218150517_add_reset_password_token_to_users.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('users', (table) => { + table.string('reset_password_token'); + }); +} + +export async function down(knex) { + return knex.schema.table('users', (table) => { + table.dropColumn('reset_password_token'); + }); +} diff --git a/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.js b/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..7373517caf52527ec37595ded39fe869f8f6efd4 --- /dev/null +++ b/packages/backend/src/db/migrations/20230218150758_add_reset_password_token_sent_at_to_users.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('users', (table) => { + table.timestamp('reset_password_token_sent_at'); + }); +} + +export async function down(knex) { + return knex.schema.table('users', (table) => { + table.dropColumn('reset_password_token_sent_at'); + }); +} diff --git a/packages/backend/src/db/migrations/20230301211751_add_full_name_to_users.js b/packages/backend/src/db/migrations/20230301211751_add_full_name_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..277e68174b85f2a72d2e6f39e306d1542de24d10 --- /dev/null +++ b/packages/backend/src/db/migrations/20230301211751_add_full_name_to_users.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.table('users', async (table) => { + table.string('full_name'); + + await knex('users').update({ full_name: 'Initial admin' }); + }); +} + +export async function down(knex) { + return knex.schema.table('users', (table) => { + table.dropColumn('full_name'); + }); +} diff --git a/packages/backend/src/db/migrations/20230303134548_create_payment_plans.js b/packages/backend/src/db/migrations/20230303134548_create_payment_plans.js new file mode 100644 index 0000000000000000000000000000000000000000..4ebe181f400c31095908da2c7eb0b88e221b5a16 --- /dev/null +++ b/packages/backend/src/db/migrations/20230303134548_create_payment_plans.js @@ -0,0 +1,23 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.createTable('payment_plans', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name').notNullable(); + table.integer('task_count').notNullable(); + table.uuid('user_id').references('id').inTable('users'); + table.string('stripe_customer_id'); + table.string('stripe_subscription_id'); + table.timestamp('current_period_started_at').nullable(); + table.timestamp('current_period_ends_at').nullable(); + table.timestamp('deleted_at').nullable(); + table.timestamps(true, true); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + return knex.schema.dropTable('payment_plans'); +} diff --git a/packages/backend/src/db/migrations/20230303180902_create_usage_data.js b/packages/backend/src/db/migrations/20230303180902_create_usage_data.js new file mode 100644 index 0000000000000000000000000000000000000000..953423d3da1f18fc9b1e5969b80ef66d5ec83989 --- /dev/null +++ b/packages/backend/src/db/migrations/20230303180902_create_usage_data.js @@ -0,0 +1,19 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.createTable('usage_data', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('user_id').references('id').inTable('users'); + table.string('consumed_task_count').notNullable(); + table.timestamp('next_reset_at').nullable(); + table.timestamp('deleted_at').nullable(); + table.timestamps(true, true); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + return knex.schema.dropTable('usage_data'); +} diff --git a/packages/backend/src/db/migrations/20230306103149_alter_consumed_task_count_of_usage_data.js b/packages/backend/src/db/migrations/20230306103149_alter_consumed_task_count_of_usage_data.js new file mode 100644 index 0000000000000000000000000000000000000000..7a402b3d2b8988c8b9f09e5de152604b433301a1 --- /dev/null +++ b/packages/backend/src/db/migrations/20230306103149_alter_consumed_task_count_of_usage_data.js @@ -0,0 +1,17 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.alterTable('usage_data', (table) => { + table.integer('consumed_task_count').notNullable().alter(); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.alterTable('usage_data', (table) => { + table.string('consumed_task_count').notNullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.js b/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..0fb35e1da6135b338479bc34335d1da01a125eb6 --- /dev/null +++ b/packages/backend/src/db/migrations/20230318220822_add_trial_expiry_date_to_users.js @@ -0,0 +1,17 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('users', (table) => { + table.date('trial_expiry_date'); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('users', (table) => { + table.dropColumn('trial_expiry_date'); + }); +} diff --git a/packages/backend/src/db/migrations/20230323145809_create_subscriptions.js b/packages/backend/src/db/migrations/20230323145809_create_subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..1d8ce06f74e95a4343b46d87bd23737ffcb7580d --- /dev/null +++ b/packages/backend/src/db/migrations/20230323145809_create_subscriptions.js @@ -0,0 +1,26 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.createTable('subscriptions', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('user_id').references('id').inTable('users'); + table.string('paddle_subscription_id').unique().notNullable(); + table.string('paddle_plan_id').notNullable(); + table.string('update_url').notNullable(); + table.string('cancel_url').notNullable(); + table.string('status').notNullable(); + table.string('next_bill_amount').notNullable(); + table.date('next_bill_date').notNullable(); + table.date('last_bill_date'); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.dropTable('subscriptions'); +} diff --git a/packages/backend/src/db/migrations/20230324210051_add_deleted_at_to_subscriptions.js b/packages/backend/src/db/migrations/20230324210051_add_deleted_at_to_subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..28a7d263f9c009755a8e8b357972bb3132952d7b --- /dev/null +++ b/packages/backend/src/db/migrations/20230324210051_add_deleted_at_to_subscriptions.js @@ -0,0 +1,17 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.alterTable('subscriptions', (table) => { + table.timestamp('deleted_at').nullable(); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.alterTable('subscriptions', (table) => { + table.dropColumn('deleted_at'); + }); +} diff --git a/packages/backend/src/db/migrations/20230402183738_add_subscription_id_in_usage_data.js b/packages/backend/src/db/migrations/20230402183738_add_subscription_id_in_usage_data.js new file mode 100644 index 0000000000000000000000000000000000000000..d0ba0cb2c12c0f943e1667549c049962051919ea --- /dev/null +++ b/packages/backend/src/db/migrations/20230402183738_add_subscription_id_in_usage_data.js @@ -0,0 +1,17 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('usage_data', (table) => { + table.uuid('subscription_id').references('id').inTable('subscriptions'); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('usage_data', (table) => { + table.dropColumn('subscription_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.js b/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.js new file mode 100644 index 0000000000000000000000000000000000000000..f7457b8200aade3b8a978507a7fa3eef19751397 --- /dev/null +++ b/packages/backend/src/db/migrations/20230411203412_add_cancellation_effective_date_to_subscriptions.js @@ -0,0 +1,17 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('subscriptions', (table) => { + table.date('cancellation_effective_date'); + }); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.table('subscriptions', (table) => { + table.dropColumn('cancellation_effective_date'); + }); +} diff --git a/packages/backend/src/db/migrations/20230415134138_drop_payment_plans.js b/packages/backend/src/db/migrations/20230415134138_drop_payment_plans.js new file mode 100644 index 0000000000000000000000000000000000000000..51dbc3b0ab28abbd623e7447ed142800f7169e0e --- /dev/null +++ b/packages/backend/src/db/migrations/20230415134138_drop_payment_plans.js @@ -0,0 +1,24 @@ +import appConfig from '../../config/app.js'; + +export async function up(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.dropTable('payment_plans'); +} + +export async function down(knex) { + if (!appConfig.isCloud) return; + + return knex.schema.createTable('payment_plans', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name').notNullable(); + table.integer('task_count').notNullable(); + table.uuid('user_id').references('id').inTable('users'); + table.string('stripe_customer_id'); + table.string('stripe_subscription_id'); + table.timestamp('current_period_started_at').nullable(); + table.timestamp('current_period_ends_at').nullable(); + table.timestamp('deleted_at').nullable(); + table.timestamps(true, true); + }); +} diff --git a/packages/backend/src/db/migrations/20230609201228_add_webhook_path_in_step.js b/packages/backend/src/db/migrations/20230609201228_add_webhook_path_in_step.js new file mode 100644 index 0000000000000000000000000000000000000000..361e71192fa3841362dc957b503196d0e47f78d4 --- /dev/null +++ b/packages/backend/src/db/migrations/20230609201228_add_webhook_path_in_step.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('steps', (table) => { + table.string('webhook_path'); + }); +} + +export async function down(knex) { + return knex.schema.table('steps', (table) => { + table.dropColumn('webhook_path'); + }); +} diff --git a/packages/backend/src/db/migrations/20230609201909_populate_data_in_webhook_path_in_step.js b/packages/backend/src/db/migrations/20230609201909_populate_data_in_webhook_path_in_step.js new file mode 100644 index 0000000000000000000000000000000000000000..37a23c0119f08e0ea222ff84a814724b4cd632f7 --- /dev/null +++ b/packages/backend/src/db/migrations/20230609201909_populate_data_in_webhook_path_in_step.js @@ -0,0 +1,23 @@ +export async function up(knex) { + return await knex('steps') + .where('type', 'trigger') + .whereIn('app_key', [ + 'gitlab', + 'typeform', + 'twilio', + 'flowers-software', + 'webhook', + ]) + .update({ + webhook_path: knex.raw('? || ??', [ + '/webhooks/flows/', + knex.ref('flow_id'), + ]), + }); +} + +export async function down(knex) { + return await knex('steps').update({ + webhook_path: null, + }); +} diff --git a/packages/backend/src/db/migrations/20230615200200_create_roles.js b/packages/backend/src/db/migrations/20230615200200_create_roles.js new file mode 100644 index 0000000000000000000000000000000000000000..5593b6c9177b980f7e51d51a32d6ee5206ca090c --- /dev/null +++ b/packages/backend/src/db/migrations/20230615200200_create_roles.js @@ -0,0 +1,43 @@ +import capitalize from 'lodash/capitalize.js'; +import lowerCase from 'lodash/lowerCase.js'; + +export async function up(knex) { + await knex.schema.createTable('roles', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name').notNullable(); + table.string('key').notNullable(); + table.string('description'); + + table.timestamps(true, true); + }); + + const uniqueUserRoles = await knex('users').select('role').groupBy('role'); + + let shouldCreateAdminRole = true; + for (const { role } of uniqueUserRoles) { + // skip empty roles + if (!role) continue; + + const lowerCaseRole = lowerCase(role); + + if (lowerCaseRole === 'admin') { + shouldCreateAdminRole = false; + } + + await knex('roles').insert({ + name: capitalize(role), + key: lowerCaseRole, + }); + } + + if (shouldCreateAdminRole) { + await knex('roles').insert({ + name: 'Admin', + key: 'admin', + }); + } +} + +export async function down(knex) { + return knex.schema.dropTable('roles'); +} diff --git a/packages/backend/src/db/migrations/20230615205857_create_permissions.js b/packages/backend/src/db/migrations/20230615205857_create_permissions.js new file mode 100644 index 0000000000000000000000000000000000000000..598d16db2851cfbed54278ffb0e6d704ed91da85 --- /dev/null +++ b/packages/backend/src/db/migrations/20230615205857_create_permissions.js @@ -0,0 +1,66 @@ +const getPermissionForRole = (roleId, subject, actions, conditions = []) => + actions.map((action) => ({ + role_id: roleId, + subject, + action, + conditions, + })); + +export async function up(knex) { + await knex.schema.createTable('permissions', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('role_id').references('id').inTable('roles'); + table.string('action').notNullable(); + table.string('subject').notNullable(); + table.jsonb('conditions').notNullable().defaultTo([]); + + table.timestamps(true, true); + }); + + const roles = await knex('roles').select(['id', 'key']); + + for (const role of roles) { + // `admin` role should have no conditions unlike others by default + const isAdmin = role.key === 'admin'; + const roleConditions = isAdmin ? [] : ['isCreator']; + + // default permissions + await knex('permissions').insert([ + ...getPermissionForRole( + role.id, + 'Connection', + ['create', 'read', 'delete', 'update'], + roleConditions + ), + ...getPermissionForRole(role.id, 'Execution', ['read'], roleConditions), + ...getPermissionForRole( + role.id, + 'Flow', + ['create', 'delete', 'publish', 'read', 'update'], + roleConditions + ), + ]); + + // admin specific permission + if (isAdmin) { + await knex('permissions').insert([ + ...getPermissionForRole(role.id, 'User', [ + 'create', + 'read', + 'delete', + 'update', + ]), + ...getPermissionForRole(role.id, 'Role', [ + 'create', + 'read', + 'delete', + 'update', + ]), + ]); + } + } +} + +export async function down(knex) { + return knex.schema.dropTable('permissions'); +} diff --git a/packages/backend/src/db/migrations/20230615215004_add_role_id_to_users.js b/packages/backend/src/db/migrations/20230615215004_add_role_id_to_users.js new file mode 100644 index 0000000000000000000000000000000000000000..f3fba1d43457da7138ad0754f74807163c254aad --- /dev/null +++ b/packages/backend/src/db/migrations/20230615215004_add_role_id_to_users.js @@ -0,0 +1,27 @@ +export async function up(knex) { + await knex.schema.table('users', async (table) => { + table.uuid('role_id').references('id').inTable('roles'); + }); + + const theRole = await knex('roles').select('id').limit(1).first(); + const roles = await knex('roles').select('id', 'key'); + + for (const role of roles) { + await knex('users') + .where({ + role: role.key, + }) + .update({ + role_id: role.id, + }); + } + + // backfill not-migratables + await knex('users').whereNull('role_id').update({ role_id: theRole.id }); +} + +export async function down(knex) { + return await knex.schema.table('users', (table) => { + table.dropColumn('role_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20230623115503_remove_role_column_in_users.js b/packages/backend/src/db/migrations/20230623115503_remove_role_column_in_users.js new file mode 100644 index 0000000000000000000000000000000000000000..56003257f302285ec95f17570ca73885daf001e4 --- /dev/null +++ b/packages/backend/src/db/migrations/20230623115503_remove_role_column_in_users.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('users', async (table) => { + table.dropColumn('role'); + }); +} + +export async function down(knex) { + return await knex.schema.table('users', (table) => { + table.string('role').defaultTo('user'); + }); +} diff --git a/packages/backend/src/db/migrations/20230702210636_create_saml_auth_providers.js b/packages/backend/src/db/migrations/20230702210636_create_saml_auth_providers.js new file mode 100644 index 0000000000000000000000000000000000000000..41cb01df6b06f5d203a914242f4608acaf1cb545 --- /dev/null +++ b/packages/backend/src/db/migrations/20230702210636_create_saml_auth_providers.js @@ -0,0 +1,22 @@ +export async function up(knex) { + return knex.schema.createTable('saml_auth_providers', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name').notNullable(); + table.text('certificate').notNullable(); + table.string('signature_algorithm').notNullable(); + table.string('issuer').notNullable(); + table.text('entry_point').notNullable(); + table.text('firstname_attribute_name').notNullable(); + table.text('surname_attribute_name').notNullable(); + table.text('email_attribute_name').notNullable(); + table.text('role_attribute_name').notNullable(); + table.uuid('default_role_id').references('id').inTable('roles'); + table.boolean('active').defaultTo(false); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('saml_auth_providers'); +} diff --git a/packages/backend/src/db/migrations/20230707094923_create_identities.js b/packages/backend/src/db/migrations/20230707094923_create_identities.js new file mode 100644 index 0000000000000000000000000000000000000000..99be1f39dd328b0bbe1a5820d24db92b9585dd5a --- /dev/null +++ b/packages/backend/src/db/migrations/20230707094923_create_identities.js @@ -0,0 +1,15 @@ +export async function up(knex) { + return knex.schema.createTable('identities', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.uuid('user_id').references('id').inTable('users'); + table.string('remote_id').notNullable(); + table.string('provider_id').notNullable(); + table.string('provider_type').notNullable(); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('identities'); +} diff --git a/packages/backend/src/db/migrations/20230715214424_make_user_password_nullable.js b/packages/backend/src/db/migrations/20230715214424_make_user_password_nullable.js new file mode 100644 index 0000000000000000000000000000000000000000..7779d8e7d24d3fddaebb813d7ed1ba39ae4f7c48 --- /dev/null +++ b/packages/backend/src/db/migrations/20230715214424_make_user_password_nullable.js @@ -0,0 +1,9 @@ +export async function up(knex) { + return await knex.schema.alterTable('users', (table) => { + table.string('password').nullable().alter(); + }); +} + +export async function down() { + // void +} diff --git a/packages/backend/src/db/migrations/20230807114158_seed_saml_permissions_to_admin.js b/packages/backend/src/db/migrations/20230807114158_seed_saml_permissions_to_admin.js new file mode 100644 index 0000000000000000000000000000000000000000..16ff8681e3a1cae1be91330d68f737bd1641f4df --- /dev/null +++ b/packages/backend/src/db/migrations/20230807114158_seed_saml_permissions_to_admin.js @@ -0,0 +1,27 @@ +const getPermissionForRole = (roleId, subject, actions) => + actions.map((action) => ({ + role_id: roleId, + subject, + action, + conditions: [], + })); + +export async function up(knex) { + const role = await knex('roles') + .first(['id', 'key']) + .where({ key: 'admin' }) + .limit(1); + + await knex('permissions').insert( + getPermissionForRole(role.id, 'SamlAuthProvider', [ + 'create', + 'read', + 'delete', + 'update', + ]) + ); +} + +export async function down(knex) { + await knex('permissions').where({ subject: 'SamlAuthProvider' }).delete(); +} diff --git a/packages/backend/src/db/migrations/20230810124730_create_config.js b/packages/backend/src/db/migrations/20230810124730_create_config.js new file mode 100644 index 0000000000000000000000000000000000000000..9b0ef1708931c569eb2acb5fdf8b1fdd517cce2e --- /dev/null +++ b/packages/backend/src/db/migrations/20230810124730_create_config.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.createTable('config', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('key').unique().notNullable(); + table.jsonb('value').notNullable().defaultTo({}); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('config'); +} diff --git a/packages/backend/src/db/migrations/20230810134714_seed_update_config_permissions_to_admin.js b/packages/backend/src/db/migrations/20230810134714_seed_update_config_permissions_to_admin.js new file mode 100644 index 0000000000000000000000000000000000000000..63c7da6db785b1b5b0f9e96edbcf74956a456425 --- /dev/null +++ b/packages/backend/src/db/migrations/20230810134714_seed_update_config_permissions_to_admin.js @@ -0,0 +1,22 @@ +const getPermissionForRole = (roleId, subject, actions) => + actions.map((action) => ({ + role_id: roleId, + subject, + action, + conditions: [], + })); + +export async function up(knex) { + const role = await knex('roles') + .first(['id', 'key']) + .where({ key: 'admin' }) + .limit(1); + + await knex('permissions').insert( + getPermissionForRole(role.id, 'Config', ['update']) + ); +} + +export async function down(knex) { + await knex('permissions').where({ subject: 'Config' }).delete(); +} diff --git a/packages/backend/src/db/migrations/20230811142340_create_saml_auth_providers_role_mappings.js b/packages/backend/src/db/migrations/20230811142340_create_saml_auth_providers_role_mappings.js new file mode 100644 index 0000000000000000000000000000000000000000..0f31079756763f0ec2b94dc86312985ffbaf5932 --- /dev/null +++ b/packages/backend/src/db/migrations/20230811142340_create_saml_auth_providers_role_mappings.js @@ -0,0 +1,22 @@ +export async function up(knex) { + return knex.schema.createTable( + 'saml_auth_providers_role_mappings', + (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table + .uuid('saml_auth_provider_id') + .references('id') + .inTable('saml_auth_providers'); + table.uuid('role_id').references('id').inTable('roles'); + table.string('remote_role_name').notNullable(); + + table.unique(['saml_auth_provider_id', 'remote_role_name']); + + table.timestamps(true, true); + } + ); +} + +export async function down(knex) { + return knex.schema.dropTable('saml_auth_providers_role_mappings'); +} diff --git a/packages/backend/src/db/migrations/20230812132005_create_app_configs.js b/packages/backend/src/db/migrations/20230812132005_create_app_configs.js new file mode 100644 index 0000000000000000000000000000000000000000..028ebbe593a345424992153136071889e5e98938 --- /dev/null +++ b/packages/backend/src/db/migrations/20230812132005_create_app_configs.js @@ -0,0 +1,15 @@ +export async function up(knex) { + return knex.schema.createTable('app_configs', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('key').unique().notNullable(); + table.boolean('allow_custom_connection').notNullable().defaultTo(false); + table.boolean('shared').notNullable().defaultTo(false); + table.boolean('disabled').notNullable().defaultTo(false); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('app_configs'); +} diff --git a/packages/backend/src/db/migrations/20230813172729_create_app_auth_clients.js b/packages/backend/src/db/migrations/20230813172729_create_app_auth_clients.js new file mode 100644 index 0000000000000000000000000000000000000000..4d690e802098c2715303f2ef902d05495ddaf20a --- /dev/null +++ b/packages/backend/src/db/migrations/20230813172729_create_app_auth_clients.js @@ -0,0 +1,19 @@ +export async function up(knex) { + return knex.schema.createTable('app_auth_clients', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('name').unique().notNullable(); + table + .uuid('app_config_id') + .notNullable() + .references('id') + .inTable('app_configs'); + table.text('auth_defaults').notNullable(); + table.boolean('active').notNullable().defaultTo(false); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('app_auth_clients'); +} diff --git a/packages/backend/src/db/migrations/20230815161102_add_app_auth_client_id_in_connections.js b/packages/backend/src/db/migrations/20230815161102_add_app_auth_client_id_in_connections.js new file mode 100644 index 0000000000000000000000000000000000000000..c5210926c143efecea16471d67b4b739e0feae27 --- /dev/null +++ b/packages/backend/src/db/migrations/20230815161102_add_app_auth_client_id_in_connections.js @@ -0,0 +1,14 @@ +export async function up(knex) { + await knex.schema.table('connections', async (table) => { + table + .uuid('app_auth_client_id') + .references('id') + .inTable('app_auth_clients'); + }); +} + +export async function down(knex) { + return await knex.schema.table('connections', (table) => { + table.dropColumn('app_auth_client_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20230816121044_seed_update_app_permissions_to_admin.js b/packages/backend/src/db/migrations/20230816121044_seed_update_app_permissions_to_admin.js new file mode 100644 index 0000000000000000000000000000000000000000..ae99045048cc2f5a5a78ec99314521e2ba3cdb1f --- /dev/null +++ b/packages/backend/src/db/migrations/20230816121044_seed_update_app_permissions_to_admin.js @@ -0,0 +1,22 @@ +const getPermissionForRole = (roleId, subject, actions) => + actions.map((action) => ({ + role_id: roleId, + subject, + action, + conditions: [], + })); + +export async function up(knex) { + const role = await knex('roles') + .first(['id', 'key']) + .where({ key: 'admin' }) + .limit(1); + + await knex('permissions').insert( + getPermissionForRole(role.id, 'App', ['create', 'read', 'delete', 'update']) + ); +} + +export async function down(knex) { + await knex('permissions').where({ subject: 'App' }).delete(); +} diff --git a/packages/backend/src/db/migrations/20230816173027_make_role_id_not_nullable_in_users.js b/packages/backend/src/db/migrations/20230816173027_make_role_id_not_nullable_in_users.js new file mode 100644 index 0000000000000000000000000000000000000000..e39f3878e6f87df8f6e627eeb76af11250e27c3a --- /dev/null +++ b/packages/backend/src/db/migrations/20230816173027_make_role_id_not_nullable_in_users.js @@ -0,0 +1,23 @@ +export async function up(knex) { + const role = await knex('roles') + .select('id') + .whereIn('key', ['user', 'admin']) + .orderBy('key', 'desc') + .limit(1) + .first(); + + if (role) { + // backfill nulls + await knex('users').whereNull('role_id').update({ role_id: role.id }); + } + + return await knex.schema.alterTable('users', (table) => { + table.uuid('role_id').notNullable().alter(); + }); +} + +export async function down(knex) { + return await knex.schema.alterTable('users', (table) => { + table.uuid('role_id').nullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20230824105813_soft_delete_soft_deleted_user_associations.js b/packages/backend/src/db/migrations/20230824105813_soft_delete_soft_deleted_user_associations.js new file mode 100644 index 0000000000000000000000000000000000000000..fdd1b6cec12d921fec30b9422753f450a9c62da1 --- /dev/null +++ b/packages/backend/src/db/migrations/20230824105813_soft_delete_soft_deleted_user_associations.js @@ -0,0 +1,33 @@ +export async function up(knex) { + const users = await knex('users').whereNotNull('deleted_at'); + const userIds = users.map((user) => user.id); + + const flows = await knex('flows').whereIn('user_id', userIds); + const flowIds = flows.map((flow) => flow.id); + const executions = await knex('executions').whereIn('flow_id', flowIds); + const executionIds = executions.map((execution) => execution.id); + + await knex('execution_steps').whereIn('execution_id', executionIds).update({ + deleted_at: knex.fn.now(), + }); + + await knex('executions').whereIn('id', executionIds).update({ + deleted_at: knex.fn.now(), + }); + + await knex('steps').whereIn('flow_id', flowIds).update({ + deleted_at: knex.fn.now(), + }); + + await knex('flows').whereIn('id', flowIds).update({ + deleted_at: knex.fn.now(), + }); + + await knex('connections').whereIn('user_id', userIds).update({ + deleted_at: knex.fn.now(), + }); +} + +export async function down() { + // void +} diff --git a/packages/backend/src/db/migrations/20230828134734_convert_permission_conditions_to_array.js b/packages/backend/src/db/migrations/20230828134734_convert_permission_conditions_to_array.js new file mode 100644 index 0000000000000000000000000000000000000000..c6056cf15c1acff2a2d3c990cc39f62d5460b52a --- /dev/null +++ b/packages/backend/src/db/migrations/20230828134734_convert_permission_conditions_to_array.js @@ -0,0 +1,9 @@ +export async function up(knex) { + await knex('permissions') + .where(knex.raw('conditions::text'), '=', knex.raw("'{}'::text")) + .update('conditions', JSON.stringify([])); +} + +export async function down() { + // void +} diff --git a/packages/backend/src/db/migrations/20231013094544_convert_user_emails_to_lowercase.js b/packages/backend/src/db/migrations/20231013094544_convert_user_emails_to_lowercase.js new file mode 100644 index 0000000000000000000000000000000000000000..e179ff455effedb122f5f0204c3b6b9659b3891d --- /dev/null +++ b/packages/backend/src/db/migrations/20231013094544_convert_user_emails_to_lowercase.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex('users') + .whereRaw('email != LOWER(email)') + .update({ + email: knex.raw('LOWER(email)'), + }); +} + +export async function down() { + // void +} diff --git a/packages/backend/src/db/migrations/20231025101146_add_flow_id_index_in_executions.js b/packages/backend/src/db/migrations/20231025101146_add_flow_id_index_in_executions.js new file mode 100644 index 0000000000000000000000000000000000000000..be9c1135da0642099254125b7a789767556bfda4 --- /dev/null +++ b/packages/backend/src/db/migrations/20231025101146_add_flow_id_index_in_executions.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('executions', (table) => { + table.index('flow_id'); + }); +} + +export async function down(knex) { + await knex.schema.table('executions', (table) => { + table.dropIndex('flow_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20231025101923_add_updated_at_index_in_executions.js b/packages/backend/src/db/migrations/20231025101923_add_updated_at_index_in_executions.js new file mode 100644 index 0000000000000000000000000000000000000000..43d95dd5488c6107779b16ad3869bdaea631d3b9 --- /dev/null +++ b/packages/backend/src/db/migrations/20231025101923_add_updated_at_index_in_executions.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('executions', (table) => { + table.index('updated_at'); + }); +} + +export async function down(knex) { + await knex.schema.table('executions', (table) => { + table.dropIndex('updated_at'); + }); +} diff --git a/packages/backend/src/db/migrations/20240227164849_create_datastore_model.js b/packages/backend/src/db/migrations/20240227164849_create_datastore_model.js new file mode 100644 index 0000000000000000000000000000000000000000..c303989e87cbfcfa3bf6c2b44ebd66fab4502072 --- /dev/null +++ b/packages/backend/src/db/migrations/20240227164849_create_datastore_model.js @@ -0,0 +1,16 @@ +export async function up(knex) { + return knex.schema.createTable('datastore', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('key').notNullable(); + table.string('value'); + table.string('scope').notNullable(); + table.uuid('scope_id').notNullable(); + table.index(['key', 'scope', 'scope_id']); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('datastore'); +} diff --git a/packages/backend/src/db/migrations/20240326194638_add_app_key_to_app_auth_clients.js b/packages/backend/src/db/migrations/20240326194638_add_app_key_to_app_auth_clients.js new file mode 100644 index 0000000000000000000000000000000000000000..4ab8210687eebce878153884258c1f1d493d74ce --- /dev/null +++ b/packages/backend/src/db/migrations/20240326194638_add_app_key_to_app_auth_clients.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.string('app_key'); + }); +} + +export async function down(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.dropColumn('app_key'); + }); +} diff --git a/packages/backend/src/db/migrations/20240326195028_migrate_app_config_id_to_app_key.js b/packages/backend/src/db/migrations/20240326195028_migrate_app_config_id_to_app_key.js new file mode 100644 index 0000000000000000000000000000000000000000..05842ad7ef64bcedee3a46e0213afbb7dfbe27e5 --- /dev/null +++ b/packages/backend/src/db/migrations/20240326195028_migrate_app_config_id_to_app_key.js @@ -0,0 +1,17 @@ +export async function up(knex) { + const appAuthClients = await knex('app_auth_clients').select('*'); + + for (const appAuthClient of appAuthClients) { + const appConfig = await knex('app_configs') + .where('id', appAuthClient.app_config_id) + .first(); + + await knex('app_auth_clients') + .where('id', appAuthClient.id) + .update({ app_key: appConfig.key }); + } +} + +export async function down() { + // void +} diff --git a/packages/backend/src/db/migrations/20240326195748_remove_app_config_id_from_app_auth_clients.js b/packages/backend/src/db/migrations/20240326195748_remove_app_config_id_from_app_auth_clients.js new file mode 100644 index 0000000000000000000000000000000000000000..12b7c7d536c918d902ba4218637ad41343f6dcff --- /dev/null +++ b/packages/backend/src/db/migrations/20240326195748_remove_app_config_id_from_app_auth_clients.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.dropColumn('app_config_id'); + }); +} + +export async function down(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.uuid('app_config_id').references('id').inTable('app_configs'); + }); +} diff --git a/packages/backend/src/db/migrations/20240326202110_add_not_nullable_to_app_key_of_app_auth_clients.js b/packages/backend/src/db/migrations/20240326202110_add_not_nullable_to_app_key_of_app_auth_clients.js new file mode 100644 index 0000000000000000000000000000000000000000..9c6145b457227d272078f9438dd55cc116b48e99 --- /dev/null +++ b/packages/backend/src/db/migrations/20240326202110_add_not_nullable_to_app_key_of_app_auth_clients.js @@ -0,0 +1,11 @@ +export async function up(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.string('app_key').notNullable().alter(); + }); +} + +export async function down(knex) { + await knex.schema.table('app_auth_clients', (table) => { + table.string('app_key').nullable().alter(); + }); +} diff --git a/packages/backend/src/db/migrations/20240422130323_create_access_tokens.js b/packages/backend/src/db/migrations/20240422130323_create_access_tokens.js new file mode 100644 index 0000000000000000000000000000000000000000..d71af5b813309dfadbadb6768e0acfcc8dbec41a --- /dev/null +++ b/packages/backend/src/db/migrations/20240422130323_create_access_tokens.js @@ -0,0 +1,15 @@ +export async function up(knex) { + return knex.schema.createTable('access_tokens', (table) => { + table.uuid('id').primary().defaultTo(knex.raw('gen_random_uuid()')); + table.string('token').notNullable(); + table.integer('expires_in').notNullable(); + table.timestamp('revoked_at').nullable(); + table.uuid('user_id').references('id').inTable('users'); + + table.timestamps(true, true); + }); +} + +export async function down(knex) { + return knex.schema.dropTable('access_tokens'); +} diff --git a/packages/backend/src/db/migrations/20240424100113_add_indexes_to_access_tokens.js b/packages/backend/src/db/migrations/20240424100113_add_indexes_to_access_tokens.js new file mode 100644 index 0000000000000000000000000000000000000000..0cb5d96f8d29a3810d97146178f0ab4de2b1ba52 --- /dev/null +++ b/packages/backend/src/db/migrations/20240424100113_add_indexes_to_access_tokens.js @@ -0,0 +1,13 @@ +export async function up(knex) { + return knex.schema.table('access_tokens', (table) => { + table.index('token'); + table.index('user_id'); + }); +} + +export async function down(knex) { + return knex.schema.table('access_tokens', (table) => { + table.dropIndex('token'); + table.dropIndex('user_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20240430132947_add_saml_session_id_in_access_tokens.js b/packages/backend/src/db/migrations/20240430132947_add_saml_session_id_in_access_tokens.js new file mode 100644 index 0000000000000000000000000000000000000000..42423bd55034b5860b331305058fcdd80e7f2ac3 --- /dev/null +++ b/packages/backend/src/db/migrations/20240430132947_add_saml_session_id_in_access_tokens.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.table('access_tokens', (table) => { + table.string('saml_session_id').nullable(); + }); +} + +export async function down(knex) { + return knex.schema.table('access_tokens', (table) => { + table.dropColumn('saml_session_id'); + }); +} diff --git a/packages/backend/src/db/migrations/20240507135944_update_installation_completed_for_config.js b/packages/backend/src/db/migrations/20240507135944_update_installation_completed_for_config.js new file mode 100644 index 0000000000000000000000000000000000000000..0ffbfd6d3140526b638b35ec860a41d72971f7d7 --- /dev/null +++ b/packages/backend/src/db/migrations/20240507135944_update_installation_completed_for_config.js @@ -0,0 +1,17 @@ +export async function up(knex) { + const users = await knex('users').limit(1); + + // no user implies installation is not completed yet. + if (users.length === 0) return; + + await knex('config').insert({ + key: 'installation.completed', + value: { + data: true + } + }); +}; + +export async function down(knex) { + await knex('config').where({ key: 'installation.completed' }).delete(); +}; diff --git a/packages/backend/src/db/migrations/20240509202750_make_value_column_text_in_datastore.js b/packages/backend/src/db/migrations/20240509202750_make_value_column_text_in_datastore.js new file mode 100644 index 0000000000000000000000000000000000000000..67c5a67b7fabf0989b5d9e6413b290a05bf99d2e --- /dev/null +++ b/packages/backend/src/db/migrations/20240509202750_make_value_column_text_in_datastore.js @@ -0,0 +1,11 @@ +export async function up(knex) { + return knex.schema.alterTable('datastore', (table) => { + table.text('value').alter(); + }); +} + +export async function down(knex) { + return knex.schema.alterTable('datastore', (table) => { + table.string('value').alter(); + }); +} diff --git a/packages/backend/src/errors/already-processed.js b/packages/backend/src/errors/already-processed.js new file mode 100644 index 0000000000000000000000000000000000000000..d38b0ea390c0210bc227780a50be01cbdac09cc9 --- /dev/null +++ b/packages/backend/src/errors/already-processed.js @@ -0,0 +1,3 @@ +import BaseError from './base.js'; + +export default class AlreadyProcessedError extends BaseError {} diff --git a/packages/backend/src/errors/base.js b/packages/backend/src/errors/base.js new file mode 100644 index 0000000000000000000000000000000000000000..33dfb57cf7aa3ee28e4f6e2e521f5294f6ec12f0 --- /dev/null +++ b/packages/backend/src/errors/base.js @@ -0,0 +1,33 @@ +export default class BaseError extends Error { + details = {}; + + constructor(error) { + let computedError; + + try { + computedError = JSON.parse(error); + } catch { + computedError = + typeof error === 'string' || Array.isArray(error) ? { error } : error; + } + + let computedMessage; + + try { + // challenge to input to see if it is stringified JSON + JSON.parse(error); + computedMessage = error; + } catch { + if (typeof error === 'string') { + computedMessage = error; + } else { + computedMessage = JSON.stringify(error, null, 2); + } + } + + super(computedMessage); + + this.details = computedError; + this.name = this.constructor.name; + } +} diff --git a/packages/backend/src/errors/early-exit.js b/packages/backend/src/errors/early-exit.js new file mode 100644 index 0000000000000000000000000000000000000000..5e8a2451420cabf330646f5658901c4537d6cb87 --- /dev/null +++ b/packages/backend/src/errors/early-exit.js @@ -0,0 +1,3 @@ +import BaseError from './base.js'; + +export default class EarlyExitError extends BaseError {} diff --git a/packages/backend/src/errors/generate-auth-url.js b/packages/backend/src/errors/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..734a3001cacff64f65206340b53a910a3b668951 --- /dev/null +++ b/packages/backend/src/errors/generate-auth-url.js @@ -0,0 +1,10 @@ +import BaseError from './base'; + +export default class GenerateAuthUrlError extends BaseError { + constructor(error) { + const computedError = error.response?.data || error.message; + super(computedError); + + this.message = `Error occured while creating authorization URL!`; + } +} diff --git a/packages/backend/src/errors/http.js b/packages/backend/src/errors/http.js new file mode 100644 index 0000000000000000000000000000000000000000..1d0af01e9f824be291e68ec7dbaba3102ab2503b --- /dev/null +++ b/packages/backend/src/errors/http.js @@ -0,0 +1,10 @@ +import BaseError from './base.js'; + +export default class HttpError extends BaseError { + constructor(error) { + const computedError = error.response?.data || error.message; + super(computedError); + + this.response = error.response; + } +} diff --git a/packages/backend/src/errors/quote-exceeded.js b/packages/backend/src/errors/quote-exceeded.js new file mode 100644 index 0000000000000000000000000000000000000000..7ca48d3c458324d97a6c20777252d653f3d05f2b --- /dev/null +++ b/packages/backend/src/errors/quote-exceeded.js @@ -0,0 +1,9 @@ +import BaseError from './base.js'; + +export default class QuotaExceededError extends BaseError { + constructor(error = 'The allowed task quota has been exhausted!') { + super(error); + + this.statusCode = 422; + } +} diff --git a/packages/backend/src/graphql/mutation-resolvers.js b/packages/backend/src/graphql/mutation-resolvers.js new file mode 100644 index 0000000000000000000000000000000000000000..5d90f1373b3bf012b5294ad69cab7d78262f07f7 --- /dev/null +++ b/packages/backend/src/graphql/mutation-resolvers.js @@ -0,0 +1,73 @@ +import createAppAuthClient from './mutations/create-app-auth-client.ee.js'; +import createAppConfig from './mutations/create-app-config.ee.js'; +import createConnection from './mutations/create-connection.js'; +import createFlow from './mutations/create-flow.js'; +import createRole from './mutations/create-role.ee.js'; +import createStep from './mutations/create-step.js'; +import createUser from './mutations/create-user.ee.js'; +import deleteConnection from './mutations/delete-connection.js'; +import deleteCurrentUser from './mutations/delete-current-user.ee.js'; +import deleteFlow from './mutations/delete-flow.js'; +import deleteRole from './mutations/delete-role.ee.js'; +import deleteStep from './mutations/delete-step.js'; +import deleteUser from './mutations/delete-user.ee.js'; +import duplicateFlow from './mutations/duplicate-flow.js'; +import executeFlow from './mutations/execute-flow.js'; +import forgotPassword from './mutations/forgot-password.ee.js'; +import generateAuthUrl from './mutations/generate-auth-url.js'; +import login from './mutations/login.js'; +import registerUser from './mutations/register-user.ee.js'; +import resetConnection from './mutations/reset-connection.js'; +import resetPassword from './mutations/reset-password.ee.js'; +import updateAppAuthClient from './mutations/update-app-auth-client.ee.js'; +import updateAppConfig from './mutations/update-app-config.ee.js'; +import updateConfig from './mutations/update-config.ee.js'; +import updateConnection from './mutations/update-connection.js'; +import updateCurrentUser from './mutations/update-current-user.js'; +import updateFlow from './mutations/update-flow.js'; +import updateFlowStatus from './mutations/update-flow-status.js'; +import updateRole from './mutations/update-role.ee.js'; +import updateStep from './mutations/update-step.js'; +import updateUser from './mutations/update-user.ee.js'; +import upsertSamlAuthProvider from './mutations/upsert-saml-auth-provider.ee.js'; +import upsertSamlAuthProvidersRoleMappings from './mutations/upsert-saml-auth-providers-role-mappings.ee.js'; +import verifyConnection from './mutations/verify-connection.js'; + +const mutationResolvers = { + createAppAuthClient, + createAppConfig, + createConnection, + createFlow, + createRole, + createStep, + createUser, + deleteConnection, + deleteCurrentUser, + deleteFlow, + deleteRole, + deleteStep, + deleteUser, + duplicateFlow, + executeFlow, + forgotPassword, + generateAuthUrl, + login, + registerUser, + resetConnection, + resetPassword, + updateAppAuthClient, + updateAppConfig, + updateConfig, + updateConnection, + updateCurrentUser, + updateFlow, + updateFlowStatus, + updateRole, + updateStep, + updateUser, + upsertSamlAuthProvider, + upsertSamlAuthProvidersRoleMappings, + verifyConnection, +}; + +export default mutationResolvers; diff --git a/packages/backend/src/graphql/mutations/create-app-auth-client.ee.js b/packages/backend/src/graphql/mutations/create-app-auth-client.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..6af409842a393b510ccc158560c20807e1f1b770 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-app-auth-client.ee.js @@ -0,0 +1,17 @@ +import AppConfig from '../../models/app-config.js'; + +const createAppAuthClient = async (_parent, params, context) => { + context.currentUser.can('update', 'App'); + + const appConfig = await AppConfig.query() + .findById(params.input.appConfigId) + .throwIfNotFound(); + + const appAuthClient = await appConfig + .$relatedQuery('appAuthClients') + .insert(params.input); + + return appAuthClient; +}; + +export default createAppAuthClient; diff --git a/packages/backend/src/graphql/mutations/create-app-config.ee.js b/packages/backend/src/graphql/mutations/create-app-config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..32b1debc68a731e01d8bbf725470f6501f371f41 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-app-config.ee.js @@ -0,0 +1,18 @@ +import App from '../../models/app.js'; +import AppConfig from '../../models/app-config.js'; + +const createAppConfig = async (_parent, params, context) => { + context.currentUser.can('update', 'App'); + + const key = params.input.key; + + const app = await App.findOneByKey(key); + + if (!app) throw new Error('The app cannot be found!'); + + const appConfig = await AppConfig.query().insert(params.input); + + return appConfig; +}; + +export default createAppConfig; diff --git a/packages/backend/src/graphql/mutations/create-connection.js b/packages/backend/src/graphql/mutations/create-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..2378a0eb8ee524656feb5cbb50469a02383793e2 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-connection.js @@ -0,0 +1,48 @@ +import App from '../../models/app.js'; +import AppConfig from '../../models/app-config.js'; + +const createConnection = async (_parent, params, context) => { + context.currentUser.can('create', 'Connection'); + + const { key, appAuthClientId } = params.input; + + const app = await App.findOneByKey(key); + + const appConfig = await AppConfig.query().findOne({ key }); + + let formattedData = params.input.formattedData; + if (appConfig) { + if (appConfig.disabled) + throw new Error( + 'This application has been disabled for new connections!' + ); + + if (!appConfig.allowCustomConnection && formattedData) + throw new Error(`Custom connections cannot be created for ${app.name}!`); + + if (appConfig.shared && !formattedData) { + const authClient = await appConfig + .$relatedQuery('appAuthClients') + .findById(appAuthClientId) + .where({ + active: true, + }) + .throwIfNotFound(); + + formattedData = authClient.formattedAuthDefaults; + } + } + + const createdConnection = await context.currentUser + .$relatedQuery('connections') + .insert({ + key, + appAuthClientId, + formattedData, + verified: false, + }); + + return createdConnection; +}; + +export default createConnection; diff --git a/packages/backend/src/graphql/mutations/create-flow.js b/packages/backend/src/graphql/mutations/create-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..c16166d93335ee4c14b8219dee73b3c66131ebaa --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-flow.js @@ -0,0 +1,45 @@ +import App from '../../models/app.js'; +import Step from '../../models/step.js'; + +const createFlow = async (_parent, params, context) => { + context.currentUser.can('create', 'Flow'); + + const connectionId = params?.input?.connectionId; + const appKey = params?.input?.triggerAppKey; + + if (appKey) { + await App.findOneByKey(appKey); + } + + const flow = await context.currentUser.$relatedQuery('flows').insert({ + name: 'Name your flow', + }); + + if (connectionId) { + const hasConnection = await context.currentUser + .$relatedQuery('connections') + .findById(connectionId); + + if (!hasConnection) { + throw new Error('The connection does not exist!'); + } + } + + await Step.query().insert({ + flowId: flow.id, + type: 'trigger', + position: 1, + appKey, + connectionId, + }); + + await Step.query().insert({ + flowId: flow.id, + type: 'action', + position: 2, + }); + + return flow; +}; + +export default createFlow; diff --git a/packages/backend/src/graphql/mutations/create-role.ee.js b/packages/backend/src/graphql/mutations/create-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7e85207b04c2718827aaf182cda0ab170226e777 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-role.ee.js @@ -0,0 +1,29 @@ +import kebabCase from 'lodash/kebabCase.js'; +import Role from '../../models/role.js'; + +const createRole = async (_parent, params, context) => { + context.currentUser.can('create', 'Role'); + + const { name, description, permissions } = params.input; + const key = kebabCase(name); + + const existingRole = await Role.query().findOne({ key }); + + if (existingRole) { + throw new Error('Role already exists!'); + } + + return await Role.query() + .insertGraph( + { + key, + name, + description, + permissions, + }, + { relate: ['permissions'] } + ) + .returning('*'); +}; + +export default createRole; diff --git a/packages/backend/src/graphql/mutations/create-step.js b/packages/backend/src/graphql/mutations/create-step.js new file mode 100644 index 0000000000000000000000000000000000000000..05b8d3644a9a8c5cc0d7ddfd080532d34efeac59 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-step.js @@ -0,0 +1,56 @@ +import App from '../../models/app.js'; +import Flow from '../../models/flow.js'; + +const createStep = async (_parent, params, context) => { + const conditions = context.currentUser.can('update', 'Flow'); + const userFlows = context.currentUser.$relatedQuery('flows'); + const allFlows = Flow.query(); + const flowsQuery = conditions.isCreator ? userFlows : allFlows; + + const { input } = params; + + if (input.appKey && input.key) { + await App.checkAppAndAction(input.appKey, input.key); + } + + if (input.appKey && !input.key) { + await App.findOneByKey(input.appKey); + } + + const flow = await flowsQuery + .findOne({ + id: input.flow.id, + }) + .throwIfNotFound(); + + const previousStep = await flow + .$relatedQuery('steps') + .findOne({ + id: input.previousStep.id, + }) + .throwIfNotFound(); + + const step = await flow.$relatedQuery('steps').insertAndFetch({ + key: input.key, + appKey: input.appKey, + type: 'action', + position: previousStep.position + 1, + }); + + const nextSteps = await flow + .$relatedQuery('steps') + .where('position', '>=', step.position) + .whereNot('id', step.id); + + const nextStepQueries = nextSteps.map(async (nextStep, index) => { + await nextStep.$query().patchAndFetch({ + position: step.position + index + 1, + }); + }); + + await Promise.all(nextStepQueries); + + return step; +}; + +export default createStep; diff --git a/packages/backend/src/graphql/mutations/create-user.ee.js b/packages/backend/src/graphql/mutations/create-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2bd732a1f6838f4bfc600df5274378eea0ba9ba9 --- /dev/null +++ b/packages/backend/src/graphql/mutations/create-user.ee.js @@ -0,0 +1,38 @@ +import User from '../../models/user.js'; +import Role from '../../models/role.js'; + +const createUser = async (_parent, params, context) => { + context.currentUser.can('create', 'User'); + + const { fullName, email, password } = params.input; + + const existingUser = await User.query().findOne({ + email: email.toLowerCase(), + }); + + if (existingUser) { + throw new Error('User already exists!'); + } + + const userPayload = { + fullName, + email, + password, + }; + + try { + context.currentUser.can('update', 'Role'); + + userPayload.roleId = params.input.role.id; + } catch { + // void + const role = await Role.query().findOne({ key: 'admin' }); + userPayload.roleId = role.id; + } + + const user = await User.query().insert(userPayload); + + return user; +}; + +export default createUser; diff --git a/packages/backend/src/graphql/mutations/delete-app-auth-client.ee.js b/packages/backend/src/graphql/mutations/delete-app-auth-client.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..0bc84e457f7a9631afb75f3c9bfc7a75f6f16b55 --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-app-auth-client.ee.js @@ -0,0 +1,16 @@ +import AppAuthClient from '../../models/app-auth-client'; + +const deleteAppAuthClient = async (_parent, params, context) => { + context.currentUser.can('delete', 'App'); + + await AppAuthClient.query() + .delete() + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + return; +}; + +export default deleteAppAuthClient; diff --git a/packages/backend/src/graphql/mutations/delete-connection.js b/packages/backend/src/graphql/mutations/delete-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..7647807d9deaca1c0f2cb77a8fda1885f3ef642c --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-connection.js @@ -0,0 +1,15 @@ +const deleteConnection = async (_parent, params, context) => { + context.currentUser.can('delete', 'Connection'); + + await context.currentUser + .$relatedQuery('connections') + .delete() + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + return; +}; + +export default deleteConnection; diff --git a/packages/backend/src/graphql/mutations/delete-current-user.ee.js b/packages/backend/src/graphql/mutations/delete-current-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..273b4ce285157209f2537d5bd8367ea330d839a8 --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-current-user.ee.js @@ -0,0 +1,58 @@ +import { Duration } from 'luxon'; +import deleteUserQueue from '../../queues/delete-user.ee.js'; +import flowQueue from '../../queues/flow.js'; +import Flow from '../../models/flow.js'; +import ExecutionStep from '../../models/execution-step.js'; +import appConfig from '../../config/app.js'; + +const deleteCurrentUser = async (_parent, params, context) => { + const id = context.currentUser.id; + + const flows = await context.currentUser.$relatedQuery('flows').where({ + active: true, + }); + + const repeatableJobs = await flowQueue.getRepeatableJobs(); + + for (const flow of flows) { + const job = repeatableJobs.find((job) => job.id === flow.id); + + if (job) { + await flowQueue.removeRepeatableByKey(job.key); + } + } + + const executionIds = ( + await context.currentUser + .$relatedQuery('executions') + .select('executions.id') + ).map((execution) => execution.id); + const flowIds = flows.map((flow) => flow.id); + + await ExecutionStep.query().delete().whereIn('execution_id', executionIds); + await context.currentUser.$relatedQuery('executions').delete(); + await context.currentUser.$relatedQuery('steps').delete(); + await Flow.query().whereIn('id', flowIds).delete(); + await context.currentUser.$relatedQuery('connections').delete(); + await context.currentUser.$relatedQuery('identities').delete(); + + if (appConfig.isCloud) { + await context.currentUser.$relatedQuery('subscriptions').delete(); + await context.currentUser.$relatedQuery('usageData').delete(); + } + + await context.currentUser.$query().delete(); + + const jobName = `Delete user - ${id}`; + const jobPayload = { id }; + const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); + const jobOptions = { + delay: millisecondsFor30Days, + }; + + await deleteUserQueue.add(jobName, jobPayload, jobOptions); + + return true; +}; + +export default deleteCurrentUser; diff --git a/packages/backend/src/graphql/mutations/delete-flow.js b/packages/backend/src/graphql/mutations/delete-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..484e09c98f9d0279ad5521cf0e3c844f807815ea --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-flow.js @@ -0,0 +1,53 @@ +import Flow from '../../models/flow.js'; +import ExecutionStep from '../../models/execution-step.js'; +import globalVariable from '../../helpers/global-variable.js'; +import logger from '../../helpers/logger.js'; + +const deleteFlow = async (_parent, params, context) => { + const conditions = context.currentUser.can('delete', 'Flow'); + const isCreator = conditions.isCreator; + const allFlows = Flow.query(); + const userFlows = context.currentUser.$relatedQuery('flows'); + const baseQuery = isCreator ? userFlows : allFlows; + + const flow = await baseQuery + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + const triggerStep = await flow.getTriggerStep(); + const trigger = await triggerStep?.getTriggerCommand(); + + if (trigger?.type === 'webhook' && trigger.unregisterHook) { + const $ = await globalVariable({ + flow, + connection: await triggerStep.$relatedQuery('connection'), + app: await triggerStep.getApp(), + step: triggerStep, + }); + + try { + await trigger.unregisterHook($); + } catch (error) { + // suppress error as the remote resource might have been already deleted + logger.debug( + `Failed to unregister webhook for flow ${flow.id}: ${error.message}` + ); + } + } + + const executionIds = ( + await flow.$relatedQuery('executions').select('executions.id') + ).map((execution) => execution.id); + + await ExecutionStep.query().delete().whereIn('execution_id', executionIds); + + await flow.$relatedQuery('executions').delete(); + await flow.$relatedQuery('steps').delete(); + await flow.$query().delete(); + + return; +}; + +export default deleteFlow; diff --git a/packages/backend/src/graphql/mutations/delete-role.ee.js b/packages/backend/src/graphql/mutations/delete-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..9b0249a2a11a5f3826368c74c4cd820387abd59c --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-role.ee.js @@ -0,0 +1,36 @@ +import Role from '../../models/role.js'; +import SamlAuthProvider from '../../models/saml-auth-provider.ee.js'; + +const deleteRole = async (_parent, params, context) => { + context.currentUser.can('delete', 'Role'); + + const role = await Role.query().findById(params.input.id).throwIfNotFound(); + const count = await role.$relatedQuery('users').resultSize(); + + if (count > 0) { + throw new Error('All users must be migrated away from the role!'); + } + + if (role.isAdmin) { + throw new Error('Admin role cannot be deleted!'); + } + + const samlAuthProviderUsingDefaultRole = await SamlAuthProvider.query() + .where({ default_role_id: role.id }) + .limit(1) + .first(); + + if (samlAuthProviderUsingDefaultRole) { + throw new Error( + 'You need to change the default role in the SAML configuration before deleting this role.' + ); + } + + // delete permissions first + await role.$relatedQuery('permissions').delete(); + await role.$query().delete(); + + return true; +}; + +export default deleteRole; diff --git a/packages/backend/src/graphql/mutations/delete-step.js b/packages/backend/src/graphql/mutations/delete-step.js new file mode 100644 index 0000000000000000000000000000000000000000..7406a41c2a24d76ec29709e497153837117ce565 --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-step.js @@ -0,0 +1,40 @@ +import Step from '../../models/step.js'; + +const deleteStep = async (_parent, params, context) => { + const conditions = context.currentUser.can('update', 'Flow'); + const isCreator = conditions.isCreator; + const allSteps = Step.query(); + const userSteps = context.currentUser.$relatedQuery('steps'); + const baseQuery = isCreator ? userSteps : allSteps; + + const step = await baseQuery + .withGraphFetched('flow') + .findOne({ + 'steps.id': params.input.id, + }) + .throwIfNotFound(); + + await step.$relatedQuery('executionSteps').delete(); + await step.$query().delete(); + + const nextSteps = await step.flow + .$relatedQuery('steps') + .where('position', '>', step.position); + + const nextStepQueries = nextSteps.map(async (nextStep) => { + await nextStep.$query().patch({ + position: nextStep.position - 1, + }); + }); + + await Promise.all(nextStepQueries); + + step.flow = await step.flow + .$query() + .withGraphJoined('steps') + .orderBy('steps.position', 'asc'); + + return step; +}; + +export default deleteStep; diff --git a/packages/backend/src/graphql/mutations/delete-user.ee.js b/packages/backend/src/graphql/mutations/delete-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7b66dd057c5d62012495d2ab84447e38fa43397d --- /dev/null +++ b/packages/backend/src/graphql/mutations/delete-user.ee.js @@ -0,0 +1,24 @@ +import { Duration } from 'luxon'; +import User from '../../models/user.js'; +import deleteUserQueue from '../../queues/delete-user.ee.js'; + +const deleteUser = async (_parent, params, context) => { + context.currentUser.can('delete', 'User'); + + const id = params.input.id; + + await User.query().deleteById(id); + + const jobName = `Delete user - ${id}`; + const jobPayload = { id }; + const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis(); + const jobOptions = { + delay: millisecondsFor30Days, + }; + + await deleteUserQueue.add(jobName, jobPayload, jobOptions); + + return true; +}; + +export default deleteUser; diff --git a/packages/backend/src/graphql/mutations/duplicate-flow.js b/packages/backend/src/graphql/mutations/duplicate-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..77e0a70bca243d591be5fa413c7d5fbf37d8e884 --- /dev/null +++ b/packages/backend/src/graphql/mutations/duplicate-flow.js @@ -0,0 +1,78 @@ +function updateStepId(value, newStepIds) { + let newValue = value; + + const stepIdEntries = Object.entries(newStepIds); + for (const stepIdEntry of stepIdEntries) { + const [oldStepId, newStepId] = stepIdEntry; + const partialOldVariable = `{{step.${oldStepId}.`; + const partialNewVariable = `{{step.${newStepId}.`; + + newValue = newValue.replace(partialOldVariable, partialNewVariable); + } + + return newValue; +} + +function updateStepVariables(parameters, newStepIds) { + const entries = Object.entries(parameters); + return entries.reduce((result, [key, value]) => { + if (typeof value === 'string') { + return { + ...result, + [key]: updateStepId(value, newStepIds), + }; + } + + if (Array.isArray(value)) { + return { + ...result, + [key]: value.map((item) => updateStepVariables(item, newStepIds)), + }; + } + + return { + ...result, + [key]: value, + }; + }, {}); +} + +const duplicateFlow = async (_parent, params, context) => { + context.currentUser.can('create', 'Flow'); + + const flow = await context.currentUser + .$relatedQuery('flows') + .withGraphJoined('[steps]') + .orderBy('steps.position', 'asc') + .findOne({ 'flows.id': params.input.id }) + .throwIfNotFound(); + + const duplicatedFlow = await context.currentUser + .$relatedQuery('flows') + .insert({ + name: `Copy of ${flow.name}`, + active: false, + }); + + const newStepIds = {}; + for (const step of flow.steps) { + const duplicatedStep = await duplicatedFlow.$relatedQuery('steps').insert({ + key: step.key, + appKey: step.appKey, + type: step.type, + connectionId: step.connectionId, + position: step.position, + parameters: updateStepVariables(step.parameters, newStepIds), + }); + + if (duplicatedStep.isTrigger) { + await duplicatedStep.updateWebhookUrl(); + } + + newStepIds[step.id] = duplicatedStep.id; + } + + return duplicatedFlow; +}; + +export default duplicateFlow; diff --git a/packages/backend/src/graphql/mutations/execute-flow.js b/packages/backend/src/graphql/mutations/execute-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..501b723bcb9d11b0a076a4bf305978d285504494 --- /dev/null +++ b/packages/backend/src/graphql/mutations/execute-flow.js @@ -0,0 +1,28 @@ +import testRun from '../../services/test-run.js'; +import Step from '../../models/step.js'; + +const executeFlow = async (_parent, params, context) => { + const conditions = context.currentUser.can('update', 'Flow'); + const isCreator = conditions.isCreator; + const allSteps = Step.query(); + const userSteps = context.currentUser.$relatedQuery('steps'); + const baseQuery = isCreator ? userSteps : allSteps; + + const { stepId } = params.input; + + const untilStep = await baseQuery.clone().findById(stepId).throwIfNotFound(); + + const { executionStep } = await testRun({ stepId }); + + if (executionStep.isFailed) { + throw new Error(JSON.stringify(executionStep.errorDetails)); + } + + await untilStep.$query().patch({ + status: 'completed', + }); + + return { data: executionStep.dataOut, step: untilStep }; +}; + +export default executeFlow; diff --git a/packages/backend/src/graphql/mutations/forgot-password.ee.js b/packages/backend/src/graphql/mutations/forgot-password.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..da5446d972e4d00ba3123c32231133e37da2d803 --- /dev/null +++ b/packages/backend/src/graphql/mutations/forgot-password.ee.js @@ -0,0 +1,43 @@ +import appConfig from '../../config/app.js'; +import User from '../../models/user.js'; +import emailQueue from '../../queues/email.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../../helpers/remove-job-configuration.js'; + +const forgotPassword = async (_parent, params) => { + const { email } = params.input; + + const user = await User.query().findOne({ email: email.toLowerCase() }); + + if (!user) { + throw new Error('Email address not found!'); + } + + await user.generateResetPasswordToken(); + + const jobName = `Reset Password Email - ${user.id}`; + + const jobPayload = { + email: user.email, + subject: 'Reset Password', + template: 'reset-password-instructions', + params: { + token: user.resetPasswordToken, + webAppUrl: appConfig.webAppUrl, + fullName: user.fullName, + }, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + await emailQueue.add(jobName, jobPayload, jobOptions); + + return true; +}; + +export default forgotPassword; diff --git a/packages/backend/src/graphql/mutations/generate-auth-url.js b/packages/backend/src/graphql/mutations/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..17948c1463b8efe728d3218e968c02d42376be0d --- /dev/null +++ b/packages/backend/src/graphql/mutations/generate-auth-url.js @@ -0,0 +1,30 @@ +import globalVariable from '../../helpers/global-variable.js'; +import App from '../../models/app.js'; + +const generateAuthUrl = async (_parent, params, context) => { + context.currentUser.can('create', 'Connection'); + + const connection = await context.currentUser + .$relatedQuery('connections') + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + if (!connection.formattedData) { + return null; + } + + const authInstance = ( + await import(`../../apps/${connection.key}/auth/index.js`) + ).default; + + const app = await App.findOneByKey(connection.key); + + const $ = await globalVariable({ connection, app }); + await authInstance.generateAuthUrl($); + + return connection.formattedData; +}; + +export default generateAuthUrl; diff --git a/packages/backend/src/graphql/mutations/login.js b/packages/backend/src/graphql/mutations/login.js new file mode 100644 index 0000000000000000000000000000000000000000..b05045706ae903fa85499e2c2ef9466d502789d0 --- /dev/null +++ b/packages/backend/src/graphql/mutations/login.js @@ -0,0 +1,17 @@ +import User from '../../models/user.js'; +import createAuthTokenByUserId from '../../helpers/create-auth-token-by-user-id.js'; + +const login = async (_parent, params) => { + const user = await User.query().findOne({ + email: params.input.email.toLowerCase(), + }); + + if (user && (await user.login(params.input.password))) { + const token = await createAuthTokenByUserId(user.id); + return { token, user }; + } + + throw new Error('User could not be found.'); +}; + +export default login; diff --git a/packages/backend/src/graphql/mutations/register-user.ee.js b/packages/backend/src/graphql/mutations/register-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..e734e763ac2597b5d059659db165bbe3a7d6fca1 --- /dev/null +++ b/packages/backend/src/graphql/mutations/register-user.ee.js @@ -0,0 +1,30 @@ +import appConfig from '../../config/app.js'; +import User from '../../models/user.js'; +import Role from '../../models/role.js'; + +const registerUser = async (_parent, params) => { + if (!appConfig.isCloud) return; + + const { fullName, email, password } = params.input; + + const existingUser = await User.query().findOne({ + email: email.toLowerCase(), + }); + + if (existingUser) { + throw new Error('User already exists!'); + } + + const role = await Role.query().findOne({ key: 'user' }); + + const user = await User.query().insert({ + fullName, + email, + password, + roleId: role.id, + }); + + return user; +}; + +export default registerUser; diff --git a/packages/backend/src/graphql/mutations/reset-connection.js b/packages/backend/src/graphql/mutations/reset-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..212cede7f8c51daddd18befdd8a3aec8ed9e252f --- /dev/null +++ b/packages/backend/src/graphql/mutations/reset-connection.js @@ -0,0 +1,22 @@ +const resetConnection = async (_parent, params, context) => { + context.currentUser.can('create', 'Connection'); + + let connection = await context.currentUser + .$relatedQuery('connections') + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + if (!connection.formattedData) { + return null; + } + + connection = await connection.$query().patchAndFetch({ + formattedData: { screenName: connection.formattedData.screenName }, + }); + + return connection; +}; + +export default resetConnection; diff --git a/packages/backend/src/graphql/mutations/reset-password.ee.js b/packages/backend/src/graphql/mutations/reset-password.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..309b006a2e353f30d33d82a6b861116fe5bfc4e8 --- /dev/null +++ b/packages/backend/src/graphql/mutations/reset-password.ee.js @@ -0,0 +1,23 @@ +import User from '../../models/user.js'; + +const resetPassword = async (_parent, params) => { + const { token, password } = params.input; + + if (!token) { + throw new Error('Reset password token is required!'); + } + + const user = await User.query().findOne({ reset_password_token: token }); + + if (!user || !user.isResetPasswordTokenValid()) { + throw new Error( + 'Reset password link is not valid or expired. Try generating a new link.' + ); + } + + await user.resetPassword(password); + + return true; +}; + +export default resetPassword; diff --git a/packages/backend/src/graphql/mutations/update-app-auth-client.ee.js b/packages/backend/src/graphql/mutations/update-app-auth-client.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..bbd0380cdadccaeac101f96afce6bba3741b9190 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-app-auth-client.ee.js @@ -0,0 +1,17 @@ +import AppAuthClient from '../../models/app-auth-client.js'; + +const updateAppAuthClient = async (_parent, params, context) => { + context.currentUser.can('update', 'App'); + + const { id, ...appAuthClientData } = params.input; + + const appAuthClient = await AppAuthClient.query() + .findById(id) + .throwIfNotFound(); + + await appAuthClient.$query().patch(appAuthClientData); + + return appAuthClient; +}; + +export default updateAppAuthClient; diff --git a/packages/backend/src/graphql/mutations/update-app-config.ee.js b/packages/backend/src/graphql/mutations/update-app-config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1187aee705d632916c52e0a7814ef1db5493b075 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-app-config.ee.js @@ -0,0 +1,15 @@ +import AppConfig from '../../models/app-config.js'; + +const updateAppConfig = async (_parent, params, context) => { + context.currentUser.can('update', 'App'); + + const { id, ...appConfigToUpdate } = params.input; + + const appConfig = await AppConfig.query().findById(id).throwIfNotFound(); + + await appConfig.$query().patch(appConfigToUpdate); + + return appConfig; +}; + +export default updateAppConfig; diff --git a/packages/backend/src/graphql/mutations/update-config.ee.js b/packages/backend/src/graphql/mutations/update-config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..ad216d64df583179f1aff95123106f5f5c59d83f --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-config.ee.js @@ -0,0 +1,40 @@ +import Config from '../../models/config.js'; + +const updateConfig = async (_parent, params, context) => { + context.currentUser.can('update', 'Config'); + + const config = params.input; + const configKeys = Object.keys(config); + const updates = []; + + for (const key of configKeys) { + const newValue = config[key]; + + if (newValue) { + const entryUpdate = Config.query() + .insert({ + key, + value: { + data: newValue, + }, + }) + .onConflict('key') + .merge({ + value: { + data: newValue, + }, + }); + + updates.push(entryUpdate); + } else { + const entryUpdate = Config.query().findOne({ key }).delete(); + updates.push(entryUpdate); + } + } + + await Promise.all(updates); + + return config; +}; + +export default updateConfig; diff --git a/packages/backend/src/graphql/mutations/update-connection.js b/packages/backend/src/graphql/mutations/update-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..bc8cdc58dc5be2f2a96b6041b12b71979a2a7c88 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-connection.js @@ -0,0 +1,33 @@ +import AppAuthClient from '../../models/app-auth-client.js'; + +const updateConnection = async (_parent, params, context) => { + context.currentUser.can('create', 'Connection'); + + let connection = await context.currentUser + .$relatedQuery('connections') + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + let formattedData = params.input.formattedData; + + if (params.input.appAuthClientId) { + const appAuthClient = await AppAuthClient.query() + .findById(params.input.appAuthClientId) + .throwIfNotFound(); + + formattedData = appAuthClient.formattedAuthDefaults; + } + + connection = await connection.$query().patchAndFetch({ + formattedData: { + ...connection.formattedData, + ...formattedData, + }, + }); + + return connection; +}; + +export default updateConnection; diff --git a/packages/backend/src/graphql/mutations/update-current-user.js b/packages/backend/src/graphql/mutations/update-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..187d78c9a16dea3246becf1d2a68f7b1bea577b6 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-current-user.js @@ -0,0 +1,11 @@ +const updateCurrentUser = async (_parent, params, context) => { + const user = await context.currentUser.$query().patchAndFetch({ + email: params.input.email, + password: params.input.password, + fullName: params.input.fullName, + }); + + return user; +}; + +export default updateCurrentUser; diff --git a/packages/backend/src/graphql/mutations/update-flow-status.js b/packages/backend/src/graphql/mutations/update-flow-status.js new file mode 100644 index 0000000000000000000000000000000000000000..169996f2cfd84e26ce10b30415e1c57937b88167 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-flow-status.js @@ -0,0 +1,86 @@ +import Flow from '../../models/flow.js'; +import flowQueue from '../../queues/flow.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../../helpers/remove-job-configuration.js'; +import globalVariable from '../../helpers/global-variable.js'; + +const JOB_NAME = 'flow'; +const EVERY_15_MINUTES_CRON = '*/15 * * * *'; + +const updateFlowStatus = async (_parent, params, context) => { + const conditions = context.currentUser.can('publish', 'Flow'); + const isCreator = conditions.isCreator; + const allFlows = Flow.query(); + const userFlows = context.currentUser.$relatedQuery('flows'); + const baseQuery = isCreator ? userFlows : allFlows; + + let flow = await baseQuery + .clone() + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + const newActiveValue = params.input.active; + + if (flow.active === newActiveValue) { + return flow; + } + + const triggerStep = await flow.getTriggerStep(); + const trigger = await triggerStep.getTriggerCommand(); + const interval = trigger.getInterval?.(triggerStep.parameters); + const repeatOptions = { + pattern: interval || EVERY_15_MINUTES_CRON, + }; + + if (trigger.type === 'webhook') { + const $ = await globalVariable({ + flow, + connection: await triggerStep.$relatedQuery('connection'), + app: await triggerStep.getApp(), + step: triggerStep, + testRun: false, + }); + + if (newActiveValue && trigger.registerHook) { + await trigger.registerHook($); + } else if (!newActiveValue && trigger.unregisterHook) { + await trigger.unregisterHook($); + } + } else { + if (newActiveValue) { + flow = await flow.$query().patchAndFetch({ + publishedAt: new Date().toISOString(), + }); + + const jobName = `${JOB_NAME}-${flow.id}`; + + await flowQueue.add( + jobName, + { flowId: flow.id }, + { + repeat: repeatOptions, + jobId: flow.id, + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + } + ); + } else { + const repeatableJobs = await flowQueue.getRepeatableJobs(); + const job = repeatableJobs.find((job) => job.id === flow.id); + + await flowQueue.removeRepeatableByKey(job.key); + } + } + + flow = await flow.$query().withGraphFetched('steps').patchAndFetch({ + active: newActiveValue, + }); + + return flow; +}; + +export default updateFlowStatus; diff --git a/packages/backend/src/graphql/mutations/update-flow.js b/packages/backend/src/graphql/mutations/update-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..e8e21ef97075523c2ecfe18fe2703435b04499ac --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-flow.js @@ -0,0 +1,18 @@ +const updateFlow = async (_parent, params, context) => { + context.currentUser.can('update', 'Flow'); + + let flow = await context.currentUser + .$relatedQuery('flows') + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + flow = await flow.$query().patchAndFetch({ + name: params.input.name, + }); + + return flow; +}; + +export default updateFlow; diff --git a/packages/backend/src/graphql/mutations/update-role.ee.js b/packages/backend/src/graphql/mutations/update-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2566dc40f23c27ddf4029ffbb45c00b428bc12c7 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-role.ee.js @@ -0,0 +1,62 @@ +import Role from '../../models/role.js'; +import Permission from '../../models/permission.js'; +import permissionCatalog from '../../helpers/permission-catalog.ee.js'; + +const updateRole = async (_parent, params, context) => { + context.currentUser.can('update', 'Role'); + + const { id, name, description, permissions } = params.input; + + const role = await Role.query().findById(id).throwIfNotFound(); + + try { + const updatedRole = await Role.transaction(async (trx) => { + await role.$relatedQuery('permissions', trx).delete(); + + if (permissions?.length) { + const sanitizedPermissions = permissions + .filter((permission) => { + const { action, subject, conditions } = permission; + + const relevantAction = permissionCatalog.actions.find( + (actionCatalogItem) => actionCatalogItem.key === action + ); + const validSubject = relevantAction.subjects.includes(subject); + const validConditions = conditions.every((condition) => { + return !!permissionCatalog.conditions.find( + (conditionCatalogItem) => conditionCatalogItem.key === condition + ); + }); + + return validSubject && validConditions; + }) + .map((permission) => ({ + ...permission, + roleId: role.id, + })); + + await Permission.query().insert(sanitizedPermissions); + } + + await role.$query(trx).patch({ + name, + description, + }); + + return await Role.query(trx) + .leftJoinRelated({ + permissions: true, + }) + .withGraphFetched({ + permissions: true, + }) + .findById(id); + }); + + return updatedRole; + } catch (err) { + throw new Error('The role could not be updated!'); + } +}; + +export default updateRole; diff --git a/packages/backend/src/graphql/mutations/update-step.js b/packages/backend/src/graphql/mutations/update-step.js new file mode 100644 index 0000000000000000000000000000000000000000..3d87bb537c576758dbe617589964db995b0852f4 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-step.js @@ -0,0 +1,67 @@ +import App from '../../models/app.js'; +import Step from '../../models/step.js'; +import Connection from '../../models/connection.js'; + +const updateStep = async (_parent, params, context) => { + const { isCreator } = context.currentUser.can('update', 'Flow'); + const userSteps = context.currentUser.$relatedQuery('steps'); + const allSteps = Step.query(); + const baseQuery = isCreator ? userSteps : allSteps; + + const { input } = params; + + let step = await baseQuery + .findOne({ + 'steps.id': input.id, + flow_id: input.flow.id, + }) + .throwIfNotFound(); + + if (input.connection.id) { + let canSeeAllConnections = false; + try { + const conditions = context.currentUser.can('read', 'Connection'); + + canSeeAllConnections = !conditions.isCreator; + } catch { + // void + } + + const userConnections = context.currentUser.$relatedQuery('connections'); + const allConnections = Connection.query(); + const baseConnectionsQuery = canSeeAllConnections + ? allConnections + : userConnections; + + const connection = await baseConnectionsQuery + .clone() + .findById(input.connection?.id); + + if (!connection) { + throw new Error('The connection does not exist!'); + } + } + + if (step.isTrigger) { + await App.checkAppAndTrigger(input.appKey, input.key); + } + + if (step.isAction) { + await App.checkAppAndAction(input.appKey, input.key); + } + + step = await Step.query() + .patchAndFetchById(input.id, { + key: input.key, + appKey: input.appKey, + connectionId: input.connection.id, + parameters: input.parameters, + }) + .withGraphFetched('connection'); + + await step.updateWebhookUrl(); + + return step; +}; + +export default updateStep; diff --git a/packages/backend/src/graphql/mutations/update-user.ee.js b/packages/backend/src/graphql/mutations/update-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..0e27eedb402f31e5736c3ff33283d57dd672a3b2 --- /dev/null +++ b/packages/backend/src/graphql/mutations/update-user.ee.js @@ -0,0 +1,27 @@ +import User from '../../models/user.js'; + +const updateUser = async (_parent, params, context) => { + context.currentUser.can('update', 'User'); + + const userPayload = { + email: params.input.email, + fullName: params.input.fullName, + }; + + try { + context.currentUser.can('update', 'Role'); + + userPayload.roleId = params.input.role.id; + } catch { + // void + } + + const user = await User.query().patchAndFetchById( + params.input.id, + userPayload + ); + + return user; +}; + +export default updateUser; diff --git a/packages/backend/src/graphql/mutations/upsert-saml-auth-provider.ee.js b/packages/backend/src/graphql/mutations/upsert-saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..382945b402f8456b8ef3e59329e08f03e8ae73c1 --- /dev/null +++ b/packages/backend/src/graphql/mutations/upsert-saml-auth-provider.ee.js @@ -0,0 +1,30 @@ +import SamlAuthProvider from '../../models/saml-auth-provider.ee.js'; + +const upsertSamlAuthProvider = async (_parent, params, context) => { + context.currentUser.can('create', 'SamlAuthProvider'); + + const samlAuthProviderPayload = { + ...params.input, + }; + + const existingSamlAuthProvider = await SamlAuthProvider.query() + .limit(1) + .first(); + + if (!existingSamlAuthProvider) { + const samlAuthProvider = await SamlAuthProvider.query().insert( + samlAuthProviderPayload + ); + + return samlAuthProvider; + } + + const samlAuthProvider = await SamlAuthProvider.query().patchAndFetchById( + existingSamlAuthProvider.id, + samlAuthProviderPayload + ); + + return samlAuthProvider; +}; + +export default upsertSamlAuthProvider; diff --git a/packages/backend/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ee.js b/packages/backend/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..0040d48eaa0438c09b9445271579b2cc89474f82 --- /dev/null +++ b/packages/backend/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.ee.js @@ -0,0 +1,42 @@ +import SamlAuthProvider from '../../models/saml-auth-provider.ee.js'; +import SamlAuthProvidersRoleMapping from '../../models/saml-auth-providers-role-mapping.ee.js'; +import isEmpty from 'lodash/isEmpty.js'; + +const upsertSamlAuthProvidersRoleMappings = async ( + _parent, + params, + context +) => { + context.currentUser.can('update', 'SamlAuthProvider'); + + const samlAuthProviderId = params.input.samlAuthProviderId; + + const samlAuthProvider = await SamlAuthProvider.query() + .findById(samlAuthProviderId) + .throwIfNotFound(); + + await samlAuthProvider + .$relatedQuery('samlAuthProvidersRoleMappings') + .delete(); + + if (isEmpty(params.input.samlAuthProvidersRoleMappings)) { + return []; + } + + const samlAuthProvidersRoleMappingsData = + params.input.samlAuthProvidersRoleMappings.map( + (samlAuthProvidersRoleMapping) => ({ + ...samlAuthProvidersRoleMapping, + samlAuthProviderId: samlAuthProvider.id, + }) + ); + + const samlAuthProvidersRoleMappings = + await SamlAuthProvidersRoleMapping.query().insert( + samlAuthProvidersRoleMappingsData + ); + + return samlAuthProvidersRoleMappings; +}; + +export default upsertSamlAuthProvidersRoleMappings; diff --git a/packages/backend/src/graphql/mutations/verify-connection.js b/packages/backend/src/graphql/mutations/verify-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..fdd957649d1482ec5fd801c5a250fd45d652afe0 --- /dev/null +++ b/packages/backend/src/graphql/mutations/verify-connection.js @@ -0,0 +1,29 @@ +import App from '../../models/app.js'; +import globalVariable from '../../helpers/global-variable.js'; + +const verifyConnection = async (_parent, params, context) => { + context.currentUser.can('create', 'Connection'); + + let connection = await context.currentUser + .$relatedQuery('connections') + .findOne({ + id: params.input.id, + }) + .throwIfNotFound(); + + const app = await App.findOneByKey(connection.key); + const $ = await globalVariable({ connection, app }); + await app.auth.verifyCredentials($); + + connection = await connection.$query().patchAndFetch({ + verified: true, + draft: false, + }); + + return { + ...connection, + app, + }; +}; + +export default verifyConnection; diff --git a/packages/backend/src/graphql/resolvers.js b/packages/backend/src/graphql/resolvers.js new file mode 100644 index 0000000000000000000000000000000000000000..70021c5194e9458ff008db567aaa77545fa05b1a --- /dev/null +++ b/packages/backend/src/graphql/resolvers.js @@ -0,0 +1,7 @@ +import mutationResolvers from './mutation-resolvers.js'; + +const resolvers = { + Mutation: mutationResolvers, +}; + +export default resolvers; diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql new file mode 100644 index 0000000000000000000000000000000000000000..2a5454d360eb357dfb33b1e2e1e6e881c39df10a --- /dev/null +++ b/packages/backend/src/graphql/schema.graphql @@ -0,0 +1,614 @@ +type Query { + placeholderQuery(name: String): Boolean +} +type Mutation { + createAppConfig(input: CreateAppConfigInput): AppConfig + createAppAuthClient(input: CreateAppAuthClientInput): AppAuthClient + createConnection(input: CreateConnectionInput): Connection + createFlow(input: CreateFlowInput): Flow + createRole(input: CreateRoleInput): Role + createStep(input: CreateStepInput): Step + createUser(input: CreateUserInput): User + deleteConnection(input: DeleteConnectionInput): Boolean + deleteCurrentUser: Boolean + deleteFlow(input: DeleteFlowInput): Boolean + deleteRole(input: DeleteRoleInput): Boolean + deleteStep(input: DeleteStepInput): Step + deleteUser(input: DeleteUserInput): Boolean + duplicateFlow(input: DuplicateFlowInput): Flow + executeFlow(input: ExecuteFlowInput): executeFlowType + forgotPassword(input: ForgotPasswordInput): Boolean + generateAuthUrl(input: GenerateAuthUrlInput): AuthLink + login(input: LoginInput): Auth + registerUser(input: RegisterUserInput): User + resetConnection(input: ResetConnectionInput): Connection + resetPassword(input: ResetPasswordInput): Boolean + updateAppAuthClient(input: UpdateAppAuthClientInput): AppAuthClient + updateAppConfig(input: UpdateAppConfigInput): AppConfig + updateConfig(input: JSONObject): JSONObject + updateConnection(input: UpdateConnectionInput): Connection + updateCurrentUser(input: UpdateCurrentUserInput): User + updateFlow(input: UpdateFlowInput): Flow + updateFlowStatus(input: UpdateFlowStatusInput): Flow + updateRole(input: UpdateRoleInput): Role + updateStep(input: UpdateStepInput): Step + updateUser(input: UpdateUserInput): User + upsertSamlAuthProvider(input: UpsertSamlAuthProviderInput): SamlAuthProvider + upsertSamlAuthProvidersRoleMappings( + input: UpsertSamlAuthProvidersRoleMappingsInput + ): [SamlAuthProvidersRoleMapping] + verifyConnection(input: VerifyConnectionInput): Connection +} + +""" +Exposes a URL that specifies the behaviour of this scalar. +""" +directive @specifiedBy( + """ + The URL that specifies the behaviour of this scalar. + """ + url: String! +) on SCALAR + +type Trigger { + name: String + key: String + description: String + showWebhookUrl: Boolean + pollInterval: Int + type: String + substeps: [Substep] +} + +type Action { + name: String + key: String + description: String + substeps: [Substep] +} + +type Substep { + key: String + name: String + arguments: [SubstepArgument] +} + +type SubstepArgument { + label: String + key: String + type: String + description: String + required: Boolean + variables: Boolean + options: [SubstepArgumentOption] + source: SubstepArgumentSource + additionalFields: SubstepArgumentAdditionalFields + dependsOn: [String] + fields: [SubstepArgument] + value: JSONObject +} + +type SubstepArgumentOption { + label: String + value: JSONObject +} + +type SubstepArgumentSource { + type: String + name: String + arguments: [SubstepArgumentSourceArgument] +} + +type SubstepArgumentSourceArgument { + name: String + value: String +} + +type SubstepArgumentAdditionalFields { + type: String + name: String + arguments: [SubstepArgumentAdditionalFieldsArgument] +} + +type SubstepArgumentAdditionalFieldsArgument { + name: String + value: String +} + +type AppConfig { + id: String + key: String + allowCustomConnection: Boolean + canConnect: Boolean + canCustomConnect: Boolean + shared: Boolean + disabled: Boolean +} + +type App { + name: String + key: String + connectionCount: Int + flowCount: Int + iconUrl: String + docUrl: String + authDocUrl: String + primaryColor: String + supportsConnections: Boolean + auth: AppAuth + triggers: [Trigger] + actions: [Action] + connections: [Connection] +} + +type AppAuth { + fields: [Field] + authenticationSteps: [AuthenticationStep] + sharedAuthenticationSteps: [AuthenticationStep] + reconnectionSteps: [ReconnectionStep] + sharedReconnectionSteps: [ReconnectionStep] +} + +enum ArgumentEnumType { + integer + string +} + +type Auth { + user: User + token: String +} + +type AuthenticationStep { + type: String + name: String + arguments: [AuthenticationStepArgument] +} + +type AuthenticationStepArgument { + name: String + value: String + type: ArgumentEnumType + properties: [AuthenticationStepProperty] +} + +type AuthenticationStepProperty { + name: String + value: String +} + +type AuthLink { + url: String +} + +type Connection { + id: String + key: String + reconnectable: Boolean + appAuthClientId: String + formattedData: ConnectionData + verified: Boolean + app: App + createdAt: String + flowCount: Int +} + +type ConnectionData { + screenName: String +} + +type executeFlowType { + data: JSONObject + step: Step +} + +type ExecutionStep { + id: String + executionId: String + stepId: String + step: Step + status: String + dataIn: JSONObject + dataOut: JSONObject + errorDetails: JSONObject + createdAt: String + updatedAt: String +} + +type Field { + key: String + label: String + type: String + required: Boolean + readOnly: Boolean + value: String + placeholder: String + description: String + docUrl: String + clickToCopy: Boolean + options: [SubstepArgumentOption] +} + +enum FlowStatus { + paused + published + draft +} + +type Flow { + id: String + name: String + active: Boolean + steps: [Step] + createdAt: String + updatedAt: String + status: FlowStatus +} + +type SamlAuthProvider { + id: String + name: String + certificate: String + signatureAlgorithm: String + issuer: String + entryPoint: String + firstnameAttributeName: String + surnameAttributeName: String + emailAttributeName: String + roleAttributeName: String + active: Boolean + defaultRoleId: String +} + +type SamlAuthProvidersRoleMapping { + id: String + samlAuthProviderId: String + roleId: String + remoteRoleName: String +} + +input CreateConnectionInput { + key: String! + appAuthClientId: String + formattedData: JSONObject +} + +input GenerateAuthUrlInput { + id: String! +} + +input UpdateConnectionInput { + id: String! + formattedData: JSONObject + appAuthClientId: String +} + +input ResetConnectionInput { + id: String! +} + +input VerifyConnectionInput { + id: String! +} + +input UpsertSamlAuthProviderInput { + name: String! + certificate: String! + signatureAlgorithm: String! + issuer: String! + entryPoint: String! + firstnameAttributeName: String! + surnameAttributeName: String! + emailAttributeName: String! + roleAttributeName: String! + defaultRoleId: String! + active: Boolean! +} + +input UpsertSamlAuthProvidersRoleMappingsInput { + samlAuthProviderId: String! + samlAuthProvidersRoleMappings: [SamlAuthProviderRoleMappingInput] +} + +input SamlAuthProviderRoleMappingInput { + roleId: String! + remoteRoleName: String! +} + +input DeleteConnectionInput { + id: String! +} + +input CreateFlowInput { + triggerAppKey: String + connectionId: String +} + +input UpdateFlowInput { + id: String! + name: String! +} + +input UpdateFlowStatusInput { + id: String! + active: Boolean! +} + +input ExecuteFlowInput { + stepId: String! +} + +input DeleteFlowInput { + id: String! +} + +input DuplicateFlowInput { + id: String! +} + +input CreateStepInput { + id: String + previousStepId: String + key: String + appKey: String + connection: StepConnectionInput + flow: StepFlowInput + parameters: JSONObject + previousStep: PreviousStepInput +} + +input UpdateStepInput { + id: String + previousStepId: String + key: String + appKey: String + connection: StepConnectionInput + flow: StepFlowInput + parameters: JSONObject + previousStep: PreviousStepInput +} + +input DeleteStepInput { + id: String! +} + +input CreateUserInput { + fullName: String! + email: String! + password: String! + role: UserRoleInput! +} + +input UserRoleInput { + id: String +} + +input UpdateUserInput { + id: String! + fullName: String + email: String + role: UserRoleInput +} + +input DeleteUserInput { + id: String! +} + +input RegisterUserInput { + fullName: String! + email: String! + password: String! +} + +input UpdateCurrentUserInput { + email: String + password: String + fullName: String +} + +input ForgotPasswordInput { + email: String! +} + +input ResetPasswordInput { + token: String! + password: String! +} + +input LoginInput { + email: String! + password: String! +} + +input PermissionInput { + action: String! + subject: String! + conditions: [String] +} + +input CreateRoleInput { + name: String! + description: String + permissions: [PermissionInput] +} + +input UpdateRoleInput { + id: String! + name: String! + description: String + permissions: [PermissionInput] +} + +input DeleteRoleInput { + id: String! +} + +""" +The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). +""" +scalar JSONObject + +input PreviousStepInput { + id: String +} + +type ReconnectionStep { + type: String + name: String + arguments: [ReconnectionStepArgument] +} + +type ReconnectionStepArgument { + name: String + value: String + type: ArgumentEnumType + properties: [ReconnectionStepProperty] +} + +type ReconnectionStepProperty { + name: String + value: String +} + +type Step { + id: String + previousStepId: String + key: String + appKey: String + iconUrl: String + webhookUrl: String + type: StepEnumType + parameters: JSONObject + connection: Connection + flow: Flow + position: Int + status: String + executionSteps: [ExecutionStep] +} + +input StepConnectionInput { + id: String +} + +enum StepEnumType { + trigger + action +} + +input StepFlowInput { + id: String +} + +input StepInput { + id: String + previousStepId: String + key: String + appKey: String + connection: StepConnectionInput + flow: StepFlowInput + parameters: JSONObject + previousStep: PreviousStepInput +} + +type User { + id: String + fullName: String + email: String + role: Role + permissions: [Permission] + createdAt: String + updatedAt: String +} + +type Role { + id: String + name: String + key: String + description: String + isAdmin: Boolean + permissions: [Permission] +} + +type PageInfo { + currentPage: Int! + totalPages: Int! +} + +type ExecutionStepEdge { + node: ExecutionStep +} + +type ExecutionStepConnection { + edges: [ExecutionStepEdge] + pageInfo: PageInfo +} + +type License { + id: String + name: String + expireAt: String + verified: Boolean +} + +type Permission { + id: String + action: String + subject: String + conditions: [String] +} + +type Action { + label: String + key: String + subjects: [String] +} + +type Condition { + key: String + label: String +} + +type Subject { + label: String + key: String +} + +input CreateAppConfigInput { + key: String + allowCustomConnection: Boolean + shared: Boolean + disabled: Boolean +} + +input UpdateAppConfigInput { + id: String + allowCustomConnection: Boolean + shared: Boolean + disabled: Boolean +} + +type AppAuthClient { + id: String + appConfigId: String + name: String + active: Boolean +} + +input CreateAppAuthClientInput { + appConfigId: String + name: String + formattedAuthDefaults: JSONObject + active: Boolean +} + +input UpdateAppAuthClientInput { + id: String + name: String + formattedAuthDefaults: JSONObject + active: Boolean +} + +schema { + query: Query + mutation: Mutation +} diff --git a/packages/backend/src/helpers/add-authentication-steps.js b/packages/backend/src/helpers/add-authentication-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..631e6055b5f4c10e67c798c59771e089cd3c5548 --- /dev/null +++ b/packages/backend/src/helpers/add-authentication-steps.js @@ -0,0 +1,161 @@ +function addAuthenticationSteps(app) { + if (app.auth.generateAuthUrl) { + app.auth.authenticationSteps = authenticationStepsWithAuthUrl; + app.auth.sharedAuthenticationSteps = sharedAuthenticationStepsWithAuthUrl; + } else { + app.auth.authenticationSteps = authenticationStepsWithoutAuthUrl; + } + + return app; +} + +const authenticationStepsWithoutAuthUrl = [ + { + type: 'mutation', + name: 'createConnection', + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'formattedData', + value: '{fields.all}', + }, + ], + }, + { + type: 'mutation', + name: 'verifyConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, +]; + +const authenticationStepsWithAuthUrl = [ + { + type: 'mutation', + name: 'createConnection', + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'formattedData', + value: '{fields.all}', + }, + ], + }, + { + type: 'mutation', + name: 'generateAuthUrl', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, + { + type: 'openWithPopup', + name: 'openAuthPopup', + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + }, + { + type: 'mutation', + name: 'updateConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + }, + { + type: 'mutation', + name: 'verifyConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, +]; + +const sharedAuthenticationStepsWithAuthUrl = [ + { + type: 'mutation', + name: 'createConnection', + arguments: [ + { + name: 'key', + value: '{key}', + }, + { + name: 'appAuthClientId', + value: '{appAuthClientId}', + }, + ], + }, + { + type: 'mutation', + name: 'generateAuthUrl', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, + { + type: 'openWithPopup', + name: 'openAuthPopup', + arguments: [ + { + name: 'url', + value: '{generateAuthUrl.url}', + }, + ], + }, + { + type: 'mutation', + name: 'updateConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + { + name: 'formattedData', + value: '{openAuthPopup.all}', + }, + ], + }, + { + type: 'mutation', + name: 'verifyConnection', + arguments: [ + { + name: 'id', + value: '{createConnection.id}', + }, + ], + }, +]; + +export default addAuthenticationSteps; diff --git a/packages/backend/src/helpers/add-reconnection-steps.js b/packages/backend/src/helpers/add-reconnection-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..58b2a53041bfc17f15cac567971fd1984734c4cc --- /dev/null +++ b/packages/backend/src/helpers/add-reconnection-steps.js @@ -0,0 +1,87 @@ +import cloneDeep from 'lodash/cloneDeep.js'; + +const connectionIdArgument = { + name: 'id', + value: '{connection.id}', +}; + +const resetConnectionStep = { + type: 'mutation', + name: 'resetConnection', + arguments: [connectionIdArgument], +}; + +function replaceCreateConnection(string) { + return string.replace('{createConnection.id}', '{connection.id}'); +} + +function removeAppKeyArgument(args) { + return args.filter((argument) => argument.name !== 'key'); +} + +function addConnectionId(step) { + step.arguments = step.arguments.map((argument) => { + if (typeof argument.value === 'string') { + argument.value = replaceCreateConnection(argument.value); + } + + if (argument.properties) { + argument.properties = argument.properties.map((property) => { + return { + name: property.name, + value: replaceCreateConnection(property.value), + }; + }); + } + + return argument; + }); + + return step; +} + +function replaceCreateConnectionsWithUpdate(steps) { + const updatedSteps = cloneDeep(steps); + return updatedSteps.map((step) => { + const updatedStep = addConnectionId(step); + + if (step.name === 'createConnection') { + updatedStep.name = 'updateConnection'; + updatedStep.arguments = removeAppKeyArgument(updatedStep.arguments); + updatedStep.arguments.unshift(connectionIdArgument); + + return updatedStep; + } + + return step; + }); +} + +function addReconnectionSteps(app) { + const hasReconnectionSteps = app.auth.reconnectionSteps; + + if (hasReconnectionSteps) return app; + + if (app.auth.authenticationSteps) { + const updatedSteps = replaceCreateConnectionsWithUpdate( + app.auth.authenticationSteps + ); + + app.auth.reconnectionSteps = [resetConnectionStep, ...updatedSteps]; + } + + if (app.auth.sharedAuthenticationSteps) { + const updatedStepsWithEmbeddedDefaults = replaceCreateConnectionsWithUpdate( + app.auth.sharedAuthenticationSteps + ); + + app.auth.sharedReconnectionSteps = [ + resetConnectionStep, + ...updatedStepsWithEmbeddedDefaults, + ]; + } + + return app; +} + +export default addReconnectionSteps; diff --git a/packages/backend/src/helpers/allow-installation.js b/packages/backend/src/helpers/allow-installation.js new file mode 100644 index 0000000000000000000000000000000000000000..33826a4ba888244cdcdc03cb9b139e31d90b8ec6 --- /dev/null +++ b/packages/backend/src/helpers/allow-installation.js @@ -0,0 +1,16 @@ +import Config from '../models/config.js'; +import User from '../models/user.js'; + +export async function allowInstallation(request, response, next) { + if (await Config.isInstallationCompleted()) { + return response.status(403).end(); + } + + const hasAnyUsers = await User.query().resultSize() > 0; + + if (hasAnyUsers) { + return response.status(403).end(); + } + + next(); +}; diff --git a/packages/backend/src/helpers/app-assets-handler.js b/packages/backend/src/helpers/app-assets-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..778ad25b574c42b72f67923a2ecbbfaa529c21a4 --- /dev/null +++ b/packages/backend/src/helpers/app-assets-handler.js @@ -0,0 +1,24 @@ +import express from 'express'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const appAssetsHandler = async (app) => { + app.use('/apps/:appKey/assets/favicon.svg', (req, res, next) => { + const { appKey } = req.params; + const svgPath = `${__dirname}/../apps/${appKey}/assets/favicon.svg`; + const staticFileHandlerOptions = { + /** + * Disabling fallthrough is important to respond with HTTP 404. + * Otherwise, web app might be served. + */ + fallthrough: false, + }; + const staticFileHandler = express.static(svgPath, staticFileHandlerOptions); + + return staticFileHandler(req, res, next); + }); +}; + +export default appAssetsHandler; diff --git a/packages/backend/src/helpers/app-info-converter.js b/packages/backend/src/helpers/app-info-converter.js new file mode 100644 index 0000000000000000000000000000000000000000..98374d8f7ee6e0e66cf9fb7b17a9a6ab9b3e59fa --- /dev/null +++ b/packages/backend/src/helpers/app-info-converter.js @@ -0,0 +1,30 @@ +import appConfig from '../config/app.js'; + +const appInfoConverter = (rawAppData) => { + rawAppData.iconUrl = rawAppData.iconUrl.replace( + '{BASE_URL}', + appConfig.baseUrl + ); + + rawAppData.authDocUrl = rawAppData.authDocUrl.replace( + '{DOCS_URL}', + appConfig.docsUrl + ); + + if (rawAppData.auth?.fields) { + rawAppData.auth.fields = rawAppData.auth.fields.map((field) => { + if (field.type === 'string' && typeof field.value === 'string') { + return { + ...field, + value: field.value.replace('{WEB_APP_URL}', appConfig.webAppUrl), + }; + } + + return field; + }); + } + + return rawAppData; +}; + +export default appInfoConverter; diff --git a/packages/backend/src/helpers/authentication.js b/packages/backend/src/helpers/authentication.js new file mode 100644 index 0000000000000000000000000000000000000000..d60ac9865318c846a788ac96bcb86fae4ccfcec9 --- /dev/null +++ b/packages/backend/src/helpers/authentication.js @@ -0,0 +1,69 @@ +import { allow, rule, shield } from 'graphql-shield'; +import User from '../models/user.js'; +import AccessToken from '../models/access-token.js'; + +export const isAuthenticated = async (_parent, _args, req) => { + const token = req.headers['authorization']; + + if (token == null) return false; + + try { + const accessToken = await AccessToken.query().findOne({ + token, + revoked_at: null, + }); + + const expirationTime = + new Date(accessToken.createdAt).getTime() + accessToken.expiresIn * 1000; + + if (Date.now() > expirationTime) { + return false; + } + + const user = await accessToken.$relatedQuery('user'); + + req.currentUser = await User.query() + .findById(user.id) + .leftJoinRelated({ + role: true, + permissions: true, + }) + .withGraphFetched({ + role: true, + permissions: true, + }) + .throwIfNotFound(); + + return true; + } catch (error) { + return false; + } +}; + +export const authenticateUser = async (request, response, next) => { + if (await isAuthenticated(null, null, request)) { + next(); + } else { + return response.status(401).end(); + } +}; + +const isAuthenticatedRule = rule()(isAuthenticated); + +export const authenticationRules = { + Mutation: { + '*': isAuthenticatedRule, + forgotPassword: allow, + login: allow, + registerUser: allow, + resetPassword: allow, + }, +}; + +const authenticationOptions = { + allowExternalErrors: true, +}; + +const authentication = shield(authenticationRules, authenticationOptions); + +export default authentication; diff --git a/packages/backend/src/helpers/authentication.test.js b/packages/backend/src/helpers/authentication.test.js new file mode 100644 index 0000000000000000000000000000000000000000..a5a4b60f5ed16c449d500993181824258c8f942e --- /dev/null +++ b/packages/backend/src/helpers/authentication.test.js @@ -0,0 +1,74 @@ +import { describe, it, expect } from 'vitest'; +import { allow } from 'graphql-shield'; +import { isAuthenticated, authenticationRules } from './authentication.js'; +import { createUser } from '../../test/factories/user.js'; +import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js'; + +describe('isAuthenticated', () => { + it('should return false if no token is provided', async () => { + const req = { headers: {} }; + expect(await isAuthenticated(null, null, req)).toBe(false); + }); + + it('should return false if token is invalid', async () => { + const req = { headers: { authorization: 'invalidToken' } }; + expect(await isAuthenticated(null, null, req)).toBe(false); + }); + + it('should return true if token is valid and there is a user', async () => { + const user = await createUser(); + const token = await createAuthTokenByUserId(user.id); + + const req = { headers: { authorization: token } }; + expect(await isAuthenticated(null, null, req)).toBe(true); + }); + + it('should return false if token is valid and but there is no user', async () => { + const user = await createUser(); + const token = await createAuthTokenByUserId(user.id); + await user.$query().delete(); + + const req = { headers: { authorization: token } }; + expect(await isAuthenticated(null, null, req)).toBe(false); + }); +}); + +describe('authentication rules', () => { + const getQueryAndMutationNames = (rules) => { + const queries = Object.keys(rules.Query || {}); + const mutations = Object.keys(rules.Mutation || {}); + return { queries, mutations }; + }; + + const { queries, mutations } = getQueryAndMutationNames(authenticationRules); + + if (queries.length) { + describe('for queries', () => { + queries.forEach((query) => { + it(`should apply correct rule for query: ${query}`, () => { + const ruleApplied = authenticationRules.Query[query]; + + if (query === '*') { + expect(ruleApplied.func).toBe(isAuthenticated); + } else { + expect(ruleApplied).toEqual(allow); + } + }); + }); + }); + } + + describe('for mutations', () => { + mutations.forEach((mutation) => { + it(`should apply correct rule for mutation: ${mutation}`, () => { + const ruleApplied = authenticationRules.Mutation[mutation]; + + if (mutation === '*') { + expect(ruleApplied.func).toBe(isAuthenticated); + } else { + expect(ruleApplied).toBe(allow); + } + }); + }); + }); +}); diff --git a/packages/backend/src/helpers/authorization.js b/packages/backend/src/helpers/authorization.js new file mode 100644 index 0000000000000000000000000000000000000000..5a3e8a25cce9ae4ef3de54783a2f245b2a07fb99 --- /dev/null +++ b/packages/backend/src/helpers/authorization.js @@ -0,0 +1,89 @@ +const authorizationList = { + 'GET /api/v1/users/:userId': { + action: 'read', + subject: 'User', + }, + 'GET /api/v1/users/': { + action: 'read', + subject: 'User', + }, + 'GET /api/v1/users/:userId/apps': { + action: 'read', + subject: 'Connection', + }, + 'GET /api/v1/flows/:flowId': { + action: 'read', + subject: 'Flow', + }, + 'GET /api/v1/flows/': { + action: 'read', + subject: 'Flow', + }, + 'GET /api/v1/steps/:stepId/connection': { + action: 'read', + subject: 'Flow', + }, + 'GET /api/v1/steps/:stepId/previous-steps': { + action: 'update', + subject: 'Flow', + }, + 'POST /api/v1/steps/:stepId/dynamic-fields': { + action: 'update', + subject: 'Flow', + }, + 'POST /api/v1/steps/:stepId/dynamic-data': { + action: 'update', + subject: 'Flow', + }, + 'GET /api/v1/connections/:connectionId/flows': { + action: 'read', + subject: 'Flow', + }, + 'POST /api/v1/connections/:connectionId/test': { + action: 'update', + subject: 'Connection', + }, + 'GET /api/v1/apps/:appKey/flows': { + action: 'read', + subject: 'Flow', + }, + 'GET /api/v1/apps/:appKey/connections': { + action: 'read', + subject: 'Connection', + }, + 'GET /api/v1/executions/:executionId': { + action: 'read', + subject: 'Execution', + }, + 'GET /api/v1/executions/': { + action: 'read', + subject: 'Execution', + }, + 'GET /api/v1/executions/:executionId/execution-steps': { + action: 'read', + subject: 'Execution', + }, +}; + +export const authorizeUser = async (request, response, next) => { + const currentRoute = + request.method + ' ' + request.baseUrl + request.route.path; + const currentRouteRule = authorizationList[currentRoute]; + + try { + request.currentUser.can(currentRouteRule.action, currentRouteRule.subject); + next(); + } catch (error) { + return response.status(403).end(); + } +}; + +export const authorizeAdmin = async (request, response, next) => { + const role = await request.currentUser.$relatedQuery('role'); + + if (role?.isAdmin) { + next(); + } else { + return response.status(403).end(); + } +}; diff --git a/packages/backend/src/helpers/axios-with-proxy.js b/packages/backend/src/helpers/axios-with-proxy.js new file mode 100644 index 0000000000000000000000000000000000000000..0dbad80a47c0619f6b8199832f33c0d649812d0b --- /dev/null +++ b/packages/backend/src/helpers/axios-with-proxy.js @@ -0,0 +1,43 @@ +import axios from 'axios'; +import { HttpsProxyAgent } from 'https-proxy-agent'; +import { HttpProxyAgent } from 'http-proxy-agent'; + +const config = axios.defaults; +const httpProxyUrl = process.env.http_proxy; +const httpsProxyUrl = process.env.https_proxy; +const supportsProxy = httpProxyUrl || httpsProxyUrl; +const noProxyEnv = process.env.no_proxy; +const noProxyHosts = noProxyEnv ? noProxyEnv.split(',').map(host => host.trim()) : []; + +if (supportsProxy) { + if (httpProxyUrl) { + config.httpAgent = new HttpProxyAgent(httpProxyUrl); + } + + if (httpsProxyUrl) { + config.httpsAgent = new HttpsProxyAgent(httpsProxyUrl); + } + + config.proxy = false; +} + +const axiosWithProxyInstance = axios.create(config); + +function shouldSkipProxy(hostname) { + return noProxyHosts.some(noProxyHost => { + return hostname.endsWith(noProxyHost) || hostname === noProxyHost; + }); +}; + +axiosWithProxyInstance.interceptors.request.use(function skipProxyIfInNoProxy(requestConfig) { + const hostname = new URL(requestConfig.url).hostname; + + if (supportsProxy && shouldSkipProxy(hostname)) { + requestConfig.httpAgent = undefined; + requestConfig.httpsAgent = undefined; + } + + return requestConfig; +}); + +export default axiosWithProxyInstance; diff --git a/packages/backend/src/helpers/billing/index.ee.js b/packages/backend/src/helpers/billing/index.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..091598afb94f1328a1fa095f12f63056eafdc011 --- /dev/null +++ b/packages/backend/src/helpers/billing/index.ee.js @@ -0,0 +1,18 @@ +import appConfig from '../../config/app.js'; +import paddleClient from './paddle.ee.js'; +import paddlePlans from './plans.ee.js'; +import webhooks from './webhooks.ee.js'; + +const paddleInfo = { + sandbox: appConfig.isProd ? false : true, + vendorId: appConfig.paddleVendorId, +}; + +const billing = { + paddleClient, + paddlePlans, + paddleInfo, + webhooks, +}; + +export default billing; diff --git a/packages/backend/src/helpers/billing/paddle.ee.js b/packages/backend/src/helpers/billing/paddle.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..e4bed3fa4aa047b8e00e7dc2b9c56e8d3fa3c841 --- /dev/null +++ b/packages/backend/src/helpers/billing/paddle.ee.js @@ -0,0 +1,53 @@ +// TODO: replace with axios-with-proxy when needed +import axios from 'axios'; +import appConfig from '../../config/app.js'; +import { DateTime } from 'luxon'; + +const PADDLE_VENDOR_URL = appConfig.isDev + ? 'https://sandbox-vendors.paddle.com' + : 'https://vendors.paddle.com'; + +const axiosInstance = axios.create({ baseURL: PADDLE_VENDOR_URL }); + +const getSubscription = async (subscriptionId) => { + const data = { + vendor_id: appConfig.paddleVendorId, + vendor_auth_code: appConfig.paddleVendorAuthCode, + subscription_id: subscriptionId, + }; + + const response = await axiosInstance.post( + '/api/2.0/subscription/users', + data + ); + const subscription = response.data.response[0]; + return subscription; +}; + +const getInvoices = async (subscriptionId) => { + // TODO: iterate over previous subscriptions and include their invoices + const data = { + vendor_id: appConfig.paddleVendorId, + vendor_auth_code: appConfig.paddleVendorAuthCode, + subscription_id: subscriptionId, + is_paid: 1, + from: DateTime.now().minus({ years: 3 }).toISODate(), + to: DateTime.now().plus({ days: 3 }).toISODate(), + }; + + const response = await axiosInstance.post( + '/api/2.0/subscription/payments', + data + ); + + const invoices = response.data.response; + + return invoices; +}; + +const paddleClient = { + getSubscription, + getInvoices, +}; + +export default paddleClient; diff --git a/packages/backend/src/helpers/billing/plans.ee.js b/packages/backend/src/helpers/billing/plans.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..41046984844d07f601a6bb8de066dc1209c94d05 --- /dev/null +++ b/packages/backend/src/helpers/billing/plans.ee.js @@ -0,0 +1,29 @@ +import appConfig from '../../config/app.js'; + +const testPlans = [ + { + name: '10k - monthly', + limit: '10,000', + quota: 10000, + price: '€20', + productId: '47384', + }, +]; + +const prodPlans = [ + { + name: '10k - monthly', + limit: '10,000', + quota: 10000, + price: '€20', + productId: '826658', + }, +]; + +const plans = appConfig.isProd ? prodPlans : testPlans; + +export function getPlanById(id) { + return plans.find((plan) => plan.productId === id); +} + +export default plans; diff --git a/packages/backend/src/helpers/billing/webhooks.ee.js b/packages/backend/src/helpers/billing/webhooks.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..19cc194c22e1da43dee2eb77851a3ddb372aff50 --- /dev/null +++ b/packages/backend/src/helpers/billing/webhooks.ee.js @@ -0,0 +1,80 @@ +import Subscription from '../../models/subscription.ee.js'; +import Billing from './index.ee.js'; + +const handleSubscriptionCreated = async (request) => { + const subscription = await Subscription.query().insertAndFetch( + formatSubscription(request) + ); + await subscription + .$relatedQuery('usageData') + .insert(formatUsageData(request)); +}; + +const handleSubscriptionUpdated = async (request) => { + await Subscription.query() + .findOne({ + paddle_subscription_id: request.body.subscription_id, + }) + .patch(formatSubscription(request)); +}; + +const handleSubscriptionCancelled = async (request) => { + const subscription = await Subscription.query().findOne({ + paddle_subscription_id: request.body.subscription_id, + }); + + await subscription.$query().patchAndFetch(formatSubscription(request)); +}; + +const handleSubscriptionPaymentSucceeded = async (request) => { + const subscription = await Subscription.query() + .findOne({ + paddle_subscription_id: request.body.subscription_id, + }) + .throwIfNotFound(); + + const remoteSubscription = await Billing.paddleClient.getSubscription( + Number(subscription.paddleSubscriptionId) + ); + + await subscription.$query().patch({ + nextBillAmount: remoteSubscription.next_payment.amount.toFixed(2), + nextBillDate: remoteSubscription.next_payment.date, + lastBillDate: remoteSubscription.last_payment.date, + }); + + await subscription + .$relatedQuery('usageData') + .insert(formatUsageData(request)); +}; + +const formatSubscription = (request) => { + return { + userId: JSON.parse(request.body.passthrough).id, + paddleSubscriptionId: request.body.subscription_id, + paddlePlanId: request.body.subscription_plan_id, + cancelUrl: request.body.cancel_url, + updateUrl: request.body.update_url, + status: request.body.status, + nextBillDate: request.body.next_bill_date, + nextBillAmount: request.body.unit_price, + cancellationEffectiveDate: request.body.cancellation_effective_date, + }; +}; + +const formatUsageData = (request) => { + return { + userId: JSON.parse(request.body.passthrough).id, + consumedTaskCount: 0, + nextResetAt: request.body.next_bill_date, + }; +}; + +const webhooks = { + handleSubscriptionCreated, + handleSubscriptionUpdated, + handleSubscriptionCancelled, + handleSubscriptionPaymentSucceeded, +}; + +export default webhooks; diff --git a/packages/backend/src/helpers/check-is-cloud.js b/packages/backend/src/helpers/check-is-cloud.js new file mode 100644 index 0000000000000000000000000000000000000000..f0b93b56180e3df1bda35abec63605683a3ad029 --- /dev/null +++ b/packages/backend/src/helpers/check-is-cloud.js @@ -0,0 +1,11 @@ +import appConfig from '../config/app.js'; + +export const checkIsCloud = async (request, response, next) => { + if (appConfig.isCloud) { + next(); + } else { + return response.status(404).end(); + } +}; + +export default checkIsCloud; diff --git a/packages/backend/src/helpers/check-is-enterprise.js b/packages/backend/src/helpers/check-is-enterprise.js new file mode 100644 index 0000000000000000000000000000000000000000..0180eea297a671e0ffbb0b2c851e03c350f38179 --- /dev/null +++ b/packages/backend/src/helpers/check-is-enterprise.js @@ -0,0 +1,9 @@ +import { hasValidLicense } from './license.ee.js'; + +export const checkIsEnterprise = async (request, response, next) => { + if (await hasValidLicense()) { + next(); + } else { + return response.status(404).end(); + } +}; diff --git a/packages/backend/src/helpers/check-worker-readiness.js b/packages/backend/src/helpers/check-worker-readiness.js new file mode 100644 index 0000000000000000000000000000000000000000..e1ae0a7951142528d1ce08651a519f085f0977a0 --- /dev/null +++ b/packages/backend/src/helpers/check-worker-readiness.js @@ -0,0 +1,11 @@ +import Redis from 'ioredis'; +import logger from './logger.js'; +import redisConfig from '../config/redis.js'; + +const redisClient = new Redis(redisConfig); + +redisClient.on('ready', () => { + logger.info(`Workers are ready!`); + + redisClient.disconnect(); +}); diff --git a/packages/backend/src/helpers/compile-email.ee.js b/packages/backend/src/helpers/compile-email.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..ae866a361b770795c195387ffa4c7c2122ab1036 --- /dev/null +++ b/packages/backend/src/helpers/compile-email.ee.js @@ -0,0 +1,15 @@ +import path from 'path'; +import fs from 'fs'; +import handlebars from 'handlebars'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const compileEmail = (emailPath, replacements = {}) => { + const filePath = path.join(__dirname, `../views/emails/${emailPath}.ee.hbs`); + const source = fs.readFileSync(filePath, 'utf-8').toString(); + const template = handlebars.compile(source); + return template(replacements); +}; + +export default compileEmail; diff --git a/packages/backend/src/helpers/compute-parameters.js b/packages/backend/src/helpers/compute-parameters.js new file mode 100644 index 0000000000000000000000000000000000000000..d29252b687c0b7c5f8cec9478d7d8ff9192345df --- /dev/null +++ b/packages/backend/src/helpers/compute-parameters.js @@ -0,0 +1,48 @@ +import get from 'lodash.get'; + +const variableRegExp = /({{step\.[\da-zA-Z-]+(?:\.[^.}{]+)+}})/g; + +export default function computeParameters(parameters, executionSteps) { + const entries = Object.entries(parameters); + return entries.reduce((result, [key, value]) => { + if (typeof value === 'string') { + const parts = value.split(variableRegExp); + + const computedValue = parts + .map((part) => { + const isVariable = part.match(variableRegExp); + if (isVariable) { + const stepIdAndKeyPath = part.replace(/{{step.|}}/g, ''); + const [stepId, ...keyPaths] = stepIdAndKeyPath.split('.'); + const keyPath = keyPaths.join('.'); + const executionStep = executionSteps.find((executionStep) => { + return executionStep.stepId === stepId; + }); + const data = executionStep?.dataOut; + const dataValue = get(data, keyPath); + return dataValue; + } + + return part; + }) + .join(''); + + return { + ...result, + [key]: computedValue, + }; + } + + if (Array.isArray(value)) { + return { + ...result, + [key]: value.map((item) => computeParameters(item, executionSteps)), + }; + } + + return { + ...result, + [key]: value, + }; + }, {}); +} diff --git a/packages/backend/src/helpers/create-auth-token-by-user-id.js b/packages/backend/src/helpers/create-auth-token-by-user-id.js new file mode 100644 index 0000000000000000000000000000000000000000..2b82440f1dd3bbfddce515406c5a12950656020b --- /dev/null +++ b/packages/backend/src/helpers/create-auth-token-by-user-id.js @@ -0,0 +1,21 @@ +import crypto from 'crypto'; +import User from '../models/user.js'; +import AccessToken from '../models/access-token.js'; + +const TOKEN_EXPIRES_IN = 14 * 24 * 60 * 60; // 14 days in seconds + +const createAuthTokenByUserId = async (userId, samlSessionId) => { + const user = await User.query().findById(userId).throwIfNotFound(); + const token = await crypto.randomBytes(48).toString('hex'); + + await AccessToken.query().insert({ + token, + samlSessionId, + userId: user.id, + expiresIn: TOKEN_EXPIRES_IN, + }); + + return token; +}; + +export default createAuthTokenByUserId; diff --git a/packages/backend/src/helpers/create-bull-board-handler.js b/packages/backend/src/helpers/create-bull-board-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..03f949b6d672e2d46dc734b98052b150fb274253 --- /dev/null +++ b/packages/backend/src/helpers/create-bull-board-handler.js @@ -0,0 +1,43 @@ +import { ExpressAdapter } from '@bull-board/express'; +import { createBullBoard } from '@bull-board/api'; +import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js'; +import flowQueue from '../queues/flow.js'; +import triggerQueue from '../queues/trigger.js'; +import actionQueue from '../queues/action.js'; +import emailQueue from '../queues/email.js'; +import deleteUserQueue from '../queues/delete-user.ee.js'; +import removeCancelledSubscriptionsQueue from '../queues/remove-cancelled-subscriptions.ee.js'; +import appConfig from '../config/app.js'; + +const serverAdapter = new ExpressAdapter(); + +const queues = [ + new BullMQAdapter(flowQueue), + new BullMQAdapter(triggerQueue), + new BullMQAdapter(actionQueue), + new BullMQAdapter(emailQueue), + new BullMQAdapter(deleteUserQueue), +]; + +if (appConfig.isCloud) { + queues.push(new BullMQAdapter(removeCancelledSubscriptionsQueue)); +} + +const shouldEnableBullDashboard = () => { + return ( + appConfig.enableBullMQDashboard && + appConfig.bullMQDashboardUsername && + appConfig.bullMQDashboardPassword + ); +}; + +const createBullBoardHandler = async (serverAdapter) => { + if (!shouldEnableBullDashboard) return; + + createBullBoard({ + queues, + serverAdapter, + }); +}; + +export { createBullBoardHandler, serverAdapter }; diff --git a/packages/backend/src/helpers/define-action.js b/packages/backend/src/helpers/define-action.js new file mode 100644 index 0000000000000000000000000000000000000000..fc3b45ef209191f998fdda69f08a42ffa727dfef --- /dev/null +++ b/packages/backend/src/helpers/define-action.js @@ -0,0 +1,3 @@ +export default function defineAction(actionDefinition) { + return actionDefinition; +} diff --git a/packages/backend/src/helpers/define-app.js b/packages/backend/src/helpers/define-app.js new file mode 100644 index 0000000000000000000000000000000000000000..3630f474fe4b277c398e5d941d8b8f369d5c6d61 --- /dev/null +++ b/packages/backend/src/helpers/define-app.js @@ -0,0 +1,3 @@ +export default function defineApp(appDefinition) { + return appDefinition; +} diff --git a/packages/backend/src/helpers/define-trigger.js b/packages/backend/src/helpers/define-trigger.js new file mode 100644 index 0000000000000000000000000000000000000000..008bdf18b443e46cbcf5d7acc70ea2d4ad22e02d --- /dev/null +++ b/packages/backend/src/helpers/define-trigger.js @@ -0,0 +1,3 @@ +export default function defineTrigger(triggerDefinition) { + return triggerDefinition; +} diff --git a/packages/backend/src/helpers/delay-as-milliseconds.js b/packages/backend/src/helpers/delay-as-milliseconds.js new file mode 100644 index 0000000000000000000000000000000000000000..c657647fdf32c0e4e3cd1273b969f4ef1ec7ad40 --- /dev/null +++ b/packages/backend/src/helpers/delay-as-milliseconds.js @@ -0,0 +1,19 @@ +import delayForAsMilliseconds from './delay-for-as-milliseconds.js'; +import delayUntilAsMilliseconds from './delay-until-as-milliseconds.js'; + +const delayAsMilliseconds = (eventKey, computedParameters) => { + let delayDuration = 0; + + if (eventKey === 'delayFor') { + const { delayForUnit, delayForValue } = computedParameters; + + delayDuration = delayForAsMilliseconds(delayForUnit, Number(delayForValue)); + } else if (eventKey === 'delayUntil') { + const { delayUntil } = computedParameters; + delayDuration = delayUntilAsMilliseconds(delayUntil); + } + + return delayDuration; +}; + +export default delayAsMilliseconds; diff --git a/packages/backend/src/helpers/delay-for-as-milliseconds.js b/packages/backend/src/helpers/delay-for-as-milliseconds.js new file mode 100644 index 0000000000000000000000000000000000000000..e3ae5862f828544879c6a447b23af5a3bd47d6c9 --- /dev/null +++ b/packages/backend/src/helpers/delay-for-as-milliseconds.js @@ -0,0 +1,16 @@ +const delayAsMilliseconds = (delayForUnit, delayForValue) => { + switch (delayForUnit) { + case 'minutes': + return delayForValue * 60 * 1000; + case 'hours': + return delayForValue * 60 * 60 * 1000; + case 'days': + return delayForValue * 24 * 60 * 60 * 1000; + case 'weeks': + return delayForValue * 7 * 24 * 60 * 60 * 1000; + default: + return 0; + } +}; + +export default delayAsMilliseconds; diff --git a/packages/backend/src/helpers/delay-until-as-milliseconds.js b/packages/backend/src/helpers/delay-until-as-milliseconds.js new file mode 100644 index 0000000000000000000000000000000000000000..4508fa881ab4364f80e12eb254908f8f858ae601 --- /dev/null +++ b/packages/backend/src/helpers/delay-until-as-milliseconds.js @@ -0,0 +1,8 @@ +const delayUntilAsMilliseconds = (delayUntil) => { + const delayUntilDate = new Date(delayUntil); + const now = new Date(); + + return delayUntilDate.getTime() - now.getTime(); +}; + +export default delayUntilAsMilliseconds; diff --git a/packages/backend/src/helpers/error-handler.js b/packages/backend/src/helpers/error-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..14a120dc4450277f62162b1198d4e84589e3c12e --- /dev/null +++ b/packages/backend/src/helpers/error-handler.js @@ -0,0 +1,57 @@ +import logger from './logger.js'; +import objection from 'objection'; +import * as Sentry from './sentry.ee.js'; +const { NotFoundError, DataError } = objection; +import HttpError from '../errors/http.js'; + +// Do not remove `next` argument as the function signature will not fit for an error handler middleware +// eslint-disable-next-line no-unused-vars +const errorHandler = (error, request, response, next) => { + if (error.message === 'Not Found' || error instanceof NotFoundError) { + response.status(404).end(); + } + + if (notFoundAppError(error)) { + response.status(404).end(); + } + + if (error instanceof DataError) { + response.status(400).end(); + } + + if (error instanceof HttpError) { + const httpErrorPayload = { + errors: JSON.parse(error.message), + meta: { + type: 'HttpError', + }, + }; + + response.status(200).json(httpErrorPayload); + } + + const statusCode = error.statusCode || 500; + + logger.error(request.method + ' ' + request.url + ' ' + statusCode); + logger.error(error.stack); + + Sentry.captureException(error, { + tags: { rest: true }, + extra: { + url: request?.url, + method: request?.method, + params: request?.params, + }, + }); + + response.status(statusCode).end(); +}; + +const notFoundAppError = (error) => { + return ( + error.message.includes('An application with the') && + error.message.includes("key couldn't be found.") + ); +}; + +export default errorHandler; diff --git a/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js b/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..daeb1206203425c2329fa62166a8d78e7a502759 --- /dev/null +++ b/packages/backend/src/helpers/find-or-create-user-by-saml-identity.ee.js @@ -0,0 +1,64 @@ +import User from '../models/user.js'; +import Identity from '../models/identity.ee.js'; + +const getUser = (user, providerConfig) => ({ + name: user[providerConfig.firstnameAttributeName], + surname: user[providerConfig.surnameAttributeName], + id: user.nameID, + email: user[providerConfig.emailAttributeName], + role: user[providerConfig.roleAttributeName], +}); + +const findOrCreateUserBySamlIdentity = async ( + userIdentity, + samlAuthProvider +) => { + const mappedUser = getUser(userIdentity, samlAuthProvider); + const identity = await Identity.query().findOne({ + remote_id: mappedUser.id, + provider_type: 'saml', + }); + + if (identity) { + const user = await identity.$relatedQuery('user'); + + return user; + } + + const mappedRoles = Array.isArray(mappedUser.role) + ? mappedUser.role + : [mappedUser.role]; + + const samlAuthProviderRoleMapping = await samlAuthProvider + .$relatedQuery('samlAuthProvidersRoleMappings') + .whereIn('remote_role_name', mappedRoles) + .limit(1) + .first(); + + const createdUser = await User.query() + .insertGraph( + { + fullName: [mappedUser.name, mappedUser.surname] + .filter(Boolean) + .join(' '), + email: mappedUser.email, + roleId: + samlAuthProviderRoleMapping?.roleId || samlAuthProvider.defaultRoleId, + identities: [ + { + remoteId: mappedUser.id, + providerId: samlAuthProvider.id, + providerType: 'saml', + }, + ], + }, + { + relate: ['identities'], + } + ) + .returning('*'); + + return createdUser; +}; + +export default findOrCreateUserBySamlIdentity; diff --git a/packages/backend/src/helpers/get-app.js b/packages/backend/src/helpers/get-app.js new file mode 100644 index 0000000000000000000000000000000000000000..7d1869772aa5a658e7f031c2ce70f935d6e979b3 --- /dev/null +++ b/packages/backend/src/helpers/get-app.js @@ -0,0 +1,94 @@ +import path from 'node:path'; +import fs from 'node:fs'; +import omit from 'lodash/omit.js'; +import cloneDeep from 'lodash/cloneDeep.js'; +import addAuthenticationSteps from './add-authentication-steps.js'; +import addReconnectionSteps from './add-reconnection-steps.js'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const apps = fs + .readdirSync(path.resolve(__dirname, `../apps/`), { withFileTypes: true }) + .reduce((apps, dirent) => { + if (!dirent.isDirectory()) return apps; + + apps[dirent.name] = import( + path.resolve(__dirname, '../apps', dirent.name, 'index.js') + ); + + return apps; + }, {}); + +async function getAppDefaultExport(appKey) { + if (!Object.prototype.hasOwnProperty.call(apps, appKey)) { + throw new Error( + `An application with the "${appKey}" key couldn't be found.` + ); + } + + return (await apps[appKey]).default; +} + +function stripFunctions(data) { + return JSON.parse(JSON.stringify(data)); +} + +const getApp = async (appKey, stripFuncs = true) => { + let appData = cloneDeep(await getAppDefaultExport(appKey)); + + if (appData.auth) { + appData = addAuthenticationSteps(appData); + appData = addReconnectionSteps(appData); + } + + appData.triggers = appData?.triggers?.map((trigger) => { + return addStaticSubsteps('trigger', appData, trigger); + }); + + appData.actions = appData?.actions?.map((action) => { + return addStaticSubsteps('action', appData, action); + }); + + if (stripFuncs) { + return stripFunctions(appData); + } + + return appData; +}; + +const chooseConnectionStep = { + key: 'chooseConnection', + name: 'Choose connection', +}; + +const testStep = (stepType) => { + return { + key: 'testStep', + name: stepType === 'trigger' ? 'Test trigger' : 'Test action', + }; +}; + +const addStaticSubsteps = (stepType, appData, step) => { + const computedStep = omit(step, ['arguments']); + + computedStep.substeps = []; + + if (appData.supportsConnections) { + computedStep.substeps.push(chooseConnectionStep); + } + + if (step.arguments) { + computedStep.substeps.push({ + key: 'chooseTrigger', + name: stepType === 'trigger' ? 'Set up a trigger' : 'Set up action', + arguments: step.arguments, + }); + } + + computedStep.substeps.push(testStep(stepType)); + + return computedStep; +}; + +export default getApp; diff --git a/packages/backend/src/helpers/global-variable.js b/packages/backend/src/helpers/global-variable.js new file mode 100644 index 0000000000000000000000000000000000000000..5f7706f9cf02f36bee08f3d11e8c0d7b3b816a05 --- /dev/null +++ b/packages/backend/src/helpers/global-variable.js @@ -0,0 +1,175 @@ +import createHttpClient from './http-client/index.js'; +import EarlyExitError from '../errors/early-exit.js'; +import AlreadyProcessedError from '../errors/already-processed.js'; +import Datastore from '../models/datastore.js'; + +const globalVariable = async (options) => { + const { + connection, + app, + flow, + step, + execution, + request, + testRun = false, + } = options; + + const isTrigger = step?.isTrigger; + const lastInternalId = testRun ? undefined : await flow?.lastInternalId(); + const nextStep = await step?.getNextStep(); + + const $ = { + auth: { + set: async (args) => { + if (connection) { + await connection.$query().patchAndFetch({ + formattedData: { + ...connection.formattedData, + ...args, + }, + }); + + $.auth.data = connection.formattedData; + } + + return null; + }, + data: connection?.formattedData, + }, + app: app, + flow: { + id: flow?.id, + lastInternalId, + }, + step: { + id: step?.id, + appKey: step?.appKey, + parameters: step?.parameters || {}, + }, + nextStep: { + id: nextStep?.id, + appKey: nextStep?.appKey, + parameters: nextStep?.parameters || {}, + }, + execution: { + id: execution?.id, + testRun, + exit: () => { + throw new EarlyExitError(); + }, + }, + getLastExecutionStep: async () => + (await step?.getLastExecutionStep())?.toJSON(), + triggerOutput: { + data: [], + }, + actionOutput: { + data: { + raw: null, + }, + }, + pushTriggerItem: (triggerItem) => { + if ( + isAlreadyProcessed(triggerItem.meta.internalId) && + !$.execution.testRun + ) { + // early exit as we do not want to process duplicate items in actual executions + throw new AlreadyProcessedError(); + } + + $.triggerOutput.data.push(triggerItem); + + const isWebhookApp = app.key === 'webhook'; + + if ($.execution.testRun && !isWebhookApp) { + // early exit after receiving one item as it is enough for test execution + throw new EarlyExitError(); + } + }, + setActionItem: (actionItem) => { + $.actionOutput.data = actionItem; + }, + datastore: { + get: async ({ key }) => { + const datastore = await Datastore.query().findOne({ + key, + scope: 'flow', + scope_id: $.flow.id, + }); + + return { + key: datastore.key, + value: datastore.value, + [datastore.key]: datastore.value, + }; + }, + set: async ({ key, value }) => { + let datastore = await Datastore.query() + .where({ key, scope: 'flow', scope_id: $.flow.id }) + .first(); + + if (datastore) { + await datastore.$query().patchAndFetch({ value: value }); + } else { + datastore = await Datastore.query().insert({ + key, + value, + scope: 'flow', + scopeId: $.flow.id, + }); + } + + return { + key: datastore.key, + value: datastore.value, + [datastore.key]: datastore.value, + }; + }, + }, + }; + + if (request) { + $.request = request; + } + + if (app) { + $.http = createHttpClient({ + $, + baseURL: app.apiBaseUrl, + beforeRequest: app.beforeRequest, + }); + } + + if (step) { + $.webhookUrl = await step.getWebhookUrl(); + } + + if (isTrigger) { + const triggerCommand = await step.getTriggerCommand(); + + if (triggerCommand.type === 'webhook') { + $.flow.setRemoteWebhookId = async (remoteWebhookId) => { + await flow.$query().patchAndFetch({ + remoteWebhookId, + }); + + $.flow.remoteWebhookId = remoteWebhookId; + }; + + $.flow.remoteWebhookId = flow.remoteWebhookId; + } + } + + const lastInternalIds = + testRun || (flow && step?.isAction) + ? [] + : await flow?.lastInternalIds(2000); + + const isAlreadyProcessed = (internalId) => { + return lastInternalIds?.includes(internalId); + }; + + return $; +}; + +export default globalVariable; diff --git a/packages/backend/src/helpers/graphql-instance.js b/packages/backend/src/helpers/graphql-instance.js new file mode 100644 index 0000000000000000000000000000000000000000..a8a50db79b8d649231074225e0bc15e82cea8a79 --- /dev/null +++ b/packages/backend/src/helpers/graphql-instance.js @@ -0,0 +1,53 @@ +import path, { join } from 'path'; +import { fileURLToPath } from 'url'; +import { graphqlHTTP } from 'express-graphql'; +import { loadSchemaSync } from '@graphql-tools/load'; +import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; +import { addResolversToSchema } from '@graphql-tools/schema'; +import { applyMiddleware } from 'graphql-middleware'; + +import appConfig from '../config/app.js'; +import logger from './logger.js'; +import authentication from './authentication.js'; +import * as Sentry from './sentry.ee.js'; +import resolvers from '../graphql/resolvers.js'; +import HttpError from '../errors/http.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const schema = loadSchemaSync(join(__dirname, '../graphql/schema.graphql'), { + loaders: [new GraphQLFileLoader()], +}); + +const schemaWithResolvers = addResolversToSchema({ + schema, + resolvers, +}); + +const graphQLInstance = graphqlHTTP({ + schema: applyMiddleware( + schemaWithResolvers, + authentication.generate(schemaWithResolvers) + ), + graphiql: appConfig.isDev, + customFormatErrorFn: (error) => { + logger.error(error.path + ' : ' + error.message + '\n' + error.stack); + + if (error.originalError instanceof HttpError) { + delete error.originalError.response; + } + + Sentry.captureException(error, { + tags: { graphql: true }, + extra: { + source: error.source?.body, + positions: error.positions, + path: error.path, + }, + }); + + return error; + }, +}); + +export default graphQLInstance; diff --git a/packages/backend/src/helpers/http-client/index.js b/packages/backend/src/helpers/http-client/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e32482df42cb63bc076ad20afe11b2654811c2da --- /dev/null +++ b/packages/backend/src/helpers/http-client/index.js @@ -0,0 +1,68 @@ +import { URL } from 'node:url'; +import HttpError from '../../errors/http.js'; +import axios from '../axios-with-proxy.js'; + +const removeBaseUrlForAbsoluteUrls = (requestConfig) => { + try { + const url = new URL(requestConfig.url); + requestConfig.baseURL = url.origin; + requestConfig.url = url.pathname + url.search; + + return requestConfig; + } catch { + return requestConfig; + } +}; + +export default function createHttpClient({ $, baseURL, beforeRequest = [] }) { + const instance = axios.create({ + baseURL, + }); + + instance.interceptors.request.use((requestConfig) => { + const newRequestConfig = removeBaseUrlForAbsoluteUrls(requestConfig); + + const result = beforeRequest.reduce((newConfig, beforeRequestFunc) => { + return beforeRequestFunc($, newConfig); + }, newRequestConfig); + + /** + * axios seems to want InternalAxiosRequestConfig returned not AxioRequestConfig + * anymore even though requests do require AxiosRequestConfig. + * + * Since both interfaces are very similar (InternalAxiosRequestConfig + * extends AxiosRequestConfig), we can utilize an assertion below + **/ + return result; + }); + + instance.interceptors.response.use( + (response) => response, + async (error) => { + const { config, response } = error; + // Do not destructure `status` from `error.response` because it might not exist + const status = response?.status; + + if ( + // TODO: provide a `shouldRefreshToken` function in the app + (status === 401 || status === 403) && + $.app.auth && + $.app.auth.refreshToken && + !$.app.auth.isRefreshTokenRequested + ) { + $.app.auth.isRefreshTokenRequested = true; + await $.app.auth.refreshToken($); + + // retry the previous request before the expired token error + const newResponse = await instance.request(config); + $.app.auth.isRefreshTokenRequested = false; + + return newResponse; + } + + throw new HttpError(error); + } + ); + + return instance; +} diff --git a/packages/backend/src/helpers/inject-bull-board-handler.js b/packages/backend/src/helpers/inject-bull-board-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..17266c07a10e923158e0687072d89452b266cc4c --- /dev/null +++ b/packages/backend/src/helpers/inject-bull-board-handler.js @@ -0,0 +1,27 @@ +import basicAuth from 'express-basic-auth'; +import appConfig from '../config/app.js'; + +const injectBullBoardHandler = async (app, serverAdapter) => { + if ( + !appConfig.enableBullMQDashboard || + !appConfig.bullMQDashboardUsername || + !appConfig.bullMQDashboardPassword + ) + return; + + const queueDashboardBasePath = '/admin/queues'; + serverAdapter.setBasePath(queueDashboardBasePath); + + app.use( + queueDashboardBasePath, + basicAuth({ + users: { + [appConfig.bullMQDashboardUsername]: appConfig.bullMQDashboardPassword, + }, + challenge: true, + }), + serverAdapter.getRouter() + ); +}; + +export default injectBullBoardHandler; diff --git a/packages/backend/src/helpers/license.ee.js b/packages/backend/src/helpers/license.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..d3ae30c065519a30dc58e05b19ec5ff9889e008b --- /dev/null +++ b/packages/backend/src/helpers/license.ee.js @@ -0,0 +1,37 @@ +import memoryCache from 'memory-cache'; +import appConfig from '../config/app.js'; +import axios from './axios-with-proxy.js'; + +const CACHE_DURATION = 1000 * 60 * 60 * 24; // 24 hours in milliseconds + +const hasValidLicense = async () => { + const license = await getLicense(); + + return license ? true : false; +}; + +const getLicense = async () => { + const licenseKey = appConfig.licenseKey; + + if (!licenseKey) { + return false; + } + + const url = 'https://license.automatisch.io/api/v1/licenses/verify'; + const cachedResponse = memoryCache.get(url); + + if (cachedResponse) { + return cachedResponse; + } else { + try { + const { data } = await axios.post(url, { licenseKey }); + memoryCache.put(url, data, CACHE_DURATION); + + return data; + } catch (error) { + return false; + } + } +}; + +export { getLicense, hasValidLicense }; diff --git a/packages/backend/src/helpers/logger.js b/packages/backend/src/helpers/logger.js new file mode 100644 index 0000000000000000000000000000000000000000..d202869cb032ea9d96ccf3e044b3ef1fe3832987 --- /dev/null +++ b/packages/backend/src/helpers/logger.js @@ -0,0 +1,46 @@ +import * as winston from 'winston'; +import appConfig from '../config/app.js'; + +const levels = { + error: 0, + warn: 1, + http: 2, + info: 3, + debug: 4, +}; + +const colors = { + error: 'red', + warn: 'yellow', + info: 'green', + http: 'magenta', + debug: 'white', +}; + +winston.addColors(colors); + +const format = winston.format.combine( + winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }), + winston.format.colorize({ all: true }), + winston.format.printf( + (info) => `${info.timestamp} [${info.level}]: ${info.message}` + ) +); + +const transports = [ + new winston.transports.Console(), + new winston.transports.File({ + filename: 'logs/error.log', + level: 'error', + }), + new winston.transports.File({ filename: 'logs/server.log' }), +]; + +export const logger = winston.createLogger({ + level: appConfig.logLevel, + levels, + format, + transports, +}); + +export default logger; diff --git a/packages/backend/src/helpers/mailer.ee.js b/packages/backend/src/helpers/mailer.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2a574889f258eaa67bd7c4156e95f02593263c6b --- /dev/null +++ b/packages/backend/src/helpers/mailer.ee.js @@ -0,0 +1,14 @@ +import nodemailer from 'nodemailer'; +import appConfig from '../config/app.js'; + +const mailer = nodemailer.createTransport({ + host: appConfig.smtpHost, + port: appConfig.smtpPort, + secure: appConfig.smtpSecure, + auth: { + user: appConfig.smtpUser, + pass: appConfig.smtpPassword, + }, +}); + +export default mailer; diff --git a/packages/backend/src/helpers/morgan.js b/packages/backend/src/helpers/morgan.js new file mode 100644 index 0000000000000000000000000000000000000000..63d785386c99436884dd8a9ef6856135368c1916 --- /dev/null +++ b/packages/backend/src/helpers/morgan.js @@ -0,0 +1,24 @@ +import morgan from 'morgan'; +import logger from './logger.js'; + +const stream = { + write: (message) => + logger.http(message.substring(0, message.lastIndexOf('\n'))), +}; + +const registerGraphQLToken = () => { + morgan.token('graphql-query', (req) => { + if (req.body.query) { + return `\n GraphQL ${req.body.query}`; + } + }); +}; + +registerGraphQLToken(); + +const morganMiddleware = morgan( + ':method :url :status :res[content-length] - :response-time ms :graphql-query', + { stream } +); + +export default morganMiddleware; diff --git a/packages/backend/src/helpers/pagination-rest.js b/packages/backend/src/helpers/pagination-rest.js new file mode 100644 index 0000000000000000000000000000000000000000..89239d859a67dae9b607757c5028692df7160cfd --- /dev/null +++ b/packages/backend/src/helpers/pagination-rest.js @@ -0,0 +1,25 @@ +const paginateRest = async (query, page) => { + const pageSize = 10; + + page = parseInt(page, 10); + + if (isNaN(page) || page < 1) { + page = 1; + } + + const [records, count] = await Promise.all([ + query.limit(pageSize).offset((page - 1) * pageSize), + query.resultSize(), + ]); + + return { + pageInfo: { + currentPage: page, + totalPages: Math.ceil(count / pageSize), + }, + totalCount: count, + records, + }; +}; + +export default paginateRest; diff --git a/packages/backend/src/helpers/pagination.js b/packages/backend/src/helpers/pagination.js new file mode 100644 index 0000000000000000000000000000000000000000..419df03f1701c5ee4be8a211e2e7d804692ec484 --- /dev/null +++ b/packages/backend/src/helpers/pagination.js @@ -0,0 +1,23 @@ +const paginate = async (query, limit, offset) => { + if (limit < 1 || limit > 100) { + throw new Error('Limit must be between 1 and 100'); + } + + const [records, count] = await Promise.all([ + query.limit(limit).offset(offset), + query.resultSize(), + ]); + + return { + pageInfo: { + currentPage: Math.ceil(offset / limit + 1), + totalPages: Math.ceil(count / limit), + }, + totalCount: count, + edges: records.map((record) => ({ + node: record, + })), + }; +}; + +export default paginate; diff --git a/packages/backend/src/helpers/parse-header-link.js b/packages/backend/src/helpers/parse-header-link.js new file mode 100644 index 0000000000000000000000000000000000000000..face70f8806a04fe622909e992ed6f3a56073a3e --- /dev/null +++ b/packages/backend/src/helpers/parse-header-link.js @@ -0,0 +1,29 @@ +export default function parseLinkHeader(link) { + const parsed = {}; + + if (!link) return parsed; + + const items = link.split(','); + + for (const item of items) { + const [rawUriReference, ...rawLinkParameters] = item.split(';'); + const trimmedUriReference = rawUriReference.trim(); + + const reference = trimmedUriReference.slice(1, -1); + const parameters = {}; + + for (const rawParameter of rawLinkParameters) { + const trimmedRawParameter = rawParameter.trim(); + const [key, value] = trimmedRawParameter.split('='); + + parameters[key.trim()] = value.slice(1, -1); + } + + parsed[parameters.rel] = { + uri: reference, + parameters, + }; + } + + return parsed; +} diff --git a/packages/backend/src/helpers/passport.js b/packages/backend/src/helpers/passport.js new file mode 100644 index 0000000000000000000000000000000000000000..9c7ebaa331a22e0d05c86f90d7fe487840cf5e31 --- /dev/null +++ b/packages/backend/src/helpers/passport.js @@ -0,0 +1,129 @@ +import { URL } from 'node:url'; +import { MultiSamlStrategy } from '@node-saml/passport-saml'; +import passport from 'passport'; + +import appConfig from '../config/app.js'; +import createAuthTokenByUserId from './create-auth-token-by-user-id.js'; +import SamlAuthProvider from '../models/saml-auth-provider.ee.js'; +import AccessToken from '../models/access-token.js'; +import findOrCreateUserBySamlIdentity from './find-or-create-user-by-saml-identity.ee.js'; + +const asyncNoop = async () => { }; + +export default function configurePassport(app) { + app.use( + passport.initialize({ + userProperty: 'currentUser', + }) + ); + + passport.use( + new MultiSamlStrategy( + { + passReqToCallback: true, + getSamlOptions: async function (request, done) { + // This is a workaround to avoid session logout which passport-saml enforces + request.logout = asyncNoop; + request.logOut = asyncNoop; + + const { issuer } = request.params; + const notFoundIssuer = new Error('Issuer cannot be found!'); + + if (!issuer) return done(notFoundIssuer); + + const authProvider = await SamlAuthProvider.query().findOne({ + issuer: request.params.issuer, + }); + + if (!authProvider) { + return done(notFoundIssuer); + } + + return done(null, authProvider.config); + }, + }, + async function signonVerify(request, user, done) { + const { issuer } = request.params; + const notFoundIssuer = new Error('Issuer cannot be found!'); + + if (!issuer) return done(notFoundIssuer); + + const authProvider = await SamlAuthProvider.query().findOne({ + issuer: request.params.issuer, + }); + + if (!authProvider) { + return done(notFoundIssuer); + } + + const foundUserWithIdentity = await findOrCreateUserBySamlIdentity( + user, + authProvider + ); + + request.samlSessionId = user.sessionIndex; + + return done(null, foundUserWithIdentity); + }, + async function logoutVerify(request, user, done) { + const { issuer } = request.params; + const notFoundIssuer = new Error('Issuer cannot be found!'); + + if (!issuer) return done(notFoundIssuer); + + const authProvider = await SamlAuthProvider.query().findOne({ + issuer: request.params.issuer, + }); + + if (!authProvider) { + return done(notFoundIssuer); + } + + const foundUserWithIdentity = await findOrCreateUserBySamlIdentity( + user, + authProvider + ); + + const accessToken = await AccessToken.query().findOne({ + revoked_at: null, + saml_session_id: user.sessionIndex, + }).throwIfNotFound(); + + await accessToken.revoke(); + + return done(null, foundUserWithIdentity); + } + ) + ); + + app.get( + '/login/saml/:issuer', + passport.authenticate('saml', { + session: false, + successRedirect: '/', + }) + ); + + app.post( + '/login/saml/:issuer/callback', + passport.authenticate('saml', { + session: false, + }), + async (request, response) => { + const token = await createAuthTokenByUserId(request.currentUser.id, request.samlSessionId); + + const redirectUrl = new URL( + `/login/callback?token=${token}`, + appConfig.webAppUrl + ).toString(); + response.redirect(redirectUrl); + } + ); + + app.post( + '/logout/saml/:issuer', + passport.authenticate('saml', { + session: false, + }), + ); +} diff --git a/packages/backend/src/helpers/permission-catalog.ee.js b/packages/backend/src/helpers/permission-catalog.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1f527d9da7538ed923e4411e44aeb65ea806d697 --- /dev/null +++ b/packages/backend/src/helpers/permission-catalog.ee.js @@ -0,0 +1,72 @@ +const Connection = { + label: 'Connection', + key: 'Connection', +}; + +const Flow = { + label: 'Flow', + key: 'Flow', +}; + +const Execution = { + label: 'Execution', + key: 'Execution', +}; + +const permissionCatalog = { + conditions: [ + { + key: 'isCreator', + label: 'Is creator' + } + ], + actions: [ + { + label: 'Create', + key: 'create', + subjects: [ + Connection.key, + Flow.key, + ] + }, + { + label: 'Read', + key: 'read', + subjects: [ + Connection.key, + Execution.key, + Flow.key, + ] + }, + { + label: 'Update', + key: 'update', + subjects: [ + Connection.key, + Flow.key, + ] + }, + { + label: 'Delete', + key: 'delete', + subjects: [ + Connection.key, + Flow.key, + ] + }, + { + label: 'Publish', + key: 'publish', + subjects: [ + Flow.key, + ] + } + ], + subjects: [ + Connection, + Flow, + Execution + ] +}; + +export default permissionCatalog; diff --git a/packages/backend/src/helpers/remove-job-configuration.js b/packages/backend/src/helpers/remove-job-configuration.js new file mode 100644 index 0000000000000000000000000000000000000000..e6e014434d0c6ae5ae25773374630ddd38abeb74 --- /dev/null +++ b/packages/backend/src/helpers/remove-job-configuration.js @@ -0,0 +1,10 @@ +export const REMOVE_AFTER_30_DAYS_OR_150_JOBS = { + age: 30 * 24 * 3600, + count: 150, +}; + +export const REMOVE_AFTER_7_DAYS_OR_50_JOBS = { + age: 7 * 24 * 3600, + count: 50, +}; + diff --git a/packages/backend/src/helpers/renderer.js b/packages/backend/src/helpers/renderer.js new file mode 100644 index 0000000000000000000000000000000000000000..c4329a0090d1e7f082fc73b8fed77524ef80c8ce --- /dev/null +++ b/packages/backend/src/helpers/renderer.js @@ -0,0 +1,65 @@ +import serializers from '../serializers/index.js'; + +const isPaginated = (object) => + object?.pageInfo && + object?.totalCount !== undefined && + Array.isArray(object?.records); + +const isArray = (object) => + Array.isArray(object) || Array.isArray(object?.records); + +const totalCount = (object) => + isPaginated(object) ? object.totalCount : isArray(object) ? object.length : 1; + +const renderObject = (response, object, options) => { + let data = isPaginated(object) ? object.records : object; + + const type = isPaginated(object) + ? object.records[0]?.constructor?.name || 'Object' + : Array.isArray(object) + ? object?.[0]?.constructor?.name || 'Object' + : object.constructor.name; + + const serializer = options?.serializer + ? serializers[options.serializer] + : serializers[type]; + + if (serializer) { + data = Array.isArray(data) + ? data.map((item) => serializer(item)) + : serializer(data); + } + + const computedPayload = { + data, + meta: { + type, + count: totalCount(object), + isArray: isArray(object), + currentPage: isPaginated(object) ? object.pageInfo.currentPage : null, + totalPages: isPaginated(object) ? object.pageInfo.totalPages : null, + }, + }; + + return response.json(computedPayload); +}; + +const renderError = (response, errors, status, type) => { + const errorStatus = status || 422; + const errorType = type || 'ValidationError'; + + const payload = { + errors: errors.reduce((acc, error) => { + const key = Object.keys(error)[0]; + acc[key] = error[key]; + return acc; + }, {}), + meta: { + type: errorType, + }, + }; + + return response.status(errorStatus).send(payload); +}; + +export { renderObject, renderError }; diff --git a/packages/backend/src/helpers/sentry.ee.js b/packages/backend/src/helpers/sentry.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..f8f8b6f77bf355b8882a59c942c1a31307e7bb13 --- /dev/null +++ b/packages/backend/src/helpers/sentry.ee.js @@ -0,0 +1,55 @@ +import * as Sentry from '@sentry/node'; +import * as Tracing from '@sentry/tracing'; + +import appConfig from '../config/app.js'; + +const isSentryEnabled = () => { + if (appConfig.isDev || appConfig.isTest) return false; + return !!appConfig.sentryDsn; +}; + +export function init(app) { + if (!isSentryEnabled()) return; + + return Sentry.init({ + enabled: !!appConfig.sentryDsn, + dsn: appConfig.sentryDsn, + integrations: [ + app && new Sentry.Integrations.Http({ tracing: true }), + app && new Tracing.Integrations.Express({ app }), + app && new Tracing.Integrations.GraphQL(), + ].filter(Boolean), + tracesSampleRate: 1.0, + }); +} + +export function attachRequestHandler(app) { + if (!isSentryEnabled()) return; + + app.use(Sentry.Handlers.requestHandler()); +} + +export function attachTracingHandler(app) { + if (!isSentryEnabled()) return; + + app.use(Sentry.Handlers.tracingHandler()); +} + +export function attachErrorHandler(app) { + if (!isSentryEnabled()) return; + + app.use( + Sentry.Handlers.errorHandler({ + shouldHandleError() { + // TODO: narrow down the captured errors in time as we receive samples + return true; + }, + }) + ); +} + +export function captureException(exception, captureContext) { + if (!isSentryEnabled()) return; + + return Sentry.captureException(exception, captureContext); +} diff --git a/packages/backend/src/helpers/telemetry/index.js b/packages/backend/src/helpers/telemetry/index.js new file mode 100644 index 0000000000000000000000000000000000000000..7791aec69e5598583182050fb737a7e405504cee --- /dev/null +++ b/packages/backend/src/helpers/telemetry/index.js @@ -0,0 +1,149 @@ +import Analytics from '@rudderstack/rudder-sdk-node'; +import organizationId from './organization-id.js'; +import instanceId from './instance-id.js'; +import appConfig from '../../config/app.js'; +import os from 'os'; + +const WRITE_KEY = '284Py4VgK2MsNYV7xlKzyrALx0v'; +const DATA_PLANE_URL = 'https://telemetry.automatisch.io/v1/batch'; +const CPUS = os.cpus(); +const SIX_HOURS_IN_MILLISECONDS = 21600000; + +class Telemetry { + constructor() { + this.client = new Analytics(WRITE_KEY, DATA_PLANE_URL); + this.organizationId = organizationId(); + this.instanceId = instanceId(); + } + + setServiceType(type) { + this.serviceType = type; + } + + track(name, properties) { + if (!appConfig.telemetryEnabled) { + return; + } + + properties = { + ...properties, + appEnv: appConfig.appEnv, + instanceId: this.instanceId, + }; + + this.client.track({ + userId: this.organizationId, + event: name, + properties, + }); + } + + stepCreated(step) { + this.track('stepCreated', { + stepId: step.id, + flowId: step.flowId, + createdAt: step.createdAt, + updatedAt: step.updatedAt, + }); + } + + stepUpdated(step) { + this.track('stepUpdated', { + stepId: step.id, + flowId: step.flowId, + key: step.key, + appKey: step.appKey, + type: step.type, + position: step.position, + status: step.status, + createdAt: step.createdAt, + updatedAt: step.updatedAt, + }); + } + + flowCreated(flow) { + this.track('flowCreated', { + flowId: flow.id, + name: flow.name, + active: flow.active, + createdAt: flow.createdAt, + updatedAt: flow.updatedAt, + }); + } + + flowUpdated(flow) { + this.track('flowUpdated', { + flowId: flow.id, + name: flow.name, + active: flow.active, + createdAt: flow.createdAt, + updatedAt: flow.updatedAt, + }); + } + + executionCreated(execution) { + this.track('executionCreated', { + executionId: execution.id, + flowId: execution.flowId, + testRun: execution.testRun, + createdAt: execution.createdAt, + updatedAt: execution.updatedAt, + }); + } + + executionStepCreated(executionStep) { + this.track('executionStepCreated', { + executionStepId: executionStep.id, + executionId: executionStep.executionId, + stepId: executionStep.stepId, + status: executionStep.status, + createdAt: executionStep.createdAt, + updatedAt: executionStep.updatedAt, + }); + } + + connectionCreated(connection) { + this.track('connectionCreated', { + connectionId: connection.id, + key: connection.key, + verified: connection.verified, + createdAt: connection.createdAt, + updatedAt: connection.updatedAt, + }); + } + + connectionUpdated(connection) { + this.track('connectionUpdated', { + connectionId: connection.id, + key: connection.key, + verified: connection.verified, + createdAt: connection.createdAt, + updatedAt: connection.updatedAt, + }); + } + + diagnosticInfo() { + this.track('diagnosticInfo', { + automatischVersion: appConfig.version, + serveWebAppSeparately: appConfig.serveWebAppSeparately, + serviceType: this.serviceType, + operatingSystem: { + type: os.type(), + version: os.version(), + }, + memory: os.totalmem() / (1024 * 1024), // To get as megabytes + cpus: { + count: CPUS.length, + model: CPUS[0].model, + speed: CPUS[0].speed, + }, + }); + + setTimeout(() => this.diagnosticInfo(), SIX_HOURS_IN_MILLISECONDS); + } +} + +const telemetry = new Telemetry(); +telemetry.diagnosticInfo(); + +export default telemetry; diff --git a/packages/backend/src/helpers/telemetry/instance-id.js b/packages/backend/src/helpers/telemetry/instance-id.js new file mode 100644 index 0000000000000000000000000000000000000000..ce0b302477b679c9335723027f792d1399b5f6e4 --- /dev/null +++ b/packages/backend/src/helpers/telemetry/instance-id.js @@ -0,0 +1,7 @@ +import Crypto from 'crypto'; + +const instanceId = () => { + return Crypto.randomUUID(); +}; + +export default instanceId; diff --git a/packages/backend/src/helpers/telemetry/organization-id.js b/packages/backend/src/helpers/telemetry/organization-id.js new file mode 100644 index 0000000000000000000000000000000000000000..14c7f41afc3859f2980088ddd6b0534f5cfb3d69 --- /dev/null +++ b/packages/backend/src/helpers/telemetry/organization-id.js @@ -0,0 +1,13 @@ +import CryptoJS from 'crypto-js'; +import appConfig from '../../config/app.js'; + +const organizationId = () => { + const key = appConfig.encryptionKey; + const hash = CryptoJS.SHA3(key, { outputLength: 256 }).toString( + CryptoJS.enc.Hex + ); + + return hash; +}; + +export default organizationId; diff --git a/packages/backend/src/helpers/user-ability.js b/packages/backend/src/helpers/user-ability.js new file mode 100644 index 0000000000000000000000000000000000000000..74f4620b1e0cacb82f912a865132636c33dfe492 --- /dev/null +++ b/packages/backend/src/helpers/user-ability.js @@ -0,0 +1,23 @@ +import { + PureAbility, + fieldPatternMatcher, + mongoQueryMatcher, +} from '@casl/ability'; + +// Must be kept in sync with `packages/web/src/helpers/userAbility.ts`! +export default function userAbility(user) { + const permissions = user?.permissions; + const role = user?.role; + + // We're not using mongo, but our fields, conditions match + const options = { + conditionsMatcher: mongoQueryMatcher, + fieldMatcher: fieldPatternMatcher, + }; + + if (!role || !permissions) { + return new PureAbility([], options); + } + + return new PureAbility(permissions, options); +} diff --git a/packages/backend/src/helpers/web-ui-handler.js b/packages/backend/src/helpers/web-ui-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..a20c66bad4d7ee44935e919d0d8de32413dd8e00 --- /dev/null +++ b/packages/backend/src/helpers/web-ui-handler.js @@ -0,0 +1,25 @@ +import express from 'express'; +import path, { join } from 'path'; +import appConfig from '../config/app.js'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const webUIHandler = async (app) => { + if (appConfig.serveWebAppSeparately) return; + + const webAppPath = join(__dirname, '../../../web/'); + const webBuildPath = join(webAppPath, 'build'); + const indexHtml = join(webAppPath, 'build', 'index.html'); + + app.use(express.static(webBuildPath)); + + app.get('*', (_req, res) => { + res.set('Content-Security-Policy', 'frame-ancestors \'none\';'); + res.set('X-Frame-Options', 'DENY'); + + res.sendFile(indexHtml); + }); +}; + +export default webUIHandler; diff --git a/packages/backend/src/helpers/webhook-handler-sync.js b/packages/backend/src/helpers/webhook-handler-sync.js new file mode 100644 index 0000000000000000000000000000000000000000..f7c406421e60d9a407f82151c08433c97c5a64ff --- /dev/null +++ b/packages/backend/src/helpers/webhook-handler-sync.js @@ -0,0 +1,97 @@ +import isEmpty from 'lodash/isEmpty.js'; + +import Flow from '../models/flow.js'; +import { processTrigger } from '../services/trigger.js'; +import { processAction } from '../services/action.js'; +import globalVariable from './global-variable.js'; +import QuotaExceededError from '../errors/quote-exceeded.js'; + +export default async (flowId, request, response) => { + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + + const testRun = !flow.active; + const quotaExceeded = !testRun && !(await user.isAllowedToRunFlows()); + + if (quotaExceeded) { + throw new QuotaExceededError(); + } + + const [triggerStep, ...actionSteps] = await flow + .$relatedQuery('steps') + .withGraphFetched('connection') + .orderBy('position', 'asc'); + const app = await triggerStep.getApp(); + const isWebhookApp = app.key === 'webhook'; + + if (testRun && !isWebhookApp) { + return response.status(404); + } + + const connection = await triggerStep.$relatedQuery('connection'); + + const $ = await globalVariable({ + flow, + connection, + app, + step: triggerStep, + testRun, + request, + }); + + const triggerCommand = await triggerStep.getTriggerCommand(); + await triggerCommand.run($); + + const reversedTriggerItems = $.triggerOutput.data.reverse(); + + // This is the case when we filter out the incoming data + // in the run method of the webhook trigger. + // In this case, we don't want to process anything. + if (isEmpty(reversedTriggerItems)) { + return response.status(204); + } + + // set default status, but do not send it yet! + response.status(204); + + for (const triggerItem of reversedTriggerItems) { + const { executionId } = await processTrigger({ + flowId, + stepId: triggerStep.id, + triggerItem, + testRun, + }); + + if (testRun) { + // in case of testing, we do not process the whole process. + continue; + } + + for (const actionStep of actionSteps) { + const { executionStep: actionExecutionStep } = await processAction({ + flowId: flow.id, + stepId: actionStep.id, + executionId, + }); + + if (actionStep.key === 'respondWith' && !response.headersSent) { + const { headers, statusCode, body } = actionExecutionStep.dataOut; + + // we set the custom response headers + if (headers) { + for (const [key, value] of Object.entries(headers)) { + if (key) { + response.set(key, value); + } + } + } + + // we send the response only if it's not sent yet. This allows us to early respond from the flow. + response.status(statusCode); + response.send(body); + } + } + } + + return response; +}; diff --git a/packages/backend/src/helpers/webhook-handler.js b/packages/backend/src/helpers/webhook-handler.js new file mode 100644 index 0000000000000000000000000000000000000000..fc39b12443973d90506b498eb2ac5a154fb4b809 --- /dev/null +++ b/packages/backend/src/helpers/webhook-handler.js @@ -0,0 +1,84 @@ +import isEmpty from 'lodash/isEmpty.js'; + +import Flow from '../models/flow.js'; +import { processTrigger } from '../services/trigger.js'; +import triggerQueue from '../queues/trigger.js'; +import globalVariable from './global-variable.js'; +import QuotaExceededError from '../errors/quote-exceeded.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from './remove-job-configuration.js'; + +export default async (flowId, request, response) => { + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + + const testRun = !flow.active; + const quotaExceeded = !testRun && !(await user.isAllowedToRunFlows()); + + if (quotaExceeded) { + throw new QuotaExceededError(); + } + + const triggerStep = await flow.getTriggerStep(); + const app = await triggerStep.getApp(); + const isWebhookApp = app.key === 'webhook'; + + if (testRun && !isWebhookApp) { + return response.status(404); + } + + const connection = await triggerStep.$relatedQuery('connection'); + + const $ = await globalVariable({ + flow, + connection, + app, + step: triggerStep, + testRun, + request, + }); + + const triggerCommand = await triggerStep.getTriggerCommand(); + await triggerCommand.run($); + + const reversedTriggerItems = $.triggerOutput.data.reverse(); + + // This is the case when we filter out the incoming data + // in the run method of the webhook trigger. + // In this case, we don't want to process anything. + if (isEmpty(reversedTriggerItems)) { + return response.status(204); + } + + for (const triggerItem of reversedTriggerItems) { + if (testRun) { + await processTrigger({ + flowId, + stepId: triggerStep.id, + triggerItem, + testRun, + }); + + continue; + } + + const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + const jobPayload = { + flowId, + stepId: triggerStep.id, + triggerItem, + }; + + await triggerQueue.add(jobName, jobPayload, jobOptions); + } + + return response.status(204); +}; diff --git a/packages/backend/src/models/access-token.js b/packages/backend/src/models/access-token.js new file mode 100644 index 0000000000000000000000000000000000000000..7fba0cb86986fd12b5b1d7cc6a6fbbe325eaf967 --- /dev/null +++ b/packages/backend/src/models/access-token.js @@ -0,0 +1,66 @@ +import Base from './base.js'; +import User from './user.js'; + +class AccessToken extends Base { + static tableName = 'access_tokens'; + + static jsonSchema = { + type: 'object', + required: ['token', 'expiresIn'], + + properties: { + id: { type: 'string', format: 'uuid' }, + userId: { type: 'string', format: 'uuid' }, + token: { type: 'string', minLength: 32 }, + samlSessionId: { type: ['string', 'null'] }, + expiresIn: { type: 'integer' }, + revokedAt: { type: ['string', 'null'], format: 'date-time' }, + }, + }; + + static relationMappings = () => ({ + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'access_tokens.user_id', + to: 'users.id', + }, + }, + }); + + async terminateRemoteSamlSession() { + if (!this.samlSessionId) { + return; + } + + const user = await this + .$relatedQuery('user'); + + const firstIdentity = await user + .$relatedQuery('identities') + .first(); + + const samlAuthProvider = await firstIdentity + .$relatedQuery('samlAuthProvider') + .throwIfNotFound(); + + const response = await samlAuthProvider.terminateRemoteSession(this.samlSessionId); + + return response; + } + + async revoke() { + const response = await this.$query().patch({ revokedAt: new Date().toISOString() }); + + try { + await this.terminateRemoteSamlSession(); + } catch (error) { + // TODO: should it silently fail or not? + } + + return response; + } +} + +export default AccessToken; diff --git a/packages/backend/src/models/app-auth-client.js b/packages/backend/src/models/app-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..0121a72708e35873255a34d4634a949884d8c786 --- /dev/null +++ b/packages/backend/src/models/app-auth-client.js @@ -0,0 +1,67 @@ +import AES from 'crypto-js/aes.js'; +import enc from 'crypto-js/enc-utf8.js'; +import appConfig from '../config/app.js'; +import Base from './base.js'; + +class AppAuthClient extends Base { + static tableName = 'app_auth_clients'; + + static jsonSchema = { + type: 'object', + required: ['name', 'appKey', 'formattedAuthDefaults'], + + properties: { + id: { type: 'string', format: 'uuid' }, + appKey: { type: 'string' }, + active: { type: 'boolean' }, + authDefaults: { type: ['string', 'null'] }, + formattedAuthDefaults: { type: 'object' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + encryptData() { + if (!this.eligibleForEncryption()) return; + + this.authDefaults = AES.encrypt( + JSON.stringify(this.formattedAuthDefaults), + appConfig.encryptionKey + ).toString(); + + delete this.formattedAuthDefaults; + } + decryptData() { + if (!this.eligibleForDecryption()) return; + + this.formattedAuthDefaults = JSON.parse( + AES.decrypt(this.authDefaults, appConfig.encryptionKey).toString(enc) + ); + } + + eligibleForEncryption() { + return this.formattedAuthDefaults ? true : false; + } + + eligibleForDecryption() { + return this.authDefaults ? true : false; + } + + // TODO: Make another abstraction like beforeSave instead of using + // beforeInsert and beforeUpdate separately for the same operation. + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + this.encryptData(); + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + this.encryptData(); + } + + async $afterFind() { + this.decryptData(); + } +} + +export default AppAuthClient; diff --git a/packages/backend/src/models/app-config.js b/packages/backend/src/models/app-config.js new file mode 100644 index 0000000000000000000000000000000000000000..c66be1d23b923bb58f527e006d9d7990591811ca --- /dev/null +++ b/packages/backend/src/models/app-config.js @@ -0,0 +1,59 @@ +import App from './app.js'; +import AppAuthClient from './app-auth-client.js'; +import Base from './base.js'; + +class AppConfig extends Base { + static tableName = 'app_configs'; + + static jsonSchema = { + type: 'object', + required: ['key'], + + properties: { + id: { type: 'string', format: 'uuid' }, + key: { type: 'string' }, + allowCustomConnection: { type: 'boolean', default: false }, + shared: { type: 'boolean', default: false }, + disabled: { type: 'boolean', default: false }, + }, + }; + + static relationMappings = () => ({ + appAuthClients: { + relation: Base.HasManyRelation, + modelClass: AppAuthClient, + join: { + from: 'app_configs.key', + to: 'app_auth_clients.app_key', + }, + }, + }); + + static get virtualAttributes() { + return ['canConnect', 'canCustomConnect']; + } + + get canCustomConnect() { + return !this.disabled && this.allowCustomConnection; + } + + get canConnect() { + const hasSomeActiveAppAuthClients = !!this.appAuthClients?.some( + (appAuthClient) => appAuthClient.active + ); + const shared = this.shared; + const active = this.disabled === false; + + const conditions = [hasSomeActiveAppAuthClients, shared, active]; + + return conditions.every(Boolean); + } + + async getApp() { + if (!this.key) return null; + + return await App.findOneByKey(this.key); + } +} + +export default AppConfig; diff --git a/packages/backend/src/models/app.js b/packages/backend/src/models/app.js new file mode 100644 index 0000000000000000000000000000000000000000..3a85a139a1a6fa80de28992c0ed0d8d41b365f13 --- /dev/null +++ b/packages/backend/src/models/app.js @@ -0,0 +1,114 @@ +import fs from 'fs'; +import path, { join } from 'path'; +import { fileURLToPath } from 'url'; +import appInfoConverter from '../helpers/app-info-converter.js'; +import getApp from '../helpers/get-app.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +class App { + static folderPath = join(__dirname, '../apps'); + static list = fs + .readdirSync(this.folderPath) + .filter((file) => fs.statSync(this.folderPath + '/' + file).isDirectory()); + + static async findAll(name, stripFuncs = true) { + if (!name) + return Promise.all( + this.list.map( + async (name) => await this.findOneByName(name, stripFuncs) + ) + ); + + return Promise.all( + this.list + .filter((app) => app.includes(name.toLowerCase())) + .map((name) => this.findOneByName(name, stripFuncs)) + ); + } + + static async findOneByName(name, stripFuncs = false) { + const rawAppData = await getApp(name.toLocaleLowerCase(), stripFuncs); + + return appInfoConverter(rawAppData); + } + + static async findOneByKey(key, stripFuncs = false) { + const rawAppData = await getApp(key, stripFuncs); + + return appInfoConverter(rawAppData); + } + + static async findAuthByKey(key, stripFuncs = false) { + const rawAppData = await getApp(key, stripFuncs); + const appData = appInfoConverter(rawAppData); + + return appData?.auth || {}; + } + + static async findTriggersByKey(key, stripFuncs = false) { + const rawAppData = await getApp(key, stripFuncs); + const appData = appInfoConverter(rawAppData); + + return appData?.triggers || []; + } + + static async findTriggerSubsteps(appKey, triggerKey, stripFuncs = false) { + const rawAppData = await getApp(appKey, stripFuncs); + const appData = appInfoConverter(rawAppData); + + const trigger = appData?.triggers?.find( + (trigger) => trigger.key === triggerKey + ); + + return trigger?.substeps || []; + } + + static async findActionsByKey(key, stripFuncs = false) { + const rawAppData = await getApp(key, stripFuncs); + const appData = appInfoConverter(rawAppData); + + return appData?.actions || []; + } + + static async findActionSubsteps(appKey, actionKey, stripFuncs = false) { + const rawAppData = await getApp(appKey, stripFuncs); + const appData = appInfoConverter(rawAppData); + + const action = appData?.actions?.find((action) => action.key === actionKey); + + return action?.substeps || []; + } + + static async checkAppAndAction(appKey, actionKey) { + const app = await this.findOneByKey(appKey); + + if (!actionKey) return; + + const hasAction = app.actions?.find((action) => action.key === actionKey); + + if (!hasAction) { + throw new Error( + `${app.name} does not have an action with the "${actionKey}" key!` + ); + } + } + + static async checkAppAndTrigger(appKey, triggerKey) { + const app = await this.findOneByKey(appKey); + + if (!triggerKey) return; + + const hasTrigger = app.triggers?.find( + (trigger) => trigger.key === triggerKey + ); + + if (!hasTrigger) { + throw new Error( + `${app.name} does not have a trigger with the "${triggerKey}" key!` + ); + } + } +} + +export default App; diff --git a/packages/backend/src/models/base.js b/packages/backend/src/models/base.js new file mode 100644 index 0000000000000000000000000000000000000000..7cd22d72f35e235f3e4b3afb6a3380455a753c14 --- /dev/null +++ b/packages/backend/src/models/base.js @@ -0,0 +1,40 @@ +import { AjvValidator, Model, snakeCaseMappers } from 'objection'; +import addFormats from 'ajv-formats'; + +import ExtendedQueryBuilder from './query-builder.js'; + +class Base extends Model { + static QueryBuilder = ExtendedQueryBuilder; + + static get columnNameMappers() { + return snakeCaseMappers(); + } + + static createValidator() { + return new AjvValidator({ + onCreateAjv: (ajv) => { + addFormats.default(ajv); + }, + options: { + allErrors: true, + validateSchema: true, + ownProperties: true, + }, + }); + } + + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + + this.createdAt = new Date().toISOString(); + this.updatedAt = new Date().toISOString(); + } + + async $beforeUpdate(opts, queryContext) { + this.updatedAt = new Date().toISOString(); + + await super.$beforeUpdate(opts, queryContext); + } +} + +export default Base; diff --git a/packages/backend/src/models/config.js b/packages/backend/src/models/config.js new file mode 100644 index 0000000000000000000000000000000000000000..b65b6ece702cc1d2e762a92a4334367f461cb999 --- /dev/null +++ b/packages/backend/src/models/config.js @@ -0,0 +1,40 @@ +import Base from './base.js'; + +class Config extends Base { + static tableName = 'config'; + + static jsonSchema = { + type: 'object', + required: ['key', 'value'], + + properties: { + id: { type: 'string', format: 'uuid' }, + key: { type: 'string', minLength: 1 }, + value: { type: 'object' }, + }, + }; + + static async isInstallationCompleted() { + const installationCompletedEntry = await this + .query() + .where({ + key: 'installation.completed' + }) + .first(); + + const installationCompleted = installationCompletedEntry?.value?.data === true; + + return installationCompleted; + } + + static async markInstallationCompleted() { + return await this.query().insert({ + key: 'installation.completed', + value: { + data: true, + }, + }); + } +} + +export default Config; diff --git a/packages/backend/src/models/connection.js b/packages/backend/src/models/connection.js new file mode 100644 index 0000000000000000000000000000000000000000..d4e3f9f83b2a56288b8f5c9a75bbda3195091480 --- /dev/null +++ b/packages/backend/src/models/connection.js @@ -0,0 +1,190 @@ +import AES from 'crypto-js/aes.js'; +import enc from 'crypto-js/enc-utf8.js'; +import App from './app.js'; +import AppConfig from './app-config.js'; +import AppAuthClient from './app-auth-client.js'; +import Base from './base.js'; +import User from './user.js'; +import Step from './step.js'; +import appConfig from '../config/app.js'; +import Telemetry from '../helpers/telemetry/index.js'; +import globalVariable from '../helpers/global-variable.js'; + +class Connection extends Base { + static tableName = 'connections'; + + static jsonSchema = { + type: 'object', + required: ['key'], + + properties: { + id: { type: 'string', format: 'uuid' }, + key: { type: 'string', minLength: 1, maxLength: 255 }, + data: { type: 'string' }, + formattedData: { type: 'object' }, + userId: { type: 'string', format: 'uuid' }, + appAuthClientId: { type: 'string', format: 'uuid' }, + verified: { type: 'boolean', default: false }, + draft: { type: 'boolean' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static get virtualAttributes() { + return ['reconnectable']; + } + + static relationMappings = () => ({ + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'connections.user_id', + to: 'users.id', + }, + }, + steps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'connections.id', + to: 'steps.connection_id', + }, + }, + triggerSteps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'connections.id', + to: 'steps.connection_id', + }, + filter(builder) { + builder.where('type', '=', 'trigger'); + }, + }, + appConfig: { + relation: Base.BelongsToOneRelation, + modelClass: AppConfig, + join: { + from: 'connections.key', + to: 'app_configs.key', + }, + }, + appAuthClient: { + relation: Base.BelongsToOneRelation, + modelClass: AppAuthClient, + join: { + from: 'connections.app_auth_client_id', + to: 'app_auth_clients.id', + }, + }, + }); + + get reconnectable() { + if (this.appAuthClientId) { + return this.appAuthClient.active; + } + + if (this.appConfig) { + return !this.appConfig.disabled && this.appConfig.allowCustomConnection; + } + + return true; + } + + encryptData() { + if (!this.eligibleForEncryption()) return; + + this.data = AES.encrypt( + JSON.stringify(this.formattedData), + appConfig.encryptionKey + ).toString(); + + delete this.formattedData; + } + + decryptData() { + if (!this.eligibleForDecryption()) return; + + this.formattedData = JSON.parse( + AES.decrypt(this.data, appConfig.encryptionKey).toString(enc) + ); + } + + eligibleForEncryption() { + return this.formattedData ? true : false; + } + + eligibleForDecryption() { + return this.data ? true : false; + } + + // TODO: Make another abstraction like beforeSave instead of using + // beforeInsert and beforeUpdate separately for the same operation. + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + this.encryptData(); + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + this.encryptData(); + } + + async $afterFind() { + this.decryptData(); + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.connectionCreated(this); + } + + async $afterUpdate(opt, queryContext) { + await super.$afterUpdate(opt, queryContext); + Telemetry.connectionUpdated(this); + } + + async getApp() { + if (!this.key) return null; + + return await App.findOneByKey(this.key); + } + + async testAndUpdateConnection() { + const app = await this.getApp(); + const $ = await globalVariable({ connection: this, app }); + + let isStillVerified; + + try { + isStillVerified = !!(await app.auth.isStillVerified($)); + } catch { + isStillVerified = false; + } + + return await this.$query().patchAndFetch({ + formattedData: this.formattedData, + verified: isStillVerified, + }); + } + + async verifyWebhook(request) { + if (!this.key) return true; + + const app = await this.getApp(); + + const $ = await globalVariable({ + connection: this, + request, + }); + + if (!app.auth?.verifyWebhook) return true; + + return app.auth.verifyWebhook($); + } +} + +export default Connection; diff --git a/packages/backend/src/models/datastore.js b/packages/backend/src/models/datastore.js new file mode 100644 index 0000000000000000000000000000000000000000..015bb8245d8f3840baff30938b9540542b39d09f --- /dev/null +++ b/packages/backend/src/models/datastore.js @@ -0,0 +1,24 @@ +import Base from './base.js'; + +class Datastore extends Base { + static tableName = 'datastore'; + + static jsonSchema = { + type: 'object', + required: ['key', 'value', 'scope', 'scopeId'], + + properties: { + id: { type: 'string', format: 'uuid' }, + key: { type: 'string', minLength: 1 }, + value: { type: 'string' }, + scope: { + type: 'string', + enum: ['flow'], + default: 'flow', + }, + scopeId: { type: 'string', format: 'uuid' }, + }, + }; +} + +export default Datastore; diff --git a/packages/backend/src/models/execution-step.js b/packages/backend/src/models/execution-step.js new file mode 100644 index 0000000000000000000000000000000000000000..267bbfb9f812f5d4cb41470025dca0c513a02e4b --- /dev/null +++ b/packages/backend/src/models/execution-step.js @@ -0,0 +1,68 @@ +import appConfig from '../config/app.js'; +import Base from './base.js'; +import Execution from './execution.js'; +import Step from './step.js'; +import Telemetry from '../helpers/telemetry/index.js'; + +class ExecutionStep extends Base { + static tableName = 'execution_steps'; + + static jsonSchema = { + type: 'object', + + properties: { + id: { type: 'string', format: 'uuid' }, + executionId: { type: 'string', format: 'uuid' }, + stepId: { type: 'string' }, + dataIn: { type: ['object', 'null'] }, + dataOut: { type: ['object', 'null'] }, + status: { type: 'string', enum: ['success', 'failure'] }, + errorDetails: { type: ['object', 'null'] }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + execution: { + relation: Base.BelongsToOneRelation, + modelClass: Execution, + join: { + from: 'execution_steps.execution_id', + to: 'executions.id', + }, + }, + step: { + relation: Base.BelongsToOneRelation, + modelClass: Step, + join: { + from: 'execution_steps.step_id', + to: 'steps.id', + }, + }, + }); + + get isFailed() { + return this.status === 'failure'; + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.executionStepCreated(this); + + if (appConfig.isCloud) { + const execution = await this.$relatedQuery('execution'); + + if (!execution.testRun && !this.isFailed) { + const flow = await execution.$relatedQuery('flow'); + const user = await flow.$relatedQuery('user'); + const usageData = await user.$relatedQuery('currentUsageData'); + + await usageData.increaseConsumedTaskCountByOne(); + } + } + } +} + +export default ExecutionStep; diff --git a/packages/backend/src/models/execution.js b/packages/backend/src/models/execution.js new file mode 100644 index 0000000000000000000000000000000000000000..9b219700762d40c1700d551b67dbb04f036dbdf8 --- /dev/null +++ b/packages/backend/src/models/execution.js @@ -0,0 +1,48 @@ +import Base from './base.js'; +import Flow from './flow.js'; +import ExecutionStep from './execution-step.js'; +import Telemetry from '../helpers/telemetry/index.js'; + +class Execution extends Base { + static tableName = 'executions'; + + static jsonSchema = { + type: 'object', + + properties: { + id: { type: 'string', format: 'uuid' }, + flowId: { type: 'string', format: 'uuid' }, + testRun: { type: 'boolean', default: false }, + internalId: { type: 'string' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + flow: { + relation: Base.BelongsToOneRelation, + modelClass: Flow, + join: { + from: 'executions.flow_id', + to: 'flows.id', + }, + }, + executionSteps: { + relation: Base.HasManyRelation, + modelClass: ExecutionStep, + join: { + from: 'executions.id', + to: 'execution_steps.execution_id', + }, + }, + }); + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.executionCreated(this); + } +} + +export default Execution; diff --git a/packages/backend/src/models/flow.js b/packages/backend/src/models/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..e058de71c6ada73163a15495fc7d0dd4f153f632 --- /dev/null +++ b/packages/backend/src/models/flow.js @@ -0,0 +1,169 @@ +import { ValidationError } from 'objection'; +import Base from './base.js'; +import Step from './step.js'; +import User from './user.js'; +import Execution from './execution.js'; +import Telemetry from '../helpers/telemetry/index.js'; + +class Flow extends Base { + static tableName = 'flows'; + + static jsonSchema = { + type: 'object', + required: ['name'], + + properties: { + id: { type: 'string', format: 'uuid' }, + name: { type: 'string', minLength: 1 }, + userId: { type: 'string', format: 'uuid' }, + remoteWebhookId: { type: 'string' }, + active: { type: 'boolean' }, + publishedAt: { type: 'string' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + steps: { + relation: Base.HasManyRelation, + modelClass: Step, + join: { + from: 'flows.id', + to: 'steps.flow_id', + }, + filter(builder) { + builder.orderBy('position', 'asc'); + }, + }, + triggerStep: { + relation: Base.HasOneRelation, + modelClass: Step, + join: { + from: 'flows.id', + to: 'steps.flow_id', + }, + filter(builder) { + builder.where('type', 'trigger').limit(1).first(); + }, + }, + executions: { + relation: Base.HasManyRelation, + modelClass: Execution, + join: { + from: 'flows.id', + to: 'executions.flow_id', + }, + }, + lastExecution: { + relation: Base.HasOneRelation, + modelClass: Execution, + join: { + from: 'flows.id', + to: 'executions.flow_id', + }, + filter(builder) { + builder.orderBy('created_at', 'desc').limit(1).first(); + }, + }, + user: { + relation: Base.HasOneRelation, + modelClass: User, + join: { + from: 'flows.user_id', + to: 'users.id', + }, + }, + }); + + static async afterFind(args) { + const { result } = args; + + const referenceFlow = result[0]; + + if (referenceFlow) { + const shouldBePaused = await referenceFlow.isPaused(); + + for (const flow of result) { + if (!flow.active) { + flow.status = 'draft'; + } else if (flow.active && shouldBePaused) { + flow.status = 'paused'; + } else { + flow.status = 'published'; + } + } + } + } + + async lastInternalId() { + const lastExecution = await this.$relatedQuery('lastExecution'); + + return lastExecution ? lastExecution.internalId : null; + } + + async lastInternalIds(itemCount = 50) { + const lastExecutions = await this.$relatedQuery('executions') + .select('internal_id') + .orderBy('created_at', 'desc') + .limit(itemCount); + + return lastExecutions.map((execution) => execution.internalId); + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + + if (!this.active) return; + + const oldFlow = opt.old; + + const incompleteStep = await oldFlow.$relatedQuery('steps').findOne({ + status: 'incomplete', + }); + + if (incompleteStep) { + throw new ValidationError({ + message: 'All steps should be completed before updating flow status!', + type: 'incompleteStepsError', + }); + } + + const allSteps = await oldFlow.$relatedQuery('steps'); + + if (allSteps.length < 2) { + throw new ValidationError({ + message: + 'There should be at least one trigger and one action steps in the flow!', + type: 'insufficientStepsError', + }); + } + + return; + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.flowCreated(this); + } + + async $afterUpdate(opt, queryContext) { + await super.$afterUpdate(opt, queryContext); + Telemetry.flowUpdated(this); + } + + async getTriggerStep() { + return await this.$relatedQuery('steps').findOne({ + type: 'trigger', + }); + } + + async isPaused() { + const user = await this.$relatedQuery('user').withSoftDeleted(); + const allowedToRunFlows = await user.isAllowedToRunFlows(); + return allowedToRunFlows ? false : true; + } +} + +export default Flow; diff --git a/packages/backend/src/models/identity.ee.js b/packages/backend/src/models/identity.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..09b374248ea5a1e986a06aaa93a5c77348740e4d --- /dev/null +++ b/packages/backend/src/models/identity.ee.js @@ -0,0 +1,41 @@ +import Base from './base.js'; +import SamlAuthProvider from './saml-auth-provider.ee.js'; +import User from './user.js'; + +class Identity extends Base { + static tableName = 'identities'; + + static jsonSchema = { + type: 'object', + required: ['providerId', 'remoteId', 'userId', 'providerType'], + + properties: { + id: { type: 'string', format: 'uuid' }, + userId: { type: 'string', format: 'uuid' }, + remoteId: { type: 'string', minLength: 1 }, + providerId: { type: 'string', format: 'uuid' }, + providerType: { type: 'string', enum: ['saml'] }, + }, + }; + + static relationMappings = () => ({ + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'users.id', + to: 'identities.user_id', + }, + }, + samlAuthProvider: { + relation: Base.BelongsToOneRelation, + modelClass: SamlAuthProvider, + join: { + from: 'saml_auth_providers.id', + to: 'identities.provider_id', + }, + }, + }); +} + +export default Identity; diff --git a/packages/backend/src/models/permission.js b/packages/backend/src/models/permission.js new file mode 100644 index 0000000000000000000000000000000000000000..7ebd79c0c8ab3697725227b7d30ecb06301f62db --- /dev/null +++ b/packages/backend/src/models/permission.js @@ -0,0 +1,22 @@ +import Base from './base.js'; + +class Permission extends Base { + static tableName = 'permissions'; + + static jsonSchema = { + type: 'object', + required: ['roleId', 'action', 'subject'], + + properties: { + id: { type: 'string', format: 'uuid' }, + roleId: { type: 'string', format: 'uuid' }, + action: { type: 'string', minLength: 1 }, + subject: { type: 'string', minLength: 1 }, + conditions: { type: 'array', items: { type: 'string' } }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; +} + +export default Permission; diff --git a/packages/backend/src/models/query-builder.js b/packages/backend/src/models/query-builder.js new file mode 100644 index 0000000000000000000000000000000000000000..d3e0a89ee929b6961afdb4f789dbab6c97017255 --- /dev/null +++ b/packages/backend/src/models/query-builder.js @@ -0,0 +1,58 @@ +import { Model } from 'objection'; + +const DELETED_COLUMN_NAME = 'deleted_at'; + +const supportsSoftDeletion = (modelClass) => { + return modelClass.jsonSchema.properties.deletedAt; +}; + +const buildQueryBuidlerForClass = () => { + return (modelClass) => { + const qb = Model.QueryBuilder.forClass.call( + ExtendedQueryBuilder, + modelClass + ); + qb.onBuild((builder) => { + if ( + !builder.context().withSoftDeleted && + supportsSoftDeletion(qb.modelClass()) + ) { + builder.whereNull( + `${qb.modelClass().tableName}.${DELETED_COLUMN_NAME}` + ); + } + }); + return qb; + }; +}; + +class ExtendedQueryBuilder extends Model.QueryBuilder { + static forClass = buildQueryBuidlerForClass(); + + delete() { + if (supportsSoftDeletion(this.modelClass())) { + return this.patch({ + [DELETED_COLUMN_NAME]: new Date().toISOString(), + }); + } + + return super.delete(); + } + + hardDelete() { + return super.delete(); + } + + withSoftDeleted() { + this.context().withSoftDeleted = true; + return this; + } + + restore() { + return this.patch({ + [DELETED_COLUMN_NAME]: null, + }); + } +} + +export default ExtendedQueryBuilder; diff --git a/packages/backend/src/models/role.js b/packages/backend/src/models/role.js new file mode 100644 index 0000000000000000000000000000000000000000..08b19673e2435fd32f9a6450d238e24567689db3 --- /dev/null +++ b/packages/backend/src/models/role.js @@ -0,0 +1,54 @@ +import Base from './base.js'; +import Permission from './permission.js'; +import User from './user.js'; + +class Role extends Base { + static tableName = 'roles'; + + static jsonSchema = { + type: 'object', + required: ['name', 'key'], + + properties: { + id: { type: 'string', format: 'uuid' }, + name: { type: 'string', minLength: 1 }, + key: { type: 'string', minLength: 1 }, + description: { type: ['string', 'null'], maxLength: 255 }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static get virtualAttributes() { + return ['isAdmin']; + } + + static relationMappings = () => ({ + users: { + relation: Base.HasManyRelation, + modelClass: User, + join: { + from: 'roles.id', + to: 'users.role_id', + }, + }, + permissions: { + relation: Base.HasManyRelation, + modelClass: Permission, + join: { + from: 'roles.id', + to: 'permissions.role_id', + }, + }, + }); + + get isAdmin() { + return this.key === 'admin'; + } + + static async findAdmin() { + return await this.query().findOne({ key: 'admin' }); + } +} + +export default Role; diff --git a/packages/backend/src/models/saml-auth-provider.ee.js b/packages/backend/src/models/saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..431153f94e57efb52a098cdb680a11f641463cf8 --- /dev/null +++ b/packages/backend/src/models/saml-auth-provider.ee.js @@ -0,0 +1,133 @@ +import { URL } from 'node:url'; +import { v4 as uuidv4 } from 'uuid'; +import appConfig from '../config/app.js'; +import axios from '../helpers/axios-with-proxy.js'; +import Base from './base.js'; +import Identity from './identity.ee.js'; +import SamlAuthProvidersRoleMapping from './saml-auth-providers-role-mapping.ee.js'; + +class SamlAuthProvider extends Base { + static tableName = 'saml_auth_providers'; + + static jsonSchema = { + type: 'object', + required: [ + 'name', + 'certificate', + 'signatureAlgorithm', + 'entryPoint', + 'issuer', + 'firstnameAttributeName', + 'surnameAttributeName', + 'emailAttributeName', + 'roleAttributeName', + 'defaultRoleId', + ], + + properties: { + id: { type: 'string', format: 'uuid' }, + name: { type: 'string', minLength: 1 }, + certificate: { type: 'string', minLength: 1 }, + signatureAlgorithm: { + type: 'string', + enum: ['sha1', 'sha256', 'sha512'], + }, + issuer: { type: 'string', minLength: 1 }, + entryPoint: { type: 'string', minLength: 1 }, + firstnameAttributeName: { type: 'string', minLength: 1 }, + surnameAttributeName: { type: 'string', minLength: 1 }, + emailAttributeName: { type: 'string', minLength: 1 }, + roleAttributeName: { type: 'string', minLength: 1 }, + defaultRoleId: { type: 'string', format: 'uuid' }, + active: { type: 'boolean' }, + }, + }; + + static relationMappings = () => ({ + identities: { + relation: Base.HasOneRelation, + modelClass: Identity, + join: { + from: 'identities.provider_id', + to: 'saml_auth_providers.id', + }, + }, + samlAuthProvidersRoleMappings: { + relation: Base.HasManyRelation, + modelClass: SamlAuthProvidersRoleMapping, + join: { + from: 'saml_auth_providers.id', + to: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + }, + }, + }); + + static get virtualAttributes() { + return ['loginUrl', 'remoteLogoutUrl']; + } + + get loginUrl() { + return new URL(`/login/saml/${this.issuer}`, appConfig.baseUrl).toString(); + } + + get loginCallBackUrl() { + return new URL( + `/login/saml/${this.issuer}/callback`, + appConfig.baseUrl + ).toString(); + } + + get remoteLogoutUrl() { + return this.entryPoint; + } + + get config() { + return { + callbackUrl: this.loginCallBackUrl, + cert: this.certificate, + entryPoint: this.entryPoint, + issuer: this.issuer, + signatureAlgorithm: this.signatureAlgorithm, + logoutUrl: this.remoteLogoutUrl + }; + } + + generateLogoutRequestBody(sessionId) { + const logoutRequest = ` + + + ${this.issuer} + ${sessionId} + + `; + + const encodedLogoutRequest = Buffer.from(logoutRequest).toString('base64') + + return encodedLogoutRequest + } + + async terminateRemoteSession(sessionId) { + const logoutRequest = this.generateLogoutRequestBody(sessionId); + + const response = await axios.post( + this.remoteLogoutUrl, + new URLSearchParams({ + SAMLRequest: logoutRequest, + }).toString(), + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + } + } + ); + + return response; + } +} + +export default SamlAuthProvider; diff --git a/packages/backend/src/models/saml-auth-providers-role-mapping.ee.js b/packages/backend/src/models/saml-auth-providers-role-mapping.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..00e11bb05ca7e3a50740d51943881fa5d74b7885 --- /dev/null +++ b/packages/backend/src/models/saml-auth-providers-role-mapping.ee.js @@ -0,0 +1,31 @@ +import Base from './base.js'; +import SamlAuthProvider from './saml-auth-provider.ee.js'; + +class SamlAuthProvidersRoleMapping extends Base { + static tableName = 'saml_auth_providers_role_mappings'; + + static jsonSchema = { + type: 'object', + required: ['samlAuthProviderId', 'roleId', 'remoteRoleName'], + + properties: { + id: { type: 'string', format: 'uuid' }, + samlAuthProviderId: { type: 'string', format: 'uuid' }, + roleId: { type: 'string', format: 'uuid' }, + remoteRoleName: { type: 'string', minLength: 1 }, + }, + }; + + static relationMappings = () => ({ + samlAuthProvider: { + relation: Base.BelongsToOneRelation, + modelClass: SamlAuthProvider, + join: { + from: 'saml_auth_providers_role_mappings.saml_auth_provider_id', + to: 'saml_auth_providers.id', + }, + }, + }); +} + +export default SamlAuthProvidersRoleMapping; diff --git a/packages/backend/src/models/step.js b/packages/backend/src/models/step.js new file mode 100644 index 0000000000000000000000000000000000000000..59ddb835e1ab6cd73dd8f933fe400ed9e1d45370 --- /dev/null +++ b/packages/backend/src/models/step.js @@ -0,0 +1,267 @@ +import { URL } from 'node:url'; +import get from 'lodash.get'; +import Base from './base.js'; +import App from './app.js'; +import Flow from './flow.js'; +import Connection from './connection.js'; +import ExecutionStep from './execution-step.js'; +import Telemetry from '../helpers/telemetry/index.js'; +import appConfig from '../config/app.js'; +import globalVariable from '../helpers/global-variable.js'; +import computeParameters from '../helpers/compute-parameters.js'; + +class Step extends Base { + static tableName = 'steps'; + + static jsonSchema = { + type: 'object', + required: ['type'], + + properties: { + id: { type: 'string', format: 'uuid' }, + flowId: { type: 'string', format: 'uuid' }, + key: { type: ['string', 'null'] }, + appKey: { type: ['string', 'null'], minLength: 1, maxLength: 255 }, + type: { type: 'string', enum: ['action', 'trigger'] }, + connectionId: { type: ['string', 'null'], format: 'uuid' }, + status: { + type: 'string', + enum: ['incomplete', 'completed'], + default: 'incomplete', + }, + position: { type: 'integer' }, + parameters: { type: 'object' }, + webhookPath: { type: ['string', 'null'] }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static get virtualAttributes() { + return ['iconUrl', 'webhookUrl']; + } + + static relationMappings = () => ({ + flow: { + relation: Base.BelongsToOneRelation, + modelClass: Flow, + join: { + from: 'steps.flow_id', + to: 'flows.id', + }, + }, + connection: { + relation: Base.HasOneRelation, + modelClass: Connection, + join: { + from: 'steps.connection_id', + to: 'connections.id', + }, + }, + executionSteps: { + relation: Base.HasManyRelation, + modelClass: ExecutionStep, + join: { + from: 'steps.id', + to: 'execution_steps.step_id', + }, + }, + }); + + get webhookUrl() { + return new URL(this.webhookPath, appConfig.webhookUrl).toString(); + } + + get iconUrl() { + if (!this.appKey) return null; + + return `${appConfig.baseUrl}/apps/${this.appKey}/assets/favicon.svg`; + } + + async computeWebhookPath() { + if (this.type === 'action') return null; + + const triggerCommand = await this.getTriggerCommand(); + + if (!triggerCommand) return null; + + const { useSingletonWebhook, singletonWebhookRefValueParameter, type } = + triggerCommand; + + const isWebhook = type === 'webhook'; + + if (!isWebhook) return null; + + if (singletonWebhookRefValueParameter) { + const parameterValue = get( + this.parameters, + singletonWebhookRefValueParameter + ); + return `/webhooks/connections/${this.connectionId}/${parameterValue}`; + } + + if (useSingletonWebhook) { + return `/webhooks/connections/${this.connectionId}`; + } + + if (this.parameters.workSynchronously) { + return `/webhooks/flows/${this.flowId}/sync`; + } + + return `/webhooks/flows/${this.flowId}`; + } + + async getWebhookUrl() { + if (this.type === 'action') return; + + const path = await this.computeWebhookPath(); + const webhookUrl = new URL(path, appConfig.webhookUrl).toString(); + + return webhookUrl; + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + Telemetry.stepCreated(this); + } + + async $afterUpdate(opt, queryContext) { + await super.$afterUpdate(opt, queryContext); + Telemetry.stepUpdated(this); + } + + get isTrigger() { + return this.type === 'trigger'; + } + + get isAction() { + return this.type === 'action'; + } + + async getApp() { + if (!this.appKey) return null; + + return await App.findOneByKey(this.appKey); + } + + async getLastExecutionStep() { + const lastExecutionStep = await this.$relatedQuery('executionSteps') + .orderBy('created_at', 'desc') + .limit(1) + .first(); + + return lastExecutionStep; + } + + async getNextStep() { + const flow = await this.$relatedQuery('flow'); + + return await flow + .$relatedQuery('steps') + .findOne({ position: this.position + 1 }); + } + + async getTriggerCommand() { + const { appKey, key, isTrigger } = this; + if (!isTrigger || !appKey || !key) return null; + + const app = await App.findOneByKey(appKey); + const command = app.triggers?.find((trigger) => trigger.key === key); + + return command; + } + + async getActionCommand() { + const { appKey, key, isAction } = this; + if (!isAction || !appKey || !key) return null; + + const app = await App.findOneByKey(appKey); + const command = app.actions?.find((action) => action.key === key); + + return command; + } + + async getSetupFields() { + let setupSupsteps; + + if (this.isTrigger) { + setupSupsteps = (await this.getTriggerCommand()).substeps; + } else { + setupSupsteps = (await this.getActionCommand()).substeps; + } + + const existingArguments = setupSupsteps.find( + (substep) => substep.key === 'chooseTrigger' + ).arguments; + + return existingArguments; + } + + async createDynamicFields(dynamicFieldsKey, parameters) { + const connection = await this.$relatedQuery('connection'); + const flow = await this.$relatedQuery('flow'); + const app = await this.getApp(); + const $ = await globalVariable({ connection, app, flow, step: this }); + + const command = app.dynamicFields.find( + (data) => data.key === dynamicFieldsKey + ); + + for (const parameterKey in parameters) { + const parameterValue = parameters[parameterKey]; + $.step.parameters[parameterKey] = parameterValue; + } + + const dynamicFields = (await command.run($)) || []; + + return dynamicFields; + } + + async createDynamicData(dynamicDataKey, parameters) { + const connection = await this.$relatedQuery('connection'); + const flow = await this.$relatedQuery('flow'); + const app = await this.getApp(); + const $ = await globalVariable({ connection, app, flow, step: this }); + + const command = app.dynamicData.find((data) => data.key === dynamicDataKey); + + for (const parameterKey in parameters) { + const parameterValue = parameters[parameterKey]; + $.step.parameters[parameterKey] = parameterValue; + } + + const lastExecution = await flow.$relatedQuery('lastExecution'); + const lastExecutionId = lastExecution?.id; + + const priorExecutionSteps = lastExecutionId + ? await ExecutionStep.query().where({ + execution_id: lastExecutionId, + }) + : []; + + const computedParameters = computeParameters( + $.step.parameters, + priorExecutionSteps + ); + + $.step.parameters = computedParameters; + const dynamicData = (await command.run($)).data; + + return dynamicData; + } + + async updateWebhookUrl() { + if (this.isAction) return this; + + const payload = { + webhookPath: await this.computeWebhookPath(), + }; + + await this.$query().patchAndFetch(payload); + + return this; + } +} + +export default Step; diff --git a/packages/backend/src/models/subscription.ee.js b/packages/backend/src/models/subscription.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..bcacf8273b962a4aea373ba7e1c985292aa855bd --- /dev/null +++ b/packages/backend/src/models/subscription.ee.js @@ -0,0 +1,89 @@ +import Base from './base.js'; +import User from './user.js'; +import UsageData from './usage-data.ee.js'; +import { DateTime } from 'luxon'; +import { getPlanById } from '../helpers/billing/plans.ee.js'; + +class Subscription extends Base { + static tableName = 'subscriptions'; + + static jsonSchema = { + type: 'object', + required: [ + 'userId', + 'paddleSubscriptionId', + 'paddlePlanId', + 'updateUrl', + 'cancelUrl', + 'status', + 'nextBillAmount', + 'nextBillDate', + ], + + properties: { + id: { type: 'string', format: 'uuid' }, + userId: { type: 'string', format: 'uuid' }, + paddleSubscriptionId: { type: 'string' }, + paddlePlanId: { type: 'string' }, + updateUrl: { type: 'string' }, + cancelUrl: { type: 'string' }, + status: { type: 'string' }, + nextBillAmount: { type: 'string' }, + nextBillDate: { type: 'string' }, + lastBillDate: { type: 'string' }, + cancellationEffectiveDate: { type: 'string' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'subscription.user_id', + to: 'users.id', + }, + }, + usageData: { + relation: Base.HasManyRelation, + modelClass: UsageData, + join: { + from: 'subscriptions.id', + to: 'usage_data.subscription_id', + }, + }, + currentUsageData: { + relation: Base.HasOneRelation, + modelClass: UsageData, + join: { + from: 'subscriptions.id', + to: 'usage_data.subscription_id', + }, + }, + }); + + get plan() { + return getPlanById(this.paddlePlanId); + } + + get isCancelledAndValid() { + return ( + this.status === 'deleted' && + Number(this.cancellationEffectiveDate) > + DateTime.now().startOf('day').toMillis() + ); + } + + get isValid() { + if (this.status === 'active') return true; + if (this.status === 'past_due') return true; + if (this.isCancelledAndValid) return true; + + return false; + } +} + +export default Subscription; diff --git a/packages/backend/src/models/usage-data.ee.js b/packages/backend/src/models/usage-data.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..eebfea370d68903145efa29f3032661d6051153e --- /dev/null +++ b/packages/backend/src/models/usage-data.ee.js @@ -0,0 +1,51 @@ +import { raw } from 'objection'; +import Base from './base.js'; +import User from './user.js'; +import Subscription from './subscription.ee.js'; + +class UsageData extends Base { + static tableName = 'usage_data'; + + static jsonSchema = { + type: 'object', + required: ['userId', 'consumedTaskCount', 'nextResetAt'], + + properties: { + id: { type: 'string', format: 'uuid' }, + userId: { type: 'string', format: 'uuid' }, + subscriptionId: { type: 'string', format: 'uuid' }, + consumedTaskCount: { type: 'integer' }, + nextResetAt: { type: 'string' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + user: { + relation: Base.BelongsToOneRelation, + modelClass: User, + join: { + from: 'usage_data.user_id', + to: 'users.id', + }, + }, + subscription: { + relation: Base.BelongsToOneRelation, + modelClass: Subscription, + join: { + from: 'usage_data.subscription_id', + to: 'subscriptions.id', + }, + }, + }); + + async increaseConsumedTaskCountByOne() { + return await this.$query().patch({ + consumedTaskCount: raw('consumed_task_count + 1'), + }); + } +} + +export default UsageData; diff --git a/packages/backend/src/models/user.js b/packages/backend/src/models/user.js new file mode 100644 index 0000000000000000000000000000000000000000..c3900b4fad4c5c440b49570fb364e37568d3bed8 --- /dev/null +++ b/packages/backend/src/models/user.js @@ -0,0 +1,472 @@ +import bcrypt from 'bcrypt'; +import { DateTime } from 'luxon'; +import crypto from 'node:crypto'; + +import appConfig from '../config/app.js'; +import { hasValidLicense } from '../helpers/license.ee.js'; +import userAbility from '../helpers/user-ability.js'; +import createAuthTokenByUserId from '../helpers/create-auth-token-by-user-id.js'; +import Base from './base.js'; +import App from './app.js'; +import AccessToken from './access-token.js'; +import Connection from './connection.js'; +import Config from './config.js'; +import Execution from './execution.js'; +import Flow from './flow.js'; +import Identity from './identity.ee.js'; +import Permission from './permission.js'; +import Role from './role.js'; +import Step from './step.js'; +import Subscription from './subscription.ee.js'; +import UsageData from './usage-data.ee.js'; +import Billing from '../helpers/billing/index.ee.js'; + +class User extends Base { + static tableName = 'users'; + + static jsonSchema = { + type: 'object', + required: ['fullName', 'email'], + + properties: { + id: { type: 'string', format: 'uuid' }, + fullName: { type: 'string', minLength: 1 }, + email: { type: 'string', format: 'email', minLength: 1, maxLength: 255 }, + password: { type: 'string' }, + resetPasswordToken: { type: 'string' }, + resetPasswordTokenSentAt: { type: 'string' }, + trialExpiryDate: { type: 'string' }, + roleId: { type: 'string', format: 'uuid' }, + deletedAt: { type: 'string' }, + createdAt: { type: 'string' }, + updatedAt: { type: 'string' }, + }, + }; + + static relationMappings = () => ({ + accessTokens: { + relation: Base.HasManyRelation, + modelClass: AccessToken, + join: { + from: 'users.id', + to: 'access_tokens.user_id', + }, + }, + connections: { + relation: Base.HasManyRelation, + modelClass: Connection, + join: { + from: 'users.id', + to: 'connections.user_id', + }, + }, + flows: { + relation: Base.HasManyRelation, + modelClass: Flow, + join: { + from: 'users.id', + to: 'flows.user_id', + }, + }, + steps: { + relation: Base.ManyToManyRelation, + modelClass: Step, + join: { + from: 'users.id', + through: { + from: 'flows.user_id', + to: 'flows.id', + }, + to: 'steps.flow_id', + }, + }, + executions: { + relation: Base.ManyToManyRelation, + modelClass: Execution, + join: { + from: 'users.id', + through: { + from: 'flows.user_id', + to: 'flows.id', + }, + to: 'executions.flow_id', + }, + }, + usageData: { + relation: Base.HasManyRelation, + modelClass: UsageData, + join: { + from: 'usage_data.user_id', + to: 'users.id', + }, + }, + currentUsageData: { + relation: Base.HasOneRelation, + modelClass: UsageData, + join: { + from: 'usage_data.user_id', + to: 'users.id', + }, + filter(builder) { + builder.orderBy('created_at', 'desc').limit(1).first(); + }, + }, + subscriptions: { + relation: Base.HasManyRelation, + modelClass: Subscription, + join: { + from: 'subscriptions.user_id', + to: 'users.id', + }, + }, + currentSubscription: { + relation: Base.HasOneRelation, + modelClass: Subscription, + join: { + from: 'subscriptions.user_id', + to: 'users.id', + }, + filter(builder) { + builder.orderBy('created_at', 'desc').limit(1).first(); + }, + }, + role: { + relation: Base.HasOneRelation, + modelClass: Role, + join: { + from: 'roles.id', + to: 'users.role_id', + }, + }, + permissions: { + relation: Base.HasManyRelation, + modelClass: Permission, + join: { + from: 'users.role_id', + to: 'permissions.role_id', + }, + }, + identities: { + relation: Base.HasManyRelation, + modelClass: Identity, + join: { + from: 'identities.user_id', + to: 'users.id', + }, + }, + }); + + get authorizedFlows() { + const conditions = this.can('read', 'Flow'); + return conditions.isCreator ? this.$relatedQuery('flows') : Flow.query(); + } + + get authorizedSteps() { + const conditions = this.can('read', 'Flow'); + return conditions.isCreator ? this.$relatedQuery('steps') : Step.query(); + } + + get authorizedConnections() { + const conditions = this.can('read', 'Connection'); + return conditions.isCreator + ? this.$relatedQuery('connections') + : Connection.query(); + } + + get authorizedExecutions() { + const conditions = this.can('read', 'Execution'); + return conditions.isCreator + ? this.$relatedQuery('executions') + : Execution.query(); + } + + static async authenticate(email, password) { + const user = await User.query().findOne({ + email: email?.toLowerCase() || null, + }); + + if (user && (await user.login(password))) { + const token = await createAuthTokenByUserId(user.id); + return token; + } + } + + login(password) { + return bcrypt.compare(password, this.password); + } + + async generateResetPasswordToken() { + const resetPasswordToken = crypto.randomBytes(64).toString('hex'); + const resetPasswordTokenSentAt = new Date().toISOString(); + + await this.$query().patch({ resetPasswordToken, resetPasswordTokenSentAt }); + } + + async resetPassword(password) { + return await this.$query().patch({ + resetPasswordToken: null, + resetPasswordTokenSentAt: null, + password, + }); + } + + async isResetPasswordTokenValid() { + if (!this.resetPasswordTokenSentAt) { + return false; + } + + const sentAt = new Date(this.resetPasswordTokenSentAt); + const now = new Date(); + const fourHoursInMilliseconds = 1000 * 60 * 60 * 4; + + return now.getTime() - sentAt.getTime() < fourHoursInMilliseconds; + } + + async generateHash() { + if (this.password) { + this.password = await bcrypt.hash(this.password, 10); + } + } + + async startTrialPeriod() { + this.trialExpiryDate = DateTime.now().plus({ days: 30 }).toISODate(); + } + + async isAllowedToRunFlows() { + if (appConfig.isSelfHosted) { + return true; + } + + if (await this.inTrial()) { + return true; + } + + if ((await this.hasActiveSubscription()) && (await this.withinLimits())) { + return true; + } + + return false; + } + + async inTrial() { + if (appConfig.isSelfHosted) { + return false; + } + + if (!this.trialExpiryDate) { + return false; + } + + if (await this.hasActiveSubscription()) { + return false; + } + + const expiryDate = DateTime.fromJSDate(this.trialExpiryDate); + const now = DateTime.now(); + + return now < expiryDate; + } + + async hasActiveSubscription() { + if (!appConfig.isCloud) { + return false; + } + + const subscription = await this.$relatedQuery('currentSubscription'); + + return subscription?.isValid; + } + + async withinLimits() { + const currentSubscription = await this.$relatedQuery('currentSubscription'); + const plan = currentSubscription.plan; + const currentUsageData = await this.$relatedQuery('currentUsageData'); + + return currentUsageData.consumedTaskCount < plan.quota; + } + + async getPlanAndUsage() { + const usageData = await this.$relatedQuery( + 'currentUsageData' + ).throwIfNotFound(); + + const subscription = await this.$relatedQuery('currentSubscription'); + + const currentPlan = Billing.paddlePlans.find( + (plan) => plan.productId === subscription?.paddlePlanId + ); + + const planAndUsage = { + usage: { + task: usageData.consumedTaskCount, + }, + plan: { + id: subscription?.paddlePlanId || null, + name: subscription ? currentPlan.name : 'Free Trial', + limit: currentPlan?.limit || null, + }, + }; + + return planAndUsage; + } + + async getInvoices() { + const subscription = await this.$relatedQuery('currentSubscription'); + + if (!subscription) { + return []; + } + + const invoices = await Billing.paddleClient.getInvoices( + Number(subscription.paddleSubscriptionId) + ); + + return invoices; + } + + async getApps(name) { + const connections = await this.authorizedConnections + .clone() + .select('connections.key') + .where({ draft: false }) + .count('connections.id as count') + .groupBy('connections.key'); + + const flows = await this.authorizedFlows + .clone() + .withGraphJoined('steps') + .orderBy('created_at', 'desc'); + + const duplicatedUsedApps = flows + .map((flow) => flow.steps.map((step) => step.appKey)) + .flat() + .filter(Boolean); + + const connectionKeys = connections.map((connection) => connection.key); + const usedApps = [...new Set([...duplicatedUsedApps, ...connectionKeys])]; + + let apps = await App.findAll(name); + + apps = apps + .filter((app) => { + return usedApps.includes(app.key); + }) + .map((app) => { + const connection = connections.find( + (connection) => connection.key === app.key + ); + + app.connectionCount = connection?.count || 0; + app.flowCount = 0; + + flows.forEach((flow) => { + const usedFlow = flow.steps.find((step) => step.appKey === app.key); + + if (usedFlow) { + app.flowCount += 1; + } + }); + + return app; + }) + .sort((appA, appB) => appA.name.localeCompare(appB.name)); + + return apps; + } + + static async createAdmin({ email, password, fullName }) { + const adminRole = await Role.findAdmin(); + + const adminUser = await this.query().insert({ + email, + password, + fullName, + roleId: adminRole.id + }); + + await Config.markInstallationCompleted(); + + return adminUser; + } + + async $beforeInsert(queryContext) { + await super.$beforeInsert(queryContext); + + this.email = this.email.toLowerCase(); + await this.generateHash(); + + if (appConfig.isCloud) { + await this.startTrialPeriod(); + } + } + + async $beforeUpdate(opt, queryContext) { + await super.$beforeUpdate(opt, queryContext); + + if (this.email) { + this.email = this.email.toLowerCase(); + } + + await this.generateHash(); + } + + async $afterInsert(queryContext) { + await super.$afterInsert(queryContext); + + if (appConfig.isCloud) { + await this.$relatedQuery('usageData').insert({ + userId: this.id, + consumedTaskCount: 0, + nextResetAt: DateTime.now().plus({ days: 30 }).toISODate(), + }); + } + } + + async $afterFind() { + if (await hasValidLicense()) return this; + + if (Array.isArray(this.permissions)) { + this.permissions = this.permissions.filter((permission) => { + const restrictedSubjects = [ + 'App', + 'Role', + 'SamlAuthProvider', + 'Config', + ]; + + return !restrictedSubjects.includes(permission.subject); + }); + } + + return this; + } + + get ability() { + return userAbility(this); + } + + can(action, subject) { + const can = this.ability.can(action, subject); + + if (!can) throw new Error('Not authorized!'); + + const relevantRule = this.ability.relevantRuleFor(action, subject); + + const conditions = relevantRule?.conditions || []; + const conditionMap = Object.fromEntries( + conditions.map((condition) => [condition, true]) + ); + + return conditionMap; + } + + cannot(action, subject) { + const cannot = this.ability.cannot(action, subject); + + if (cannot) throw new Error('Not authorized!'); + + return cannot; + } +} + +export default User; diff --git a/packages/backend/src/queues/action.js b/packages/backend/src/queues/action.js new file mode 100644 index 0000000000000000000000000000000000000000..f1f4126d54b958f30b2494db998f5fe4ee2dbf5f --- /dev/null +++ b/packages/backend/src/queues/action.js @@ -0,0 +1,31 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const actionQueue = new Queue('action', redisConnection); + +process.on('SIGTERM', async () => { + await actionQueue.close(); +}); + +actionQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error('Error happened in action queue!', error); +}); + +export default actionQueue; diff --git a/packages/backend/src/queues/delete-user.ee.js b/packages/backend/src/queues/delete-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..b843750064f7e753214fd36c55ceac496040c9f8 --- /dev/null +++ b/packages/backend/src/queues/delete-user.ee.js @@ -0,0 +1,31 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const deleteUserQueue = new Queue('delete-user', redisConnection); + +process.on('SIGTERM', async () => { + await deleteUserQueue.close(); +}); + +deleteUserQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error('Error happened in delete user queue!', error); +}); + +export default deleteUserQueue; diff --git a/packages/backend/src/queues/email.js b/packages/backend/src/queues/email.js new file mode 100644 index 0000000000000000000000000000000000000000..db6eda0d16695d3c7caac4baea818afef04e838f --- /dev/null +++ b/packages/backend/src/queues/email.js @@ -0,0 +1,31 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const emailQueue = new Queue('email', redisConnection); + +process.on('SIGTERM', async () => { + await emailQueue.close(); +}); + +emailQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error('Error happened in email queue!', error); +}); + +export default emailQueue; diff --git a/packages/backend/src/queues/flow.js b/packages/backend/src/queues/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..aa4ae7130dbd517cc87897a41e69d1f9afaf0143 --- /dev/null +++ b/packages/backend/src/queues/flow.js @@ -0,0 +1,31 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const flowQueue = new Queue('flow', redisConnection); + +process.on('SIGTERM', async () => { + await flowQueue.close(); +}); + +flowQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error('Error happened in flow queue!', error); +}); + +export default flowQueue; diff --git a/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1bdddebcdd19e7a9f6e190009b0e823b0f261ae0 --- /dev/null +++ b/packages/backend/src/queues/remove-cancelled-subscriptions.ee.js @@ -0,0 +1,44 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const removeCancelledSubscriptionsQueue = new Queue( + 'remove-cancelled-subscriptions', + redisConnection +); + +process.on('SIGTERM', async () => { + await removeCancelledSubscriptionsQueue.close(); +}); + +removeCancelledSubscriptionsQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error( + 'Error happened in remove cancelled subscriptions queue!', + error + ); +}); + +removeCancelledSubscriptionsQueue.add('remove-cancelled-subscriptions', null, { + jobId: 'remove-cancelled-subscriptions', + repeat: { + pattern: '0 1 * * *', + }, +}); + +export default removeCancelledSubscriptionsQueue; diff --git a/packages/backend/src/queues/trigger.js b/packages/backend/src/queues/trigger.js new file mode 100644 index 0000000000000000000000000000000000000000..66a3d9ec724a541a34a9d38a7abc5140e5ab7511 --- /dev/null +++ b/packages/backend/src/queues/trigger.js @@ -0,0 +1,31 @@ +import process from 'process'; +import { Queue } from 'bullmq'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; + +const CONNECTION_REFUSED = 'ECONNREFUSED'; + +const redisConnection = { + connection: redisConfig, +}; + +const triggerQueue = new Queue('trigger', redisConnection); + +process.on('SIGTERM', async () => { + await triggerQueue.close(); +}); + +triggerQueue.on('error', (error) => { + if (error.code === CONNECTION_REFUSED) { + logger.error( + 'Make sure you have installed Redis and it is running.', + error + ); + + process.exit(); + } + + logger.error('Error happened in trigger queue!', error); +}); + +export default triggerQueue; diff --git a/packages/backend/src/routes/api/v1/access-tokens.js b/packages/backend/src/routes/api/v1/access-tokens.js new file mode 100644 index 0000000000000000000000000000000000000000..b41c3e0fb3745bf495ae4fedb79a1e949f63183a --- /dev/null +++ b/packages/backend/src/routes/api/v1/access-tokens.js @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import createAccessTokenAction from '../../../controllers/api/v1/access-tokens/create-access-token.js'; +import revokeAccessTokenAction from '../../../controllers/api/v1/access-tokens/revoke-access-token.js'; +import { authenticateUser } from '../../../helpers/authentication.js'; +const router = Router(); + +router.post('/', asyncHandler(createAccessTokenAction)); + +router.delete( + '/:token', + authenticateUser, + asyncHandler(revokeAccessTokenAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/admin/apps.ee.js b/packages/backend/src/routes/api/v1/admin/apps.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..ef3d97c52f3cd320e14c5b520e5d59c2f60fab45 --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/apps.ee.js @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import getAuthClientsAction from '../../../../controllers/api/v1/admin/apps/get-auth-clients.ee.js'; +import getAuthClientAction from '../../../../controllers/api/v1/admin/apps/get-auth-client.ee.js'; + +const router = Router(); + +router.get( + '/:appKey/auth-clients', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getAuthClientsAction) +); + +router.get( + '/:appKey/auth-clients/:appAuthClientId', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getAuthClientAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/admin/permissions.ee.js b/packages/backend/src/routes/api/v1/admin/permissions.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1fda9ad54b740274ed1d6b751378ac3709119fe5 --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/permissions.ee.js @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import getPermissionsCatalogAction from '../../../../controllers/api/v1/admin/permissions/get-permissions-catalog.ee.js'; + +const router = Router(); + +router.get( + '/catalog', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getPermissionsCatalogAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/admin/roles.ee.js b/packages/backend/src/routes/api/v1/admin/roles.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..fff16023486fb60b854f011fd702921325ebee8c --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/roles.ee.js @@ -0,0 +1,27 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import getRolesAction from '../../../../controllers/api/v1/admin/roles/get-roles.ee.js'; +import getRoleAction from '../../../../controllers/api/v1/admin/roles/get-role.ee.js'; + +const router = Router(); + +router.get( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getRolesAction) +); + +router.get( + '/:roleId', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getRoleAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js b/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..586f4a6f6fa1755347edf52fd8f603db9a4494ce --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/saml-auth-providers.ee.js @@ -0,0 +1,36 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../../helpers/check-is-enterprise.js'; +import getSamlAuthProvidersAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js'; +import getSamlAuthProviderAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js'; +import getRoleMappingsAction from '../../../../controllers/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js'; + +const router = Router(); + +router.get( + '/', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getSamlAuthProvidersAction) +); + +router.get( + '/:samlAuthProviderId', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getSamlAuthProviderAction) +); + +router.get( + '/:samlAuthProviderId/role-mappings', + authenticateUser, + authorizeAdmin, + checkIsEnterprise, + asyncHandler(getRoleMappingsAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/admin/users.ee.js b/packages/backend/src/routes/api/v1/admin/users.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..9752c6a097838c5f0da1df6753a2e6b1e59df037 --- /dev/null +++ b/packages/backend/src/routes/api/v1/admin/users.ee.js @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../../helpers/authentication.js'; +import { authorizeAdmin } from '../../../../helpers/authorization.js'; +import getUsersAction from '../../../../controllers/api/v1/admin/users/get-users.ee.js'; +import getUserAction from '../../../../controllers/api/v1/admin/users/get-user.ee.js'; + +const router = Router(); + +router.get('/', authenticateUser, authorizeAdmin, asyncHandler(getUsersAction)); + +router.get( + '/:userId', + authenticateUser, + authorizeAdmin, + asyncHandler(getUserAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/apps.js b/packages/backend/src/routes/api/v1/apps.js new file mode 100644 index 0000000000000000000000000000000000000000..349719664e4ba27b4de2bb5a4383f85f4a04ba46 --- /dev/null +++ b/packages/backend/src/routes/api/v1/apps.js @@ -0,0 +1,84 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; +import getAppAction from '../../../controllers/api/v1/apps/get-app.js'; +import getAppsAction from '../../../controllers/api/v1/apps/get-apps.js'; +import getAuthAction from '../../../controllers/api/v1/apps/get-auth.js'; +import getConnectionsAction from '../../../controllers/api/v1/apps/get-connections.js'; +import getConfigAction from '../../../controllers/api/v1/apps/get-config.ee.js'; +import getAuthClientsAction from '../../../controllers/api/v1/apps/get-auth-clients.ee.js'; +import getAuthClientAction from '../../../controllers/api/v1/apps/get-auth-client.ee.js'; +import getTriggersAction from '../../../controllers/api/v1/apps/get-triggers.js'; +import getTriggerSubstepsAction from '../../../controllers/api/v1/apps/get-trigger-substeps.js'; +import getActionsAction from '../../../controllers/api/v1/apps/get-actions.js'; +import getActionSubstepsAction from '../../../controllers/api/v1/apps/get-action-substeps.js'; +import getFlowsAction from '../../../controllers/api/v1/apps/get-flows.js'; + +const router = Router(); + +router.get('/', authenticateUser, asyncHandler(getAppsAction)); +router.get('/:appKey', authenticateUser, asyncHandler(getAppAction)); +router.get('/:appKey/auth', authenticateUser, asyncHandler(getAuthAction)); + +router.get( + '/:appKey/connections', + authenticateUser, + authorizeUser, + asyncHandler(getConnectionsAction) +); + +router.get( + '/:appKey/config', + authenticateUser, + checkIsEnterprise, + asyncHandler(getConfigAction) +); + +router.get( + '/:appKey/auth-clients', + authenticateUser, + checkIsEnterprise, + asyncHandler(getAuthClientsAction) +); + +router.get( + '/:appKey/auth-clients/:appAuthClientId', + authenticateUser, + checkIsEnterprise, + asyncHandler(getAuthClientAction) +); + +router.get( + '/:appKey/triggers', + authenticateUser, + asyncHandler(getTriggersAction) +); + +router.get( + '/:appKey/triggers/:triggerKey/substeps', + authenticateUser, + asyncHandler(getTriggerSubstepsAction) +); + +router.get( + '/:appKey/actions', + authenticateUser, + asyncHandler(getActionsAction) +); + +router.get( + '/:appKey/actions/:actionKey/substeps', + authenticateUser, + asyncHandler(getActionSubstepsAction) +); + +router.get( + '/:appKey/flows', + authenticateUser, + authorizeUser, + asyncHandler(getFlowsAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/automatisch.js b/packages/backend/src/routes/api/v1/automatisch.js new file mode 100644 index 0000000000000000000000000000000000000000..da838c24f9a4395c22dca1e7cc2bd58f7bb42fc2 --- /dev/null +++ b/packages/backend/src/routes/api/v1/automatisch.js @@ -0,0 +1,18 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; +import versionAction from '../../../controllers/api/v1/automatisch/version.js'; +import notificationsAction from '../../../controllers/api/v1/automatisch/notifications.js'; +import infoAction from '../../../controllers/api/v1/automatisch/info.js'; +import licenseAction from '../../../controllers/api/v1/automatisch/license.js'; +import configAction from '../../../controllers/api/v1/automatisch/config.ee.js'; + +const router = Router(); + +router.get('/version', asyncHandler(versionAction)); +router.get('/notifications', asyncHandler(notificationsAction)); +router.get('/info', asyncHandler(infoAction)); +router.get('/license', asyncHandler(licenseAction)); +router.get('/config', checkIsEnterprise, asyncHandler(configAction)); + +export default router; diff --git a/packages/backend/src/routes/api/v1/connections.js b/packages/backend/src/routes/api/v1/connections.js new file mode 100644 index 0000000000000000000000000000000000000000..223f5a0548cdabf5b89f1d3f9c8550d6375cdf74 --- /dev/null +++ b/packages/backend/src/routes/api/v1/connections.js @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getFlowsAction from '../../../controllers/api/v1/connections/get-flows.js'; +import createTestAction from '../../../controllers/api/v1/connections/create-test.js'; + +const router = Router(); + +router.get( + '/:connectionId/flows', + authenticateUser, + authorizeUser, + asyncHandler(getFlowsAction) +); + +router.post( + '/:connectionId/test', + authenticateUser, + authorizeUser, + asyncHandler(createTestAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/executions.js b/packages/backend/src/routes/api/v1/executions.js new file mode 100644 index 0000000000000000000000000000000000000000..769b29b610d0a647402decfca4453d57916bea6b --- /dev/null +++ b/packages/backend/src/routes/api/v1/executions.js @@ -0,0 +1,32 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getExecutionsAction from '../../../controllers/api/v1/executions/get-executions.js'; +import getExecutionAction from '../../../controllers/api/v1/executions/get-execution.js'; +import getExecutionStepsAction from '../../../controllers/api/v1/executions/get-execution-steps.js'; + +const router = Router(); + +router.get( + '/', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionsAction) +); + +router.get( + '/:executionId', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionAction) +); + +router.get( + '/:executionId/execution-steps', + authenticateUser, + authorizeUser, + asyncHandler(getExecutionStepsAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/flows.js b/packages/backend/src/routes/api/v1/flows.js new file mode 100644 index 0000000000000000000000000000000000000000..955d638eaa4cb8117c80a3cc86a723afa25fd0f1 --- /dev/null +++ b/packages/backend/src/routes/api/v1/flows.js @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getFlowsAction from '../../../controllers/api/v1/flows/get-flows.js'; +import getFlowAction from '../../../controllers/api/v1/flows/get-flow.js'; + +const router = Router(); + +router.get('/', authenticateUser, authorizeUser, asyncHandler(getFlowsAction)); + +router.get( + '/:flowId', + authenticateUser, + authorizeUser, + asyncHandler(getFlowAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/installation/users.js b/packages/backend/src/routes/api/v1/installation/users.js new file mode 100644 index 0000000000000000000000000000000000000000..9a3c8fd7d74554ae6ae58e7a739be6ae8f9c2f7b --- /dev/null +++ b/packages/backend/src/routes/api/v1/installation/users.js @@ -0,0 +1,14 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { allowInstallation } from '../../../../helpers/allow-installation.js'; +import createUserAction from '../../../../controllers/api/v1/installation/users/create-user.js'; + +const router = Router(); + +router.post( + '/', + allowInstallation, + asyncHandler(createUserAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/payment.ee.js b/packages/backend/src/routes/api/v1/payment.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..6310f66dd443e3e95a9b8127098d2dd8fbbf7976 --- /dev/null +++ b/packages/backend/src/routes/api/v1/payment.ee.js @@ -0,0 +1,24 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import checkIsCloud from '../../../helpers/check-is-cloud.js'; +import getPlansAction from '../../../controllers/api/v1/payment/get-plans.ee.js'; +import getPaddleInfoAction from '../../../controllers/api/v1/payment/get-paddle-info.ee.js'; + +const router = Router(); + +router.get( + '/plans', + authenticateUser, + checkIsCloud, + asyncHandler(getPlansAction) +); + +router.get( + '/paddle-info', + authenticateUser, + checkIsCloud, + asyncHandler(getPaddleInfoAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js b/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..3b067481d194c78f452b4aa1150bf5008c65614c --- /dev/null +++ b/packages/backend/src/routes/api/v1/saml-auth-providers.ee.js @@ -0,0 +1,10 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { checkIsEnterprise } from '../../../helpers/check-is-enterprise.js'; +import getSamlAuthProvidersAction from '../../../controllers/api/v1/saml-auth-providers/get-saml-auth-providers.ee.js'; + +const router = Router(); + +router.get('/', checkIsEnterprise, asyncHandler(getSamlAuthProvidersAction)); + +export default router; diff --git a/packages/backend/src/routes/api/v1/steps.js b/packages/backend/src/routes/api/v1/steps.js new file mode 100644 index 0000000000000000000000000000000000000000..e65944835c0fbff3a76f59cdda62e2f39b65c6e3 --- /dev/null +++ b/packages/backend/src/routes/api/v1/steps.js @@ -0,0 +1,40 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import getConnectionAction from '../../../controllers/api/v1/steps/get-connection.js'; +import getPreviousStepsAction from '../../../controllers/api/v1/steps/get-previous-steps.js'; +import createDynamicFieldsAction from '../../../controllers/api/v1/steps/create-dynamic-fields.js'; +import createDynamicDataAction from '../../../controllers/api/v1/steps/create-dynamic-data.js'; + +const router = Router(); + +router.get( + '/:stepId/connection', + authenticateUser, + authorizeUser, + asyncHandler(getConnectionAction) +); + +router.get( + '/:stepId/previous-steps', + authenticateUser, + authorizeUser, + asyncHandler(getPreviousStepsAction) +); + +router.post( + '/:stepId/dynamic-fields', + authenticateUser, + authorizeUser, + asyncHandler(createDynamicFieldsAction) +); + +router.post( + '/:stepId/dynamic-data', + authenticateUser, + authorizeUser, + asyncHandler(createDynamicDataAction) +); + +export default router; diff --git a/packages/backend/src/routes/api/v1/users.js b/packages/backend/src/routes/api/v1/users.js new file mode 100644 index 0000000000000000000000000000000000000000..2755c3b67c9934de44e138d1b757c6ad563e4da6 --- /dev/null +++ b/packages/backend/src/routes/api/v1/users.js @@ -0,0 +1,52 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import { authenticateUser } from '../../../helpers/authentication.js'; +import { authorizeUser } from '../../../helpers/authorization.js'; +import checkIsCloud from '../../../helpers/check-is-cloud.js'; +import getCurrentUserAction from '../../../controllers/api/v1/users/get-current-user.js'; +import getUserTrialAction from '../../../controllers/api/v1/users/get-user-trial.ee.js'; +import getAppsAction from '../../../controllers/api/v1/users/get-apps.js'; +import getInvoicesAction from '../../../controllers/api/v1/users/get-invoices.ee.js'; +import getSubscriptionAction from '../../../controllers/api/v1/users/get-subscription.ee.js'; +import getPlanAndUsageAction from '../../../controllers/api/v1/users/get-plan-and-usage.ee.js'; + +const router = Router(); + +router.get('/me', authenticateUser, asyncHandler(getCurrentUserAction)); + +router.get( + '/:userId/apps', + authenticateUser, + authorizeUser, + asyncHandler(getAppsAction) +); + +router.get( + '/invoices', + authenticateUser, + checkIsCloud, + asyncHandler(getInvoicesAction) +); + +router.get( + '/:userId/trial', + authenticateUser, + checkIsCloud, + asyncHandler(getUserTrialAction) +); + +router.get( + '/:userId/subscription', + authenticateUser, + checkIsCloud, + asyncHandler(getSubscriptionAction) +); + +router.get( + '/:userId/plan-and-usage', + authenticateUser, + checkIsCloud, + asyncHandler(getPlanAndUsageAction) +); + +export default router; diff --git a/packages/backend/src/routes/healthcheck.js b/packages/backend/src/routes/healthcheck.js new file mode 100644 index 0000000000000000000000000000000000000000..81fade31fa0ec45b049cf44ceac3e65f515f2f82 --- /dev/null +++ b/packages/backend/src/routes/healthcheck.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import indexAction from '../controllers/healthcheck/index.js'; + +const router = Router(); + +router.get('/', asyncHandler(indexAction)); + +export default router; diff --git a/packages/backend/src/routes/index.js b/packages/backend/src/routes/index.js new file mode 100644 index 0000000000000000000000000000000000000000..c67a9cf33335a271d38f5043ced088dbfb52e7ab --- /dev/null +++ b/packages/backend/src/routes/index.js @@ -0,0 +1,47 @@ +import { Router } from 'express'; +import graphQLInstance from '../helpers/graphql-instance.js'; +import webhooksRouter from './webhooks.js'; +import paddleRouter from './paddle.ee.js'; +import healthcheckRouter from './healthcheck.js'; +import automatischRouter from './api/v1/automatisch.js'; +import accessTokensRouter from './api/v1/access-tokens.js'; +import usersRouter from './api/v1/users.js'; +import paymentRouter from './api/v1/payment.ee.js'; +import flowsRouter from './api/v1/flows.js'; +import stepsRouter from './api/v1/steps.js'; +import appsRouter from './api/v1/apps.js'; +import connectionsRouter from './api/v1/connections.js'; +import executionsRouter from './api/v1/executions.js'; +import samlAuthProvidersRouter from './api/v1/saml-auth-providers.ee.js'; +import adminAppsRouter from './api/v1/admin/apps.ee.js'; +import adminSamlAuthProvidersRouter from './api/v1/admin/saml-auth-providers.ee.js'; +import rolesRouter from './api/v1/admin/roles.ee.js'; +import permissionsRouter from './api/v1/admin/permissions.ee.js'; +import adminUsersRouter from './api/v1/admin/users.ee.js'; +import installationUsersRouter from './api/v1/installation/users.js'; + +const router = Router(); + +router.use('/graphql', graphQLInstance); +router.use('/webhooks', webhooksRouter); +router.use('/paddle', paddleRouter); +router.use('/healthcheck', healthcheckRouter); +router.use('/api/v1/automatisch', automatischRouter); +router.use('/api/v1/access-tokens', accessTokensRouter); +router.use('/api/v1/users', usersRouter); +router.use('/api/v1/payment', paymentRouter); +router.use('/api/v1/apps', appsRouter); +router.use('/api/v1/connections', connectionsRouter); +router.use('/api/v1/flows', flowsRouter); +router.use('/api/v1/steps', stepsRouter); +router.use('/api/v1/executions', executionsRouter); +router.use('/api/v1/saml-auth-providers', samlAuthProvidersRouter); +router.use('/api/v1/admin/apps', adminAppsRouter); +router.use('/api/v1/admin/users', adminUsersRouter); +router.use('/api/v1/admin/roles', rolesRouter); +router.use('/api/v1/admin/permissions', permissionsRouter); +router.use('/api/v1/admin/saml-auth-providers', adminSamlAuthProvidersRouter); +router.use('/api/v1/installation/users', installationUsersRouter); + + +export default router; diff --git a/packages/backend/src/routes/paddle.ee.js b/packages/backend/src/routes/paddle.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..c82b2e07d137b6d8326dd33e19e6d9f3e19b306a --- /dev/null +++ b/packages/backend/src/routes/paddle.ee.js @@ -0,0 +1,9 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import webhooksHandler from '../controllers/paddle/webhooks.ee.js'; + +const router = Router(); + +router.post('/webhooks', asyncHandler(webhooksHandler)); + +export default router; diff --git a/packages/backend/src/routes/webhooks.js b/packages/backend/src/routes/webhooks.js new file mode 100644 index 0000000000000000000000000000000000000000..98cadef01a776df40f4210cfcca3db2bd6973407 --- /dev/null +++ b/packages/backend/src/routes/webhooks.js @@ -0,0 +1,54 @@ +import express, { Router } from 'express'; +import multer from 'multer'; + +import appConfig from '../config/app.js'; +import webhookHandlerByFlowId from '../controllers/webhooks/handler-by-flow-id.js'; +import webhookHandlerSyncByFlowId from '../controllers/webhooks/handler-sync-by-flow-id.js'; +import webhookHandlerByConnectionIdAndRefValue from '../controllers/webhooks/handler-by-connection-id-and-ref-value.js'; + +const router = Router(); +const upload = multer(); + +router.use(upload.none()); + +router.use( + express.text({ + limit: appConfig.requestBodySizeLimit, + verify(req, res, buf) { + req.rawBody = buf; + }, + }) +); + +const exposeError = (handler) => async (req, res, next) => { + try { + await handler(req, res, next); + } catch (err) { + next(err); + } +}; + +function createRouteHandler(path, handler) { + const wrappedHandler = exposeError(handler); + + router + .route(path) + .get(wrappedHandler) + .put(wrappedHandler) + .patch(wrappedHandler) + .post(wrappedHandler); +} + +createRouteHandler( + '/connections/:connectionId/:refValue', + webhookHandlerByConnectionIdAndRefValue +); +createRouteHandler( + '/connections/:connectionId', + webhookHandlerByConnectionIdAndRefValue +); +createRouteHandler('/flows/:flowId/sync', webhookHandlerSyncByFlowId); +createRouteHandler('/flows/:flowId', webhookHandlerByFlowId); +createRouteHandler('/:flowId', webhookHandlerByFlowId); + +export default router; diff --git a/packages/backend/src/serializers/action.js b/packages/backend/src/serializers/action.js new file mode 100644 index 0000000000000000000000000000000000000000..cad8ac5d1758b3bc94ce3385eee203ee8bc71657 --- /dev/null +++ b/packages/backend/src/serializers/action.js @@ -0,0 +1,9 @@ +const actionSerializer = (action) => { + return { + name: action.name, + key: action.key, + description: action.description, + }; +}; + +export default actionSerializer; diff --git a/packages/backend/src/serializers/action.test.js b/packages/backend/src/serializers/action.test.js new file mode 100644 index 0000000000000000000000000000000000000000..32b6b81bed2adaaedd8fd062792534d9a91ac92d --- /dev/null +++ b/packages/backend/src/serializers/action.test.js @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import App from '../models/app'; +import actionSerializer from './action'; + +describe('actionSerializer', () => { + it('should return the action data', async () => { + const actions = await App.findActionsByKey('github'); + const action = actions[0]; + + const expectedPayload = { + description: action.description, + key: action.key, + name: action.name, + pollInterval: action.pollInterval, + showWebhookUrl: action.showWebhookUrl, + type: action.type, + }; + + expect(actionSerializer(action)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/admin-saml-auth-provider.ee.js b/packages/backend/src/serializers/admin-saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..37d442a56802ca63e162f8f1cc091d006fb86d0f --- /dev/null +++ b/packages/backend/src/serializers/admin-saml-auth-provider.ee.js @@ -0,0 +1,18 @@ +const adminSamlAuthProviderSerializer = (samlAuthProvider) => { + return { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + certificate: samlAuthProvider.certificate, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + issuer: samlAuthProvider.issuer, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + emailAttributeName: samlAuthProvider.emailAttributeName, + roleAttributeName: samlAuthProvider.roleAttributeName, + active: samlAuthProvider.active, + defaultRoleId: samlAuthProvider.defaultRoleId, + }; +}; + +export default adminSamlAuthProviderSerializer; diff --git a/packages/backend/src/serializers/admin-saml-auth-provider.ee.test.js b/packages/backend/src/serializers/admin-saml-auth-provider.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..0ad55c81c0de879be2f908783a76c42a2b91f104 --- /dev/null +++ b/packages/backend/src/serializers/admin-saml-auth-provider.ee.test.js @@ -0,0 +1,32 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js'; +import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js'; + +describe('adminSamlAuthProviderSerializer', () => { + let samlAuthProvider; + + beforeEach(async () => { + samlAuthProvider = await createSamlAuthProvider(); + }); + + it('should return saml auth provider data', async () => { + const expectedPayload = { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + certificate: samlAuthProvider.certificate, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + issuer: samlAuthProvider.issuer, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + emailAttributeName: samlAuthProvider.emailAttributeName, + roleAttributeName: samlAuthProvider.roleAttributeName, + active: samlAuthProvider.active, + defaultRoleId: samlAuthProvider.defaultRoleId, + }; + + expect(adminSamlAuthProviderSerializer(samlAuthProvider)).toEqual( + expectedPayload + ); + }); +}); diff --git a/packages/backend/src/serializers/app-auth-client.js b/packages/backend/src/serializers/app-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..88af3dab915c5bbb5f3a81186e631a821ac2ff88 --- /dev/null +++ b/packages/backend/src/serializers/app-auth-client.js @@ -0,0 +1,10 @@ +const appAuthClientSerializer = (appAuthClient) => { + return { + id: appAuthClient.id, + appConfigId: appAuthClient.appConfigId, + name: appAuthClient.name, + active: appAuthClient.active, + }; +}; + +export default appAuthClientSerializer; diff --git a/packages/backend/src/serializers/app-auth-client.test.js b/packages/backend/src/serializers/app-auth-client.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7352f70d7510e131bf599426ee6bd2b77b13d56f --- /dev/null +++ b/packages/backend/src/serializers/app-auth-client.test.js @@ -0,0 +1,22 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createAppAuthClient } from '../../test/factories/app-auth-client'; +import appAuthClientSerializer from './app-auth-client'; + +describe('appAuthClient serializer', () => { + let appAuthClient; + + beforeEach(async () => { + appAuthClient = await createAppAuthClient(); + }); + + it('should return app auth client data', async () => { + const expectedPayload = { + id: appAuthClient.id, + appConfigId: appAuthClient.appConfigId, + name: appAuthClient.name, + active: appAuthClient.active, + }; + + expect(appAuthClientSerializer(appAuthClient)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/app-config.js b/packages/backend/src/serializers/app-config.js new file mode 100644 index 0000000000000000000000000000000000000000..0efd562b6f7e7124ea258ac0b6077598c0f8cbcd --- /dev/null +++ b/packages/backend/src/serializers/app-config.js @@ -0,0 +1,15 @@ +const appConfigSerializer = (appConfig) => { + return { + id: appConfig.id, + key: appConfig.key, + allowCustomConnection: appConfig.allowCustomConnection, + shared: appConfig.shared, + disabled: appConfig.disabled, + canConnect: appConfig.canConnect, + canCustomConnect: appConfig.canCustomConnect, + createdAt: appConfig.createdAt.getTime(), + updatedAt: appConfig.updatedAt.getTime(), + }; +}; + +export default appConfigSerializer; diff --git a/packages/backend/src/serializers/app-config.test.js b/packages/backend/src/serializers/app-config.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c0ede9184a8cfcf73928376b856712670222928d --- /dev/null +++ b/packages/backend/src/serializers/app-config.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createAppConfig } from '../../test/factories/app-config'; +import appConfigSerializer from './app-config'; + +describe('appConfig serializer', () => { + let appConfig; + + beforeEach(async () => { + appConfig = await createAppConfig(); + }); + + it('should return app config data', async () => { + const expectedPayload = { + id: appConfig.id, + key: appConfig.key, + allowCustomConnection: appConfig.allowCustomConnection, + shared: appConfig.shared, + disabled: appConfig.disabled, + canConnect: appConfig.canConnect, + canCustomConnect: appConfig.canCustomConnect, + createdAt: appConfig.createdAt.getTime(), + updatedAt: appConfig.updatedAt.getTime(), + }; + + expect(appConfigSerializer(appConfig)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/app.js b/packages/backend/src/serializers/app.js new file mode 100644 index 0000000000000000000000000000000000000000..57e0306dbe618b07f624bdc6928af932888e4dcb --- /dev/null +++ b/packages/backend/src/serializers/app.js @@ -0,0 +1,22 @@ +const appSerializer = (app) => { + let appData = { + key: app.key, + name: app.name, + iconUrl: app.iconUrl, + primaryColor: app.primaryColor, + authDocUrl: app.authDocUrl, + supportsConnections: app.supportsConnections, + }; + + if (app.connectionCount) { + appData.connectionCount = app.connectionCount; + } + + if (app.flowCount) { + appData.flowCount = app.flowCount; + } + + return appData; +}; + +export default appSerializer; diff --git a/packages/backend/src/serializers/app.test.js b/packages/backend/src/serializers/app.test.js new file mode 100644 index 0000000000000000000000000000000000000000..c2714dc2c96ee5caa76e784575cac468bb5f7886 --- /dev/null +++ b/packages/backend/src/serializers/app.test.js @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest'; +import App from '../models/app'; +import appSerializer from './app'; + +describe('appSerializer', () => { + it('should return app data', async () => { + const app = await App.findOneByKey('deepl'); + + const expectedPayload = { + name: app.name, + key: app.key, + iconUrl: app.iconUrl, + authDocUrl: app.authDocUrl, + supportsConnections: app.supportsConnections, + primaryColor: app.primaryColor, + }; + + expect(appSerializer(app)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/auth.js b/packages/backend/src/serializers/auth.js new file mode 100644 index 0000000000000000000000000000000000000000..c5d60a4eef3487469a48fc8be5707ac5fc977ecb --- /dev/null +++ b/packages/backend/src/serializers/auth.js @@ -0,0 +1,9 @@ +const authSerializer = (auth) => { + return { + fields: auth.fields, + authenticationSteps: auth.authenticationSteps, + reconnectionSteps: auth.reconnectionSteps, + }; +}; + +export default authSerializer; diff --git a/packages/backend/src/serializers/auth.test.js b/packages/backend/src/serializers/auth.test.js new file mode 100644 index 0000000000000000000000000000000000000000..6746c5597ed2e9ddd65ce5db49f1710f2e8bc3b1 --- /dev/null +++ b/packages/backend/src/serializers/auth.test.js @@ -0,0 +1,17 @@ +import { describe, it, expect } from 'vitest'; +import App from '../models/app'; +import authSerializer from './auth'; + +describe('authSerializer', () => { + it('should return auth data', async () => { + const auth = await App.findAuthByKey('deepl'); + + const expectedPayload = { + fields: auth.fields, + authenticationSteps: auth.authenticationSteps, + reconnectionSteps: auth.reconnectionSteps, + }; + + expect(authSerializer(auth)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/connection.js b/packages/backend/src/serializers/connection.js new file mode 100644 index 0000000000000000000000000000000000000000..e285f1e2438e0e4a4fde4a5923bdc6fa840f0786 --- /dev/null +++ b/packages/backend/src/serializers/connection.js @@ -0,0 +1,16 @@ +const connectionSerializer = (connection) => { + return { + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + verified: connection.verified, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; +}; + +export default connectionSerializer; diff --git a/packages/backend/src/serializers/connection.test.js b/packages/backend/src/serializers/connection.test.js new file mode 100644 index 0000000000000000000000000000000000000000..2a4df3c34f3419d5de549a8ec7b61a4317a1fbdc --- /dev/null +++ b/packages/backend/src/serializers/connection.test.js @@ -0,0 +1,28 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createConnection } from '../../test/factories/connection'; +import connectionSerializer from './connection'; + +describe('connectionSerializer', () => { + let connection; + + beforeEach(async () => { + connection = await createConnection(); + }); + + it('should return connection data', async () => { + const expectedPayload = { + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + verified: connection.verified, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + expect(connectionSerializer(connection)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/execution-step.js b/packages/backend/src/serializers/execution-step.js new file mode 100644 index 0000000000000000000000000000000000000000..52d7a4097b838d746e3a19ccf84f599cffdcea03 --- /dev/null +++ b/packages/backend/src/serializers/execution-step.js @@ -0,0 +1,21 @@ +import stepSerializer from './step.js'; + +const executionStepSerializer = (executionStep) => { + let executionStepData = { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + }; + + if (executionStep.step) { + executionStepData.step = stepSerializer(executionStep.step); + } + + return executionStepData; +}; + +export default executionStepSerializer; diff --git a/packages/backend/src/serializers/execution-step.test.js b/packages/backend/src/serializers/execution-step.test.js new file mode 100644 index 0000000000000000000000000000000000000000..037ccbcd597880b7bdf80836ea15c3d10512ffd8 --- /dev/null +++ b/packages/backend/src/serializers/execution-step.test.js @@ -0,0 +1,43 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import executionStepSerializer from './execution-step'; +import stepSerializer from './step'; +import { createExecutionStep } from '../../test/factories/execution-step'; +import { createStep } from '../../test/factories/step'; + +describe('executionStepSerializer', () => { + let executionStep, step; + + beforeEach(async () => { + step = await createStep(); + + executionStep = await createExecutionStep({ + stepId: step.id, + }); + }); + + it('should return the execution step data', async () => { + const expectedPayload = { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + }; + + expect(executionStepSerializer(executionStep)).toEqual(expectedPayload); + }); + + it('should return the execution step data with the step', async () => { + executionStep.step = step; + + const expectedPayload = { + step: stepSerializer(step), + }; + + expect(executionStepSerializer(executionStep)).toMatchObject( + expectedPayload + ); + }); +}); diff --git a/packages/backend/src/serializers/execution.js b/packages/backend/src/serializers/execution.js new file mode 100644 index 0000000000000000000000000000000000000000..db57d159ccc2f86293158912bfa958f5e5a08770 --- /dev/null +++ b/packages/backend/src/serializers/execution.js @@ -0,0 +1,22 @@ +import flowSerializer from './flow.js'; + +const executionSerializer = (execution) => { + let executionData = { + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + }; + + if (execution.status) { + executionData.status = execution.status; + } + + if (execution.flow) { + executionData.flow = flowSerializer(execution.flow); + } + + return executionData; +}; + +export default executionSerializer; diff --git a/packages/backend/src/serializers/execution.test.js b/packages/backend/src/serializers/execution.test.js new file mode 100644 index 0000000000000000000000000000000000000000..5711b0ea32c13e0b86b58f85738dbadea57ae7e9 --- /dev/null +++ b/packages/backend/src/serializers/execution.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import executionSerializer from './execution'; +import flowSerializer from './flow'; +import { createExecution } from '../../test/factories/execution'; +import { createFlow } from '../../test/factories/flow'; + +describe('executionSerializer', () => { + let flow, execution; + + beforeEach(async () => { + flow = await createFlow(); + + execution = await createExecution({ + flowId: flow.id, + }); + }); + + it('should return the execution data', async () => { + const expectedPayload = { + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + }; + + expect(executionSerializer(execution)).toEqual(expectedPayload); + }); + + it('should return the execution data with status', async () => { + execution.status = 'success'; + + const expectedPayload = { + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + status: 'success', + }; + + expect(executionSerializer(execution)).toEqual(expectedPayload); + }); + + it('should return the execution data with the flow', async () => { + execution.flow = flow; + + const expectedPayload = { + flow: flowSerializer(flow), + }; + + expect(executionSerializer(execution)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/flow.js b/packages/backend/src/serializers/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..04adc112c5806342ea874a6fce1414dca6e8ad51 --- /dev/null +++ b/packages/backend/src/serializers/flow.js @@ -0,0 +1,20 @@ +import stepSerializer from './step.js'; + +const flowSerializer = (flow) => { + let flowData = { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.status, + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + }; + + if (flow.steps?.length > 0) { + flowData.steps = flow.steps.map((step) => stepSerializer(step)); + } + + return flowData; +}; + +export default flowSerializer; diff --git a/packages/backend/src/serializers/flow.test.js b/packages/backend/src/serializers/flow.test.js new file mode 100644 index 0000000000000000000000000000000000000000..65dc939940cf137b1cd0e743a110e1334e858b43 --- /dev/null +++ b/packages/backend/src/serializers/flow.test.js @@ -0,0 +1,46 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createFlow } from '../../test/factories/flow'; +import flowSerializer from './flow'; +import stepSerializer from './step'; +import { createStep } from '../../test/factories/step'; + +describe('flowSerializer', () => { + let flow, stepOne, stepTwo; + + beforeEach(async () => { + flow = await createFlow(); + + stepOne = await createStep({ + flowId: flow.id, + type: 'trigger', + }); + + stepTwo = await createStep({ + flowId: flow.id, + type: 'action', + }); + }); + + it('should return flow data', async () => { + const expectedPayload = { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.status, + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + }; + + expect(flowSerializer(flow)).toEqual(expectedPayload); + }); + + it('should return flow data with the steps', async () => { + flow.steps = [stepOne, stepTwo]; + + const expectedPayload = { + steps: [stepSerializer(stepOne), stepSerializer(stepTwo)], + }; + + expect(flowSerializer(flow)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/index.js b/packages/backend/src/serializers/index.js new file mode 100644 index 0000000000000000000000000000000000000000..28b98e1eaf05d6215aa4b4ca2a5e7e8f9445e4ff --- /dev/null +++ b/packages/backend/src/serializers/index.js @@ -0,0 +1,41 @@ +import userSerializer from './user.js'; +import roleSerializer from './role.js'; +import permissionSerializer from './permission.js'; +import adminSamlAuthProviderSerializer from './admin-saml-auth-provider.ee.js'; +import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; +import samlAuthProviderRoleMappingSerializer from './role-mapping.ee.js'; +import appAuthClientSerializer from './app-auth-client.js'; +import appConfigSerializer from './app-config.js'; +import flowSerializer from './flow.js'; +import stepSerializer from './step.js'; +import connectionSerializer from './connection.js'; +import appSerializer from './app.js'; +import authSerializer from './auth.js'; +import triggerSerializer from './trigger.js'; +import actionSerializer from './action.js'; +import executionSerializer from './execution.js'; +import executionStepSerializer from './execution-step.js'; +import subscriptionSerializer from './subscription.ee.js'; + +const serializers = { + User: userSerializer, + Role: roleSerializer, + Permission: permissionSerializer, + AdminSamlAuthProvider: adminSamlAuthProviderSerializer, + SamlAuthProvider: samlAuthProviderSerializer, + SamlAuthProvidersRoleMapping: samlAuthProviderRoleMappingSerializer, + AppAuthClient: appAuthClientSerializer, + AppConfig: appConfigSerializer, + Flow: flowSerializer, + Step: stepSerializer, + Connection: connectionSerializer, + App: appSerializer, + Auth: authSerializer, + Trigger: triggerSerializer, + Action: actionSerializer, + Execution: executionSerializer, + ExecutionStep: executionStepSerializer, + Subscription: subscriptionSerializer, +}; + +export default serializers; diff --git a/packages/backend/src/serializers/permission.js b/packages/backend/src/serializers/permission.js new file mode 100644 index 0000000000000000000000000000000000000000..07b820209d4948d71a6d0a70a044dc8148dc2484 --- /dev/null +++ b/packages/backend/src/serializers/permission.js @@ -0,0 +1,13 @@ +const permissionSerializer = (permission) => { + return { + id: permission.id, + roleId: permission.roleId, + action: permission.action, + subject: permission.subject, + conditions: permission.conditions, + createdAt: permission.createdAt.getTime(), + updatedAt: permission.updatedAt.getTime(), + }; +}; + +export default permissionSerializer; diff --git a/packages/backend/src/serializers/permission.test.js b/packages/backend/src/serializers/permission.test.js new file mode 100644 index 0000000000000000000000000000000000000000..9fff7f61f5a4f18d9e6a6e3628e23c577aeecce3 --- /dev/null +++ b/packages/backend/src/serializers/permission.test.js @@ -0,0 +1,25 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createPermission } from '../../test/factories/permission'; +import permissionSerializer from './permission'; + +describe('permissionSerializer', () => { + let permission; + + beforeEach(async () => { + permission = await createPermission(); + }); + + it('should return permission data', async () => { + const expectedPayload = { + id: permission.id, + roleId: permission.roleId, + action: permission.action, + subject: permission.subject, + conditions: permission.conditions, + createdAt: permission.createdAt.getTime(), + updatedAt: permission.updatedAt.getTime(), + }; + + expect(permissionSerializer(permission)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/role-mapping.ee.js b/packages/backend/src/serializers/role-mapping.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..cb2f9ed1ba95f04a6ef12f04eac697bcb92b36d4 --- /dev/null +++ b/packages/backend/src/serializers/role-mapping.ee.js @@ -0,0 +1,10 @@ +const roleMappingSerializer = (roleMapping) => { + return { + id: roleMapping.id, + samlAuthProviderId: roleMapping.samlAuthProviderId, + roleId: roleMapping.roleId, + remoteRoleName: roleMapping.remoteRoleName, + }; +}; + +export default roleMappingSerializer; diff --git a/packages/backend/src/serializers/role.js b/packages/backend/src/serializers/role.js new file mode 100644 index 0000000000000000000000000000000000000000..8e9051fab57c3a602d650afb2f28f95e4702c209 --- /dev/null +++ b/packages/backend/src/serializers/role.js @@ -0,0 +1,23 @@ +import permissionSerializer from './permission.js'; + +const roleSerializer = (role) => { + let roleData = { + id: role.id, + name: role.name, + key: role.key, + description: role.description, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + isAdmin: role.isAdmin, + }; + + if (role.permissions?.length > 0) { + roleData.permissions = role.permissions.map((permission) => + permissionSerializer(permission) + ); + } + + return roleData; +}; + +export default roleSerializer; diff --git a/packages/backend/src/serializers/role.test.js b/packages/backend/src/serializers/role.test.js new file mode 100644 index 0000000000000000000000000000000000000000..b292901949c0f376e2b71a9a5778ec9868a2f024 --- /dev/null +++ b/packages/backend/src/serializers/role.test.js @@ -0,0 +1,52 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createRole } from '../../test/factories/role'; +import roleSerializer from './role'; +import permissionSerializer from './permission'; +import { createPermission } from '../../test/factories/permission'; + +describe('roleSerializer', () => { + let role, permissionOne, permissionTwo; + + beforeEach(async () => { + role = await createRole(); + + permissionOne = await createPermission({ + roleId: role.id, + action: 'read', + subject: 'User', + }); + + permissionTwo = await createPermission({ + roleId: role.id, + action: 'read', + subject: 'Role', + }); + }); + + it('should return role data', async () => { + const expectedPayload = { + id: role.id, + name: role.name, + key: role.key, + description: role.description, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + isAdmin: role.isAdmin, + }; + + expect(roleSerializer(role)).toEqual(expectedPayload); + }); + + it('should return role data with the permissions', async () => { + role.permissions = [permissionOne, permissionTwo]; + + const expectedPayload = { + permissions: [ + permissionSerializer(permissionOne), + permissionSerializer(permissionTwo), + ], + }; + + expect(roleSerializer(role)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/saml-auth-provider.ee.js b/packages/backend/src/serializers/saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..4c0bfde38f570fa374f34b8d08c0b8b7573ba4d3 --- /dev/null +++ b/packages/backend/src/serializers/saml-auth-provider.ee.js @@ -0,0 +1,10 @@ +const samlAuthProviderSerializer = (samlAuthProvider) => { + return { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + loginUrl: samlAuthProvider.loginUrl, + issuer: samlAuthProvider.issuer, + }; +}; + +export default samlAuthProviderSerializer; diff --git a/packages/backend/src/serializers/saml-auth-provider.ee.test.js b/packages/backend/src/serializers/saml-auth-provider.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..9a05c3db5f80e5d26e6cf501d30221f8c2dd11e8 --- /dev/null +++ b/packages/backend/src/serializers/saml-auth-provider.ee.test.js @@ -0,0 +1,24 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createSamlAuthProvider } from '../../test/factories/saml-auth-provider.ee.js'; +import samlAuthProviderSerializer from './saml-auth-provider.ee.js'; + +describe('samlAuthProviderSerializer', () => { + let samlAuthProvider; + + beforeEach(async () => { + samlAuthProvider = await createSamlAuthProvider(); + }); + + it('should return saml auth provider data', async () => { + const expectedPayload = { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + loginUrl: samlAuthProvider.loginUrl, + issuer: samlAuthProvider.issuer, + }; + + expect(samlAuthProviderSerializer(samlAuthProvider)).toEqual( + expectedPayload + ); + }); +}); diff --git a/packages/backend/src/serializers/step.js b/packages/backend/src/serializers/step.js new file mode 100644 index 0000000000000000000000000000000000000000..27a9060cf220e5c660242e2dde6d4093a1ee3c94 --- /dev/null +++ b/packages/backend/src/serializers/step.js @@ -0,0 +1,25 @@ +import executionStepSerializer from './execution-step.js'; + +const stepSerializer = (step) => { + let stepData = { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }; + + if (step.executionSteps?.length > 0) { + stepData.executionSteps = step.executionSteps.map((executionStep) => + executionStepSerializer(executionStep) + ); + } + + return stepData; +}; + +export default stepSerializer; diff --git a/packages/backend/src/serializers/step.test.js b/packages/backend/src/serializers/step.test.js new file mode 100644 index 0000000000000000000000000000000000000000..5ebc340e1d97824c21d61293fad25cd15a763007 --- /dev/null +++ b/packages/backend/src/serializers/step.test.js @@ -0,0 +1,45 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import { createStep } from '../../test/factories/step'; +import { createExecutionStep } from '../../test/factories/execution-step'; +import stepSerializer from './step'; +import executionStepSerializer from './execution-step'; + +describe('stepSerializer', () => { + let step; + + beforeEach(async () => { + step = await createStep(); + }); + + it('should return step data', async () => { + const expectedPayload = { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }; + + expect(stepSerializer(step)).toEqual(expectedPayload); + }); + + it('should return step data with the execution steps', async () => { + const executionStepOne = await createExecutionStep({ stepId: step.id }); + const executionStepTwo = await createExecutionStep({ stepId: step.id }); + + step.executionSteps = [executionStepOne, executionStepTwo]; + + const expectedPayload = { + executionSteps: [ + executionStepSerializer(executionStepOne), + executionStepSerializer(executionStepTwo), + ], + }; + + expect(stepSerializer(step)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/subscription.ee.js b/packages/backend/src/serializers/subscription.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..0e2e52396d7ff049c46ebcc6d49af5c7351c04ac --- /dev/null +++ b/packages/backend/src/serializers/subscription.ee.js @@ -0,0 +1,20 @@ +const subscriptinSerializer = (subscription) => { + let userData = { + id: subscription.id, + paddleSubscriptionId: subscription.paddleSubscriptionId, + paddlePlanId: subscription.paddlePlanId, + updateUrl: subscription.updateUrl, + cancelUrl: subscription.cancelUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate, + lastBillDate: subscription.lastBillDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + }; + + return userData; +}; + +export default subscriptinSerializer; diff --git a/packages/backend/src/serializers/subscription.ee.test.js b/packages/backend/src/serializers/subscription.ee.test.js new file mode 100644 index 0000000000000000000000000000000000000000..d819593240a02f845f5189c08c3abcd5e4d37681 --- /dev/null +++ b/packages/backend/src/serializers/subscription.ee.test.js @@ -0,0 +1,35 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import appConfig from '../config/app'; +import { createUser } from '../../test/factories/user'; +import { createSubscription } from '../../test/factories/subscription'; +import subscriptionSerializer from './subscription.ee.js'; + +describe('subscriptionSerializer', () => { + let user, subscription; + + beforeEach(async () => { + user = await createUser(); + subscription = await createSubscription({ userId: user.id }); + }); + + it('should return subscription data', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + const expectedPayload = { + id: subscription.id, + paddleSubscriptionId: subscription.paddleSubscriptionId, + paddlePlanId: subscription.paddlePlanId, + updateUrl: subscription.updateUrl, + cancelUrl: subscription.cancelUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate, + lastBillDate: subscription.lastBillDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + }; + + expect(subscriptionSerializer(subscription)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/trigger.js b/packages/backend/src/serializers/trigger.js new file mode 100644 index 0000000000000000000000000000000000000000..07fc0927c7dd7280a2e6deda7d161c9b0d235913 --- /dev/null +++ b/packages/backend/src/serializers/trigger.js @@ -0,0 +1,12 @@ +const triggerSerializer = (trigger) => { + return { + description: trigger.description, + key: trigger.key, + name: trigger.name, + pollInterval: trigger.pollInterval, + showWebhookUrl: trigger.showWebhookUrl, + type: trigger.type, + }; +}; + +export default triggerSerializer; diff --git a/packages/backend/src/serializers/trigger.test.js b/packages/backend/src/serializers/trigger.test.js new file mode 100644 index 0000000000000000000000000000000000000000..126a663779aa96502b12a639ebee17ae3579e6ee --- /dev/null +++ b/packages/backend/src/serializers/trigger.test.js @@ -0,0 +1,21 @@ +import { describe, it, expect } from 'vitest'; +import App from '../models/app'; +import triggerSerializer from './trigger'; + +describe('triggerSerializer', () => { + it('should return the trigger data', async () => { + const triggers = await App.findTriggersByKey('github'); + const trigger = triggers[0]; + + const expectedPayload = { + description: trigger.description, + key: trigger.key, + name: trigger.name, + pollInterval: trigger.pollInterval, + showWebhookUrl: trigger.showWebhookUrl, + type: trigger.type, + }; + + expect(triggerSerializer(trigger)).toEqual(expectedPayload); + }); +}); diff --git a/packages/backend/src/serializers/user.js b/packages/backend/src/serializers/user.js new file mode 100644 index 0000000000000000000000000000000000000000..2cdec6d52a6274c139e23860a6e6f0db6ed386d8 --- /dev/null +++ b/packages/backend/src/serializers/user.js @@ -0,0 +1,31 @@ +import roleSerializer from './role.js'; +import permissionSerializer from './permission.js'; +import appConfig from '../config/app.js'; + +const userSerializer = (user) => { + let userData = { + id: user.id, + email: user.email, + createdAt: user.createdAt.getTime(), + updatedAt: user.updatedAt.getTime(), + fullName: user.fullName, + }; + + if (user.role) { + userData.role = roleSerializer(user.role); + } + + if (user.permissions?.length > 0) { + userData.permissions = user.permissions.map((permission) => + permissionSerializer(permission) + ); + } + + if (appConfig.isCloud && user.trialExpiryDate) { + userData.trialExpiryDate = user.trialExpiryDate; + } + + return userData; +}; + +export default userSerializer; diff --git a/packages/backend/src/serializers/user.test.js b/packages/backend/src/serializers/user.test.js new file mode 100644 index 0000000000000000000000000000000000000000..7a80cb9a1b48545cbc3876866f1969a138312433 --- /dev/null +++ b/packages/backend/src/serializers/user.test.js @@ -0,0 +1,80 @@ +import { vi, describe, it, expect, beforeEach } from 'vitest'; +import { DateTime } from 'luxon'; +import appConfig from '../config/app'; +import { createUser } from '../../test/factories/user'; +import { createPermission } from '../../test/factories/permission'; +import userSerializer from './user'; +import roleSerializer from './role'; +import permissionSerializer from './permission'; + +describe('userSerializer', () => { + let user, role, permissionOne, permissionTwo; + + beforeEach(async () => { + user = await createUser(); + role = await user.$relatedQuery('role'); + + permissionOne = await createPermission({ + roleId: role.id, + action: 'read', + subject: 'User', + }); + + permissionTwo = await createPermission({ + roleId: role.id, + action: 'read', + subject: 'Role', + }); + }); + + it('should return user data', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false); + + const expectedPayload = { + createdAt: user.createdAt.getTime(), + email: user.email, + fullName: user.fullName, + id: user.id, + updatedAt: user.updatedAt.getTime(), + }; + + expect(userSerializer(user)).toEqual(expectedPayload); + }); + + it('should return user data with the role', async () => { + user.role = role; + + const expectedPayload = { + role: roleSerializer(role), + }; + + expect(userSerializer(user)).toMatchObject(expectedPayload); + }); + + it('should return user data with the permissions', async () => { + user.permissions = [permissionOne, permissionTwo]; + + const expectedPayload = { + permissions: [ + permissionSerializer(permissionOne), + permissionSerializer(permissionTwo), + ], + }; + + expect(userSerializer(user)).toMatchObject(expectedPayload); + }); + + it('should return user data with trial expiry date', async () => { + vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true); + + await user.$query().patchAndFetch({ + trialExpiryDate: DateTime.now().plus({ days: 30 }).toISODate(), + }); + + const expectedPayload = { + trialExpiryDate: user.trialExpiryDate, + }; + + expect(userSerializer(user)).toMatchObject(expectedPayload); + }); +}); diff --git a/packages/backend/src/server.js b/packages/backend/src/server.js new file mode 100644 index 0000000000000000000000000000000000000000..6396088e64ecd700d40ffff405885c70631cb50a --- /dev/null +++ b/packages/backend/src/server.js @@ -0,0 +1,18 @@ +import app from './app.js'; +import appConfig from './config/app.js'; +import logger from './helpers/logger.js'; +import telemetry from './helpers/telemetry/index.js'; + +telemetry.setServiceType('main'); + +const server = app.listen(appConfig.port, () => { + logger.info(`Server is listening on ${appConfig.baseUrl}`); +}); + +function shutdown(server) { + server.close(); +} + +process.on('SIGTERM', () => { + shutdown(server); +}); diff --git a/packages/backend/src/services/action.js b/packages/backend/src/services/action.js new file mode 100644 index 0000000000000000000000000000000000000000..4bc5fac15cf78431cc8f7a2a59daa1b7a081a3b3 --- /dev/null +++ b/packages/backend/src/services/action.js @@ -0,0 +1,76 @@ +import Step from '../models/step.js'; +import Flow from '../models/flow.js'; +import Execution from '../models/execution.js'; +import ExecutionStep from '../models/execution-step.js'; +import computeParameters from '../helpers/compute-parameters.js'; +import globalVariable from '../helpers/global-variable.js'; +import { logger } from '../helpers/logger.js'; +import HttpError from '../errors/http.js'; +import EarlyExitError from '../errors/early-exit.js'; +import AlreadyProcessedError from '../errors/already-processed.js'; + +export const processAction = async (options) => { + const { flowId, stepId, executionId } = options; + + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const execution = await Execution.query() + .findById(executionId) + .throwIfNotFound(); + + const step = await Step.query().findById(stepId).throwIfNotFound(); + + const $ = await globalVariable({ + flow, + app: await step.getApp(), + step: step, + connection: await step.$relatedQuery('connection'), + execution, + }); + + const priorExecutionSteps = await ExecutionStep.query().where({ + execution_id: $.execution.id, + }); + + const computedParameters = computeParameters( + $.step.parameters, + priorExecutionSteps + ); + + const actionCommand = await step.getActionCommand(); + + $.step.parameters = computedParameters; + + try { + await actionCommand.run($); + } catch (error) { + const shouldEarlyExit = error instanceof EarlyExitError; + const shouldNotProcess = error instanceof AlreadyProcessedError; + const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess; + + if (!shouldNotConsiderAsError) { + logger.error(error); + + if (error instanceof HttpError) { + $.actionOutput.error = error.details; + } else { + try { + $.actionOutput.error = JSON.parse(error.message); + } catch { + $.actionOutput.error = { error: error.message }; + } + } + } + } + + const executionStep = await execution + .$relatedQuery('executionSteps') + .insertAndFetch({ + stepId: $.step.id, + status: $.actionOutput.error ? 'failure' : 'success', + dataIn: computedParameters, + dataOut: $.actionOutput.error ? null : $.actionOutput.data?.raw, + errorDetails: $.actionOutput.error ? $.actionOutput.error : null, + }); + + return { flowId, stepId, executionId, executionStep, computedParameters }; +}; diff --git a/packages/backend/src/services/flow.js b/packages/backend/src/services/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..e257009ee54c93f4db38be14fb1dbf722d47353e --- /dev/null +++ b/packages/backend/src/services/flow.js @@ -0,0 +1,49 @@ +import Flow from '../models/flow.js'; +import globalVariable from '../helpers/global-variable.js'; +import EarlyExitError from '../errors/early-exit.js'; +import AlreadyProcessedError from '../errors/already-processed.js'; +import HttpError from '../errors/http.js'; +import { logger } from '../helpers/logger.js'; + +export const processFlow = async (options) => { + const { testRun, flowId } = options; + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const triggerStep = await flow.getTriggerStep(); + const triggerCommand = await triggerStep.getTriggerCommand(); + + const $ = await globalVariable({ + flow, + connection: await triggerStep.$relatedQuery('connection'), + app: await triggerStep.getApp(), + step: triggerStep, + testRun, + }); + + try { + if (triggerCommand.type === 'webhook' && !flow.active) { + await triggerCommand.testRun($); + } else { + await triggerCommand.run($); + } + } catch (error) { + const shouldEarlyExit = error instanceof EarlyExitError; + const shouldNotProcess = error instanceof AlreadyProcessedError; + const shouldNotConsiderAsError = shouldEarlyExit || shouldNotProcess; + + if (!shouldNotConsiderAsError) { + logger.error(error); + + if (error instanceof HttpError) { + $.triggerOutput.error = error.details; + } else { + try { + $.triggerOutput.error = JSON.parse(error.message); + } catch { + $.triggerOutput.error = { error: error.message }; + } + } + } + } + + return $.triggerOutput; +}; diff --git a/packages/backend/src/services/test-run.js b/packages/backend/src/services/test-run.js new file mode 100644 index 0000000000000000000000000000000000000000..880238216d4bc87eb46b1650a67ed7eec9436dc3 --- /dev/null +++ b/packages/backend/src/services/test-run.js @@ -0,0 +1,61 @@ +import Step from '../models/step.js'; +import { processFlow } from './flow.js'; +import { processTrigger } from './trigger.js'; +import { processAction } from './action.js'; + +const testRun = async (options) => { + const untilStep = await Step.query() + .findById(options.stepId) + .throwIfNotFound(); + + const flow = await untilStep.$relatedQuery('flow'); + const [triggerStep, ...actionSteps] = await flow + .$relatedQuery('steps') + .withGraphFetched('connection') + .orderBy('position', 'asc'); + + const { data, error: triggerError } = await processFlow({ + flowId: flow.id, + testRun: true, + }); + + if (triggerError) { + const { executionStep: triggerExecutionStepWithError } = + await processTrigger({ + flowId: flow.id, + stepId: triggerStep.id, + error: triggerError, + testRun: true, + }); + + return { executionStep: triggerExecutionStepWithError }; + } + + const firstTriggerItem = data[0]; + + const { executionId, executionStep: triggerExecutionStep } = + await processTrigger({ + flowId: flow.id, + stepId: triggerStep.id, + triggerItem: firstTriggerItem, + testRun: true, + }); + + if (triggerStep.id === untilStep.id) { + return { executionStep: triggerExecutionStep }; + } + + for (const actionStep of actionSteps) { + const { executionStep: actionExecutionStep } = await processAction({ + flowId: flow.id, + stepId: actionStep.id, + executionId, + }); + + if (actionStep.id === untilStep.id || actionExecutionStep.isFailed) { + return { executionStep: actionExecutionStep }; + } + } +}; + +export default testRun; diff --git a/packages/backend/src/services/trigger.js b/packages/backend/src/services/trigger.js new file mode 100644 index 0000000000000000000000000000000000000000..075a9fc307b471fb47424a28ee8f0b01f62bbd42 --- /dev/null +++ b/packages/backend/src/services/trigger.js @@ -0,0 +1,37 @@ +import Step from '../models/step.js'; +import Flow from '../models/flow.js'; +import Execution from '../models/execution.js'; +import globalVariable from '../helpers/global-variable.js'; + +export const processTrigger = async (options) => { + const { flowId, stepId, triggerItem, error, testRun } = options; + + const step = await Step.query().findById(stepId).throwIfNotFound(); + + const $ = await globalVariable({ + flow: await Flow.query().findById(flowId).throwIfNotFound(), + app: await step.getApp(), + step: step, + connection: await step.$relatedQuery('connection'), + }); + + // check if we already process this trigger data item or not! + + const execution = await Execution.query().insert({ + flowId: $.flow.id, + testRun, + internalId: triggerItem?.meta.internalId, + }); + + const executionStep = await execution + .$relatedQuery('executionSteps') + .insertAndFetch({ + stepId: $.step.id, + status: error ? 'failure' : 'success', + dataIn: $.step.parameters, + dataOut: !error ? triggerItem?.raw : null, + errorDetails: error, + }); + + return { flowId, stepId, executionId: execution.id, executionStep }; +}; diff --git a/packages/backend/src/views/emails/reset-password-instructions.ee.hbs b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs new file mode 100644 index 0000000000000000000000000000000000000000..8397c863afb9cc268c1c144de29d8579d15cc483 --- /dev/null +++ b/packages/backend/src/views/emails/reset-password-instructions.ee.hbs @@ -0,0 +1,23 @@ + + + + Reset password instructions + + +

+ Hello {{ fullName }}, +

+ +

+ Someone has requested a link to change your password, and you can do this through the link below. +

+ +

+ Change my password +

+ +

+ If you didn't request this, please ignore this email. Your password won't change until you access the link above and create a new one. +

+ + diff --git a/packages/backend/src/worker.js b/packages/backend/src/worker.js new file mode 100644 index 0000000000000000000000000000000000000000..988cb0cc644e6cc622ed1415b30a02dc442de953 --- /dev/null +++ b/packages/backend/src/worker.js @@ -0,0 +1,21 @@ +import * as Sentry from './helpers/sentry.ee.js'; +import appConfig from './config/app.js'; + +Sentry.init(); + +import './config/orm.js'; +import './helpers/check-worker-readiness.js'; +import './workers/flow.js'; +import './workers/trigger.js'; +import './workers/action.js'; +import './workers/email.js'; +import './workers/delete-user.ee.js'; + +if (appConfig.isCloud) { + import('./workers/remove-cancelled-subscriptions.ee.js'); + import('./queues/remove-cancelled-subscriptions.ee.js'); +} + +import telemetry from './helpers/telemetry/index.js'; + +telemetry.setServiceType('worker'); diff --git a/packages/backend/src/workers/action.js b/packages/backend/src/workers/action.js new file mode 100644 index 0000000000000000000000000000000000000000..9564d9a46a0f835e1d8e81895224c5a0b3a2ffdf --- /dev/null +++ b/packages/backend/src/workers/action.js @@ -0,0 +1,79 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import Step from '../models/step.js'; +import actionQueue from '../queues/action.js'; +import { processAction } from '../services/action.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; +import delayAsMilliseconds from '../helpers/delay-as-milliseconds.js'; + +const DEFAULT_DELAY_DURATION = 0; + +export const worker = new Worker( + 'action', + async (job) => { + const { stepId, flowId, executionId, computedParameters, executionStep } = + await processAction(job.data); + + if (executionStep.isFailed) return; + + const step = await Step.query().findById(stepId).throwIfNotFound(); + const nextStep = await step.getNextStep(); + + if (!nextStep) return; + + const jobName = `${executionId}-${nextStep.id}`; + + const jobPayload = { + flowId, + executionId, + stepId: nextStep.id, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + delay: DEFAULT_DELAY_DURATION, + }; + + if (step.appKey === 'delay') { + jobOptions.delay = delayAsMilliseconds(step.key, computedParameters); + } + + if (step.appKey === 'filter' && !executionStep.dataOut) { + return; + } + + await actionQueue.add(jobName, jobPayload, jobOptions); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} + \n ${err.stack} + `; + + logger.error(errorMessage); + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/src/workers/delete-user.ee.js b/packages/backend/src/workers/delete-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..b033c18368bf7973932475d559518c65e9d83b99 --- /dev/null +++ b/packages/backend/src/workers/delete-user.ee.js @@ -0,0 +1,71 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import appConfig from '../config/app.js'; +import User from '../models/user.js'; +import ExecutionStep from '../models/execution-step.js'; + +export const worker = new Worker( + 'delete-user', + async (job) => { + const { id } = job.data; + + const user = await User.query() + .withSoftDeleted() + .findById(id) + .throwIfNotFound(); + + const executionIds = ( + await user + .$relatedQuery('executions') + .withSoftDeleted() + .select('executions.id') + ).map((execution) => execution.id); + + await ExecutionStep.query() + .withSoftDeleted() + .whereIn('execution_id', executionIds) + .hardDelete(); + await user.$relatedQuery('executions').withSoftDeleted().hardDelete(); + await user.$relatedQuery('steps').withSoftDeleted().hardDelete(); + await user.$relatedQuery('flows').withSoftDeleted().hardDelete(); + await user.$relatedQuery('connections').withSoftDeleted().hardDelete(); + await user.$relatedQuery('identities').withSoftDeleted().hardDelete(); + + if (appConfig.isCloud) { + await user.$relatedQuery('subscriptions').withSoftDeleted().hardDelete(); + await user.$relatedQuery('usageData').withSoftDeleted().hardDelete(); + } + + await user.$query().withSoftDeleted().hardDelete(); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has been deleted!` + ); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message} + \n ${err.stack} + `; + + logger.error(errorMessage); + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/src/workers/email.js b/packages/backend/src/workers/email.js new file mode 100644 index 0000000000000000000000000000000000000000..e1297376d94e886a7f143f6284cda4a75146ff31 --- /dev/null +++ b/packages/backend/src/workers/email.js @@ -0,0 +1,65 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import mailer from '../helpers/mailer.ee.js'; +import compileEmail from '../helpers/compile-email.ee.js'; +import appConfig from '../config/app.js'; + +const isCloudSandbox = () => { + return appConfig.isCloud && !appConfig.isProd; +}; + +const isAutomatischEmail = (email) => { + return email.endsWith('@automatisch.io'); +}; + +export const worker = new Worker( + 'email', + async (job) => { + const { email, subject, template, params } = job.data; + + if (isCloudSandbox && !isAutomatischEmail(email)) { + logger.info( + 'Only Automatisch emails are allowed for non-production environments!' + ); + + return; + } + + await mailer.sendMail({ + to: email, + from: appConfig.fromEmail, + subject: subject, + html: compileEmail(template, params), + }); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - ${job.data.subject} email sent to ${job.data.email}!` + ); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message} + \n ${err.stack} + `; + + logger.error(errorMessage); + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/src/workers/flow.js b/packages/backend/src/workers/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..b1bcce785965118ffb3a16e2d4d8719278876639 --- /dev/null +++ b/packages/backend/src/workers/flow.js @@ -0,0 +1,100 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import flowQueue from '../queues/flow.js'; +import triggerQueue from '../queues/trigger.js'; +import { processFlow } from '../services/flow.js'; +import Flow from '../models/flow.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; + +export const worker = new Worker( + 'flow', + async (job) => { + const { flowId } = job.data; + + const flow = await Flow.query().findById(flowId).throwIfNotFound(); + const user = await flow.$relatedQuery('user'); + const allowedToRunFlows = await user.isAllowedToRunFlows(); + + if (!allowedToRunFlows) { + return; + } + + const triggerStep = await flow.getTriggerStep(); + + const { data, error } = await processFlow({ flowId }); + + const reversedData = data.reverse(); + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + for (const triggerItem of reversedData) { + const jobName = `${triggerStep.id}-${triggerItem.meta.internalId}`; + + const jobPayload = { + flowId, + stepId: triggerStep.id, + triggerItem, + }; + + await triggerQueue.add(jobName, jobPayload, jobOptions); + } + + if (error) { + const jobName = `${triggerStep.id}-error`; + + const jobPayload = { + flowId, + stepId: triggerStep.id, + error, + }; + + await triggerQueue.add(jobName, jobPayload, jobOptions); + } + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); +}); + +worker.on('failed', async (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} + \n ${err.stack} + `; + + logger.error(errorMessage); + + const flow = await Flow.query().findById(job.data.flowId); + + if (!flow) { + await flowQueue.removeRepeatableByKey(job.repeatJobKey); + + const flowNotFoundErrorMessage = ` + JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has been deleted from Redis because flow was not found! + `; + + logger.error(flowNotFoundErrorMessage); + } + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7b3952bff6700d9aed59fc5c1fc5455183a4ce0f --- /dev/null +++ b/packages/backend/src/workers/remove-cancelled-subscriptions.ee.js @@ -0,0 +1,47 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; +import { DateTime } from 'luxon'; +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import Subscription from '../models/subscription.ee.js'; + +export const worker = new Worker( + 'remove-cancelled-subscriptions', + async () => { + await Subscription.query() + .delete() + .where({ + status: 'deleted', + }) + .andWhere( + 'cancellation_effective_date', + '<=', + DateTime.now().startOf('day').toISODate() + ); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info( + `JOB ID: ${job.id} - The cancelled subscriptions have been removed!` + ); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - ERROR: The cancelled subscriptions can not be removed! ${err.message} + \n ${err.stack} + `; + logger.error(errorMessage); + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/src/workers/trigger.js b/packages/backend/src/workers/trigger.js new file mode 100644 index 0000000000000000000000000000000000000000..58749f75d640e1bfdc0a96b7d74f1fd4907e6c1f --- /dev/null +++ b/packages/backend/src/workers/trigger.js @@ -0,0 +1,65 @@ +import { Worker } from 'bullmq'; +import process from 'node:process'; + +import * as Sentry from '../helpers/sentry.ee.js'; +import redisConfig from '../config/redis.js'; +import logger from '../helpers/logger.js'; +import actionQueue from '../queues/action.js'; +import Step from '../models/step.js'; +import { processTrigger } from '../services/trigger.js'; +import { + REMOVE_AFTER_30_DAYS_OR_150_JOBS, + REMOVE_AFTER_7_DAYS_OR_50_JOBS, +} from '../helpers/remove-job-configuration.js'; + +export const worker = new Worker( + 'trigger', + async (job) => { + const { flowId, executionId, stepId, executionStep } = await processTrigger( + job.data + ); + + if (executionStep.isFailed) return; + + const step = await Step.query().findById(stepId).throwIfNotFound(); + const nextStep = await step.getNextStep(); + const jobName = `${executionId}-${nextStep.id}`; + + const jobPayload = { + flowId, + executionId, + stepId: nextStep.id, + }; + + const jobOptions = { + removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS, + removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS, + }; + + await actionQueue.add(jobName, jobPayload, jobOptions); + }, + { connection: redisConfig } +); + +worker.on('completed', (job) => { + logger.info(`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has started!`); +}); + +worker.on('failed', (job, err) => { + const errorMessage = ` + JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message} + \n ${err.stack} + `; + + logger.error(errorMessage); + + Sentry.captureException(err, { + extra: { + jobId: job.id, + }, + }); +}); + +process.on('SIGTERM', async () => { + await worker.close(); +}); diff --git a/packages/backend/test/factories/app-auth-client.js b/packages/backend/test/factories/app-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..0cadf197e32b9e692df99bdb4cf5c67fa7a4e5e4 --- /dev/null +++ b/packages/backend/test/factories/app-auth-client.js @@ -0,0 +1,22 @@ +import { faker } from '@faker-js/faker'; +import AppAuthClient from '../../src/models/app-auth-client'; + +const formattedAuthDefaults = { + oAuthRedirectUrl: faker.internet.url(), + instanceUrl: faker.internet.url(), + clientId: faker.string.uuid(), + clientSecret: faker.string.uuid(), +}; + +export const createAppAuthClient = async (params = {}) => { + params.name = params?.name || faker.person.fullName(); + params.id = params?.id || faker.string.uuid(); + params.appKey = params?.appKey || 'deepl'; + params.active = params?.active ?? true; + params.formattedAuthDefaults = + params?.formattedAuthDefaults || formattedAuthDefaults; + + const appAuthClient = await AppAuthClient.query().insertAndFetch(params); + + return appAuthClient; +}; diff --git a/packages/backend/test/factories/app-config.js b/packages/backend/test/factories/app-config.js new file mode 100644 index 0000000000000000000000000000000000000000..d71ee9983e774e2080e5cec6a6285b350c2ead73 --- /dev/null +++ b/packages/backend/test/factories/app-config.js @@ -0,0 +1,10 @@ +import AppConfig from '../../src/models/app-config.js'; +import { faker } from '@faker-js/faker'; + +export const createAppConfig = async (params = {}) => { + params.key = params?.key || faker.lorem.word(); + + const appConfig = await AppConfig.query().insertAndFetch(params); + + return appConfig; +}; diff --git a/packages/backend/test/factories/config.js b/packages/backend/test/factories/config.js new file mode 100644 index 0000000000000000000000000000000000000000..5a8e316ca0dc182da535e53d3b163cc7163a5e03 --- /dev/null +++ b/packages/backend/test/factories/config.js @@ -0,0 +1,17 @@ +import { faker } from '@faker-js/faker'; +import Config from '../../src/models/config'; + +export const createConfig = async (params = {}) => { + const configData = { + key: params?.key || faker.lorem.word(), + value: params?.value || { data: 'sampleConfig' }, + }; + + const config = await Config.query().insertAndFetch(configData); + + return config; +}; + +export const createInstallationCompletedConfig = async () => { + return await createConfig({ key: 'installation.completed', value: { data: true } }); +} diff --git a/packages/backend/test/factories/connection.js b/packages/backend/test/factories/connection.js new file mode 100644 index 0000000000000000000000000000000000000000..9692a3ab8ef6ec14c1728cb0a70542da17145d9a --- /dev/null +++ b/packages/backend/test/factories/connection.js @@ -0,0 +1,23 @@ +import appConfig from '../../src/config/app'; +import { AES } from 'crypto-js'; +import Connection from '../../src/models/connection'; + +export const createConnection = async (params = {}) => { + params.key = params?.key || 'deepl'; + + const formattedData = params.formattedData || { + screenName: 'Test - DeepL Connection', + authenticationKey: 'test key', + }; + + delete params.formattedData; + + params.data = AES.encrypt( + JSON.stringify(formattedData), + appConfig.encryptionKey + ).toString(); + + const connection = await Connection.query().insertAndFetch(params); + + return connection; +}; diff --git a/packages/backend/test/factories/execution-step.js b/packages/backend/test/factories/execution-step.js new file mode 100644 index 0000000000000000000000000000000000000000..842e3a9ffec7efdad72368f20a74510f12497092 --- /dev/null +++ b/packages/backend/test/factories/execution-step.js @@ -0,0 +1,15 @@ +import ExecutionStep from '../../src/models/execution-step'; +import { createExecution } from './execution'; +import { createStep } from './step'; + +export const createExecutionStep = async (params = {}) => { + params.executionId = params?.executionId || (await createExecution()).id; + params.stepId = params?.stepId || (await createStep()).id; + params.status = params?.status || 'success'; + params.dataIn = params?.dataIn || { dataIn: 'dataIn' }; + params.dataOut = params?.dataOut || { dataOut: 'dataOut' }; + + const executionStep = await ExecutionStep.query().insertAndFetch(params); + + return executionStep; +}; diff --git a/packages/backend/test/factories/execution.js b/packages/backend/test/factories/execution.js new file mode 100644 index 0000000000000000000000000000000000000000..22ad2b7d6a815a216c2bd59ffe72723195df676b --- /dev/null +++ b/packages/backend/test/factories/execution.js @@ -0,0 +1,11 @@ +import Execution from '../../src/models/execution'; +import { createFlow } from './flow'; + +export const createExecution = async (params = {}) => { + params.flowId = params?.flowId || (await createFlow()).id; + params.testRun = params?.testRun || false; + + const execution = await Execution.query().insertAndFetch(params); + + return execution; +}; diff --git a/packages/backend/test/factories/flow.js b/packages/backend/test/factories/flow.js new file mode 100644 index 0000000000000000000000000000000000000000..c23d4e0ecdcf993c27f628ccfe543156c8b037e7 --- /dev/null +++ b/packages/backend/test/factories/flow.js @@ -0,0 +1,13 @@ +import Flow from '../../src/models/flow'; +import { createUser } from './user'; + +export const createFlow = async (params = {}) => { + params.userId = params?.userId || (await createUser()).id; + params.name = params?.name || 'Name your flow!'; + params.createdAt = params?.createdAt || new Date().toISOString(); + params.updatedAt = params?.updatedAt || new Date().toISOString(); + + const flow = await Flow.query().insertAndFetch(params); + + return flow; +}; diff --git a/packages/backend/test/factories/permission.js b/packages/backend/test/factories/permission.js new file mode 100644 index 0000000000000000000000000000000000000000..d5434971ae4e37e8eb7dd2dd94db4fb2615d82f2 --- /dev/null +++ b/packages/backend/test/factories/permission.js @@ -0,0 +1,13 @@ +import Permission from '../../src/models/permission'; +import { createRole } from './role'; + +export const createPermission = async (params = {}) => { + params.roleId = params?.roleId || (await createRole()).id; + params.action = params?.action || 'read'; + params.subject = params?.subject || 'User'; + params.conditions = params?.conditions || ['isCreator']; + + const permission = await Permission.query().insertAndFetch(params); + + return permission; +}; diff --git a/packages/backend/test/factories/role-mapping.js b/packages/backend/test/factories/role-mapping.js new file mode 100644 index 0000000000000000000000000000000000000000..e9d37fcc43ccf8014b5777c21ea6241a7b4008a8 --- /dev/null +++ b/packages/backend/test/factories/role-mapping.js @@ -0,0 +1,16 @@ +import { createRole } from './role.js'; +import { createSamlAuthProvider } from './saml-auth-provider.ee.js'; +import SamlAuthProviderRoleMapping from '../../src/models/saml-auth-providers-role-mapping.ee.js'; + +export const createRoleMapping = async (params = {}) => { + params.roleId = params?.roleId || (await createRole()).id; + params.samlAuthProviderId = + params?.samlAuthProviderId || (await createSamlAuthProvider()).id; + + params.remoteRoleName = params?.remoteRoleName || 'User'; + + const samlAuthProviderRoleMapping = + await SamlAuthProviderRoleMapping.query().insertAndFetch(params); + + return samlAuthProviderRoleMapping; +}; diff --git a/packages/backend/test/factories/role.js b/packages/backend/test/factories/role.js new file mode 100644 index 0000000000000000000000000000000000000000..b06d93db9af8223b3b17f9a67916ff2a55525aba --- /dev/null +++ b/packages/backend/test/factories/role.js @@ -0,0 +1,10 @@ +import Role from '../../src/models/role'; + +export const createRole = async (params = {}) => { + params.name = params?.name || 'Viewer'; + params.key = params?.key || 'viewer'; + + const role = await Role.query().insertAndFetch(params); + + return role; +}; diff --git a/packages/backend/test/factories/saml-auth-provider.ee.js b/packages/backend/test/factories/saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..f205eda126dc16f941d266bd96b737b33f80b2fe --- /dev/null +++ b/packages/backend/test/factories/saml-auth-provider.ee.js @@ -0,0 +1,33 @@ +import { createRole } from './role'; +import SamlAuthProvider from '../../src/models/saml-auth-provider.ee.js'; + +export const createSamlAuthProvider = async (params = {}) => { + params.name = params?.name || 'Keycloak SAML'; + params.certificate = params?.certificate || 'certificate'; + params.signatureAlgorithm = params?.signatureAlgorithm || 'sha512'; + + params.entryPoint = + params?.entryPoint || + 'https://example.com/auth/realms/automatisch/protocol/saml'; + + params.issuer = params?.issuer || 'automatisch-client'; + + params.firstnameAttributeName = + params?.firstnameAttributeName || 'urn:oid:2.1.1.42'; + + params.surnameAttributeName = + params?.surnameAttributeName || 'urn:oid:2.1.1.4'; + + params.emailAttributeName = + params?.emailAttributeName || 'urn:oid:1.1.2342.19200300.100.1.1'; + + params.roleAttributeName = params?.roleAttributeName || 'Role'; + params.defaultRoleId = params?.defaultRoleId || (await createRole()).id; + params.active = params?.active || true; + + const samlAuthProvider = await SamlAuthProvider.query().insertAndFetch( + params + ); + + return samlAuthProvider; +}; diff --git a/packages/backend/test/factories/step.js b/packages/backend/test/factories/step.js new file mode 100644 index 0000000000000000000000000000000000000000..15573ae25facba3e12ac20d5a8b222584cceeae8 --- /dev/null +++ b/packages/backend/test/factories/step.js @@ -0,0 +1,27 @@ +import Step from '../../src/models/step'; +import { createFlow } from './flow'; + +export const createStep = async (params = {}) => { + params.flowId = params?.flowId || (await createFlow()).id; + params.type = params?.type || 'action'; + + const lastStep = await Step.query() + .where('flow_id', params.flowId) + .andWhere('deleted_at', null) + .orderBy('position', 'desc') + .limit(1) + .first(); + + params.position = + params?.position || (lastStep?.position ? lastStep.position + 1 : 1); + + params.status = params?.status || 'completed'; + params.appKey = + params?.appKey || (params.type === 'action' ? 'deepl' : 'webhook'); + + params.parameters = params?.parameters || {}; + + const step = await Step.query().insertAndFetch(params); + + return step; +}; diff --git a/packages/backend/test/factories/subscription.js b/packages/backend/test/factories/subscription.js new file mode 100644 index 0000000000000000000000000000000000000000..f63bcc96f338b825ded3f6031ed5b29d48cb2838 --- /dev/null +++ b/packages/backend/test/factories/subscription.js @@ -0,0 +1,21 @@ +import { DateTime } from 'luxon'; +import { createUser } from './user'; +import Subscription from '../../src/models/subscription.ee.js'; + +export const createSubscription = async (params = {}) => { + params.userId = params?.userId || (await createUser()).id; + params.paddleSubscriptionId = + params?.paddleSubscriptionId || 'paddleSubscriptionId'; + + params.paddlePlanId = params?.paddlePlanId || '47384'; + params.updateUrl = params?.updateUrl || 'https://example.com/update-url'; + params.cancelUrl = params?.cancelUrl || 'https://example.com/cancel-url'; + params.status = params?.status || 'active'; + params.nextBillAmount = params?.nextBillAmount || '20'; + params.nextBillDate = + params?.nextBillDate || DateTime.now().plus({ days: 30 }).toISODate(); + + const subscription = await Subscription.query().insertAndFetch(params); + + return subscription; +}; diff --git a/packages/backend/test/factories/usage-data.js b/packages/backend/test/factories/usage-data.js new file mode 100644 index 0000000000000000000000000000000000000000..c6d076141c59b863a732c82b63f2c23a1b6667fc --- /dev/null +++ b/packages/backend/test/factories/usage-data.js @@ -0,0 +1,15 @@ +import { DateTime } from 'luxon'; +import { createUser } from './user'; +import UsageData from '../../src/models/usage-data.ee.js'; + +export const createUsageData = async (params = {}) => { + params.userId = params?.userId || (await createUser()).id; + params.nextResetAt = + params?.nextResetAt || DateTime.now().plus({ days: 30 }).toISODate(); + + params.consumedTaskCount = params?.consumedTaskCount || 0; + + const usageData = await UsageData.query().insertAndFetch(params); + + return usageData; +}; diff --git a/packages/backend/test/factories/user.js b/packages/backend/test/factories/user.js new file mode 100644 index 0000000000000000000000000000000000000000..fe4719f2f379845b4281ed0ffaca06bcdf06df3c --- /dev/null +++ b/packages/backend/test/factories/user.js @@ -0,0 +1,14 @@ +import { createRole } from './role'; +import { faker } from '@faker-js/faker'; +import User from '../../src/models/user'; + +export const createUser = async (params = {}) => { + params.roleId = params?.roleId || (await createRole()).id; + params.fullName = params?.fullName || faker.person.fullName(); + params.email = params?.email || faker.internet.email(); + params.password = params?.password || faker.internet.password(); + + const user = await User.query().insertAndFetch(params); + + return user; +}; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..3789ffe75aeecdcfd396d0f5255521259e4562be --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-client.js @@ -0,0 +1,19 @@ +const getAppAuthClientMock = (appAuthClient) => { + return { + data: { + name: appAuthClient.name, + id: appAuthClient.id, + appConfigId: appAuthClient.appConfigId, + active: appAuthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'AppAuthClient', + }, + }; +}; + +export default getAppAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js new file mode 100644 index 0000000000000000000000000000000000000000..dd0cc5eebfe51daeac5180f7cfb1c9ad96c582b3 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/apps/get-auth-clients.js @@ -0,0 +1,18 @@ +const getAdminAppAuthClientsMock = (appAuthClients) => { + return { + data: appAuthClients.map((appAuthClient) => ({ + name: appAuthClient.name, + id: appAuthClient.id, + active: appAuthClient.active, + })), + meta: { + count: appAuthClients.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'AppAuthClient', + }, + }; +}; + +export default getAdminAppAuthClientsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/permissions/get-permissions-catalog.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/permissions/get-permissions-catalog.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..627bfa33e6ccdfc5534a8e49e83ba815d29751d2 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/permissions/get-permissions-catalog.ee.js @@ -0,0 +1,64 @@ +const getPermissionsCatalogMock = async () => { + const data = { + actions: [ + { + key: 'create', + label: 'Create', + subjects: ['Connection', 'Flow'], + }, + { + key: 'read', + label: 'Read', + subjects: ['Connection', 'Execution', 'Flow'], + }, + { + key: 'update', + label: 'Update', + subjects: ['Connection', 'Flow'], + }, + { + key: 'delete', + label: 'Delete', + subjects: ['Connection', 'Flow'], + }, + { + key: 'publish', + label: 'Publish', + subjects: ['Flow'], + }, + ], + conditions: [ + { + key: 'isCreator', + label: 'Is creator', + }, + ], + subjects: [ + { + key: 'Connection', + label: 'Connection', + }, + { + key: 'Flow', + label: 'Flow', + }, + { + key: 'Execution', + label: 'Execution', + }, + ], + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getPermissionsCatalogMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/roles/get-role.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/roles/get-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..05942763b4263a488e078885e5b83ef9b82b91fe --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/roles/get-role.ee.js @@ -0,0 +1,33 @@ +const getRoleMock = async (role, permissions) => { + const data = { + id: role.id, + key: role.key, + name: role.name, + isAdmin: role.isAdmin, + description: role.description, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + permissions: permissions.map((permission) => ({ + id: permission.id, + action: permission.action, + conditions: permission.conditions, + roleId: permission.roleId, + subject: permission.subject, + createdAt: permission.createdAt.getTime(), + updatedAt: permission.updatedAt.getTime(), + })), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Role', + }, + }; +}; + +export default getRoleMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/roles/get-roles.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/roles/get-roles.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..fdb7a05b0d68b5a34120ea49af80ad5b7003aa89 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/roles/get-roles.ee.js @@ -0,0 +1,26 @@ +const getRolesMock = async (roles) => { + const data = roles.map((role) => { + return { + id: role.id, + key: role.key, + name: role.name, + isAdmin: role.isAdmin, + description: role.description, + createdAt: role.createdAt.getTime(), + updatedAt: role.updatedAt.getTime(), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Role', + }, + }; +}; + +export default getRolesMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..66fdf08a3efd3e34a06423e9e5e48103e3748b7a --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-role-mappings.ee.js @@ -0,0 +1,23 @@ +const getRoleMappingsMock = async (roleMappings) => { + const data = roleMappings.map((roleMapping) => { + return { + id: roleMapping.id, + samlAuthProviderId: roleMapping.samlAuthProviderId, + roleId: roleMapping.roleId, + remoteRoleName: roleMapping.remoteRoleName, + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'SamlAuthProvidersRoleMapping', + }, + }; +}; + +export default getRoleMappingsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7a2d4c778cba990dc5c11d729c4bdaa996ac945c --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-provider.ee.js @@ -0,0 +1,29 @@ +const getSamlAuthProviderMock = async (samlAuthProvider) => { + const data = { + active: samlAuthProvider.active, + certificate: samlAuthProvider.certificate, + defaultRoleId: samlAuthProvider.defaultRoleId, + emailAttributeName: samlAuthProvider.emailAttributeName, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + id: samlAuthProvider.id, + issuer: samlAuthProvider.issuer, + name: samlAuthProvider.name, + roleAttributeName: samlAuthProvider.roleAttributeName, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'SamlAuthProvider', + }, + }; +}; + +export default getSamlAuthProviderMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..30d5bfc540949d1123f811c9bd05aa0e8d4561f3 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/saml-auth-providers/get-saml-auth-providers.ee.js @@ -0,0 +1,31 @@ +const getSamlAuthProvidersMock = async (samlAuthProviders) => { + const data = samlAuthProviders.map((samlAuthProvider) => { + return { + active: samlAuthProvider.active, + certificate: samlAuthProvider.certificate, + defaultRoleId: samlAuthProvider.defaultRoleId, + emailAttributeName: samlAuthProvider.emailAttributeName, + entryPoint: samlAuthProvider.entryPoint, + firstnameAttributeName: samlAuthProvider.firstnameAttributeName, + id: samlAuthProvider.id, + issuer: samlAuthProvider.issuer, + name: samlAuthProvider.name, + roleAttributeName: samlAuthProvider.roleAttributeName, + signatureAlgorithm: samlAuthProvider.signatureAlgorithm, + surnameAttributeName: samlAuthProvider.surnameAttributeName, + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'SamlAuthProvider', + }, + }; +}; + +export default getSamlAuthProvidersMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/users/get-user.js b/packages/backend/test/mocks/rest/api/v1/admin/users/get-user.js new file mode 100644 index 0000000000000000000000000000000000000000..d917f16fcf86acbbaa753d1c273176a36695151d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/users/get-user.js @@ -0,0 +1,30 @@ +const getUserMock = (currentUser, role) => { + return { + data: { + createdAt: currentUser.createdAt.getTime(), + email: currentUser.email, + fullName: currentUser.fullName, + id: currentUser.id, + role: { + createdAt: role.createdAt.getTime(), + description: null, + id: role.id, + isAdmin: role.isAdmin, + key: role.key, + name: role.name, + updatedAt: role.updatedAt.getTime(), + }, + trialExpiryDate: currentUser.trialExpiryDate.toISOString(), + updatedAt: currentUser.updatedAt.getTime(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'User', + }, + }; +}; + +export default getUserMock; diff --git a/packages/backend/test/mocks/rest/api/v1/admin/users/get-users.js b/packages/backend/test/mocks/rest/api/v1/admin/users/get-users.js new file mode 100644 index 0000000000000000000000000000000000000000..0ae517732a6d5a3d47a3ec371751a5be23496624 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/admin/users/get-users.js @@ -0,0 +1,38 @@ +const getUsersMock = async (users, roles) => { + const data = users.map((user) => { + const role = roles.find((r) => r.id === user.roleId); + + return { + createdAt: user.createdAt.getTime(), + email: user.email, + fullName: user.fullName, + id: user.id, + role: role + ? { + createdAt: role.createdAt.getTime(), + description: role.description, + id: role.id, + isAdmin: role.isAdmin, + key: role.key, + name: role.name, + updatedAt: role.updatedAt.getTime(), + } + : null, + trialExpiryDate: user.trialExpiryDate.toISOString(), + updatedAt: user.updatedAt.getTime(), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'User', + }, + }; +}; + +export default getUsersMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-action-substeps.js b/packages/backend/test/mocks/rest/api/v1/apps/get-action-substeps.js new file mode 100644 index 0000000000000000000000000000000000000000..29c5f5be28d70b340d379feeff17c71fad4f342f --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-action-substeps.js @@ -0,0 +1,14 @@ +const getActionSubstepsMock = (substeps) => { + return { + data: substeps, + meta: { + count: substeps.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getActionSubstepsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-actions.js b/packages/backend/test/mocks/rest/api/v1/apps/get-actions.js new file mode 100644 index 0000000000000000000000000000000000000000..913d134a400d98076cfe130298e6e1cfca7c878f --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-actions.js @@ -0,0 +1,22 @@ +const getActionsMock = (actions) => { + const actionsData = actions.map((trigger) => { + return { + name: trigger.name, + key: trigger.key, + description: trigger.description, + }; + }); + + return { + data: actionsData, + meta: { + count: actions.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getActionsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-app.js b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js new file mode 100644 index 0000000000000000000000000000000000000000..e5b96c386c5536839bd09666233d5cf5e3ae7799 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-app.js @@ -0,0 +1,21 @@ +const getAppMock = (app) => { + return { + data: { + authDocUrl: app.authDocUrl, + iconUrl: app.iconUrl, + key: app.key, + name: app.name, + primaryColor: app.primaryColor, + supportsConnections: app.supportsConnections, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getAppMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js b/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js new file mode 100644 index 0000000000000000000000000000000000000000..a097d1f2c571de35bd1ed4c0bc3bee5411282c4d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-apps.js @@ -0,0 +1,23 @@ +const getAppsMock = (apps) => { + const appsData = apps.map((app) => ({ + authDocUrl: app.authDocUrl, + iconUrl: app.iconUrl, + key: app.key, + name: app.name, + primaryColor: app.primaryColor, + supportsConnections: app.supportsConnections, + })); + + return { + data: appsData, + meta: { + count: appsData.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getAppsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..3789ffe75aeecdcfd396d0f5255521259e4562be --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-client.js @@ -0,0 +1,19 @@ +const getAppAuthClientMock = (appAuthClient) => { + return { + data: { + name: appAuthClient.name, + id: appAuthClient.id, + appConfigId: appAuthClient.appConfigId, + active: appAuthClient.active, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'AppAuthClient', + }, + }; +}; + +export default getAppAuthClientMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js new file mode 100644 index 0000000000000000000000000000000000000000..0a697dec96acc75a055b5ddca20f31da61c4ea0b --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-auth-clients.js @@ -0,0 +1,18 @@ +const getAppAuthClientsMock = (appAuthClients) => { + return { + data: appAuthClients.map((appAuthClient) => ({ + name: appAuthClient.name, + id: appAuthClient.id, + active: appAuthClient.active, + })), + meta: { + count: appAuthClients.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'AppAuthClient', + }, + }; +}; + +export default getAppAuthClientsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js b/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js new file mode 100644 index 0000000000000000000000000000000000000000..68ea18cdf48fda1907aa8b418d8d714eccfea87a --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-auth.js @@ -0,0 +1,18 @@ +const getAuthMock = (auth) => { + return { + data: { + fields: auth.fields, + authenticationSteps: auth.authenticationSteps, + reconnectionSteps: auth.reconnectionSteps, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getAuthMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-config.js b/packages/backend/test/mocks/rest/api/v1/apps/get-config.js new file mode 100644 index 0000000000000000000000000000000000000000..5faee1181e63fa66626bdc420d9ec4af204a4971 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-config.js @@ -0,0 +1,24 @@ +const getAppConfigMock = (appConfig) => { + return { + data: { + id: appConfig.id, + key: appConfig.key, + allowCustomConnection: appConfig.allowCustomConnection, + shared: appConfig.shared, + disabled: appConfig.disabled, + canConnect: appConfig.canConnect, + canCustomConnect: appConfig.canCustomConnect, + createdAt: appConfig.createdAt.getTime(), + updatedAt: appConfig.updatedAt.getTime(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'AppConfig', + }, + }; +}; + +export default getAppConfigMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js new file mode 100644 index 0000000000000000000000000000000000000000..a6242e80250d7167a0a5a73385e95fc6681d1b4f --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-connections.js @@ -0,0 +1,25 @@ +const getConnectionsMock = (connections) => { + return { + data: connections.map((connection) => ({ + id: connection.id, + key: connection.key, + reconnectable: connection.reconnectable, + verified: connection.verified, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + })), + meta: { + count: connections.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default getConnectionsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-trigger-substeps.js b/packages/backend/test/mocks/rest/api/v1/apps/get-trigger-substeps.js new file mode 100644 index 0000000000000000000000000000000000000000..baad19d1a5c39eb87af7c1b5f4c2cf61f22d27f4 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-trigger-substeps.js @@ -0,0 +1,14 @@ +const getTriggerSubstepsMock = (substeps) => { + return { + data: substeps, + meta: { + count: substeps.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getTriggerSubstepsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/apps/get-triggers.js b/packages/backend/test/mocks/rest/api/v1/apps/get-triggers.js new file mode 100644 index 0000000000000000000000000000000000000000..cca01326dba4de1fd792163fca1d5d8d142a6742 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/apps/get-triggers.js @@ -0,0 +1,25 @@ +const getTriggersMock = (triggers) => { + const triggersData = triggers.map((trigger) => { + return { + description: trigger.description, + key: trigger.key, + name: trigger.name, + pollInterval: trigger.pollInterval, + showWebhookUrl: trigger.showWebhookUrl, + type: trigger.type, + }; + }); + + return { + data: triggersData, + meta: { + count: triggers.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getTriggersMock; diff --git a/packages/backend/test/mocks/rest/api/v1/automatisch/config.js b/packages/backend/test/mocks/rest/api/v1/automatisch/config.js new file mode 100644 index 0000000000000000000000000000000000000000..ba5cb83868f6d37d48f455d51a1a5a83014647b1 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/automatisch/config.js @@ -0,0 +1,28 @@ +const infoMock = ( + logoConfig, + primaryDarkConfig, + primaryLightConfig, + primaryMainConfig, + titleConfig +) => { + return { + data: { + disableFavicon: false, + disableNotificationsPage: false, + 'logo.svgData': logoConfig.value.data, + 'palette.primary.dark': primaryDarkConfig.value.data, + 'palette.primary.light': primaryLightConfig.value.data, + 'palette.primary.main': primaryMainConfig.value.data, + title: titleConfig.value.data, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default infoMock; diff --git a/packages/backend/test/mocks/rest/api/v1/automatisch/info.js b/packages/backend/test/mocks/rest/api/v1/automatisch/info.js new file mode 100644 index 0000000000000000000000000000000000000000..ec0716314f1e26032e1af8618b195ed356c903c7 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/automatisch/info.js @@ -0,0 +1,18 @@ +const infoMock = () => { + return { + data: { + isCloud: false, + isMation: false, + isEnterprise: true, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default infoMock; diff --git a/packages/backend/test/mocks/rest/api/v1/automatisch/license.js b/packages/backend/test/mocks/rest/api/v1/automatisch/license.js new file mode 100644 index 0000000000000000000000000000000000000000..1a02ebb6a8c0b4bfbfa140fd49b47c2c5a7cd84c --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/automatisch/license.js @@ -0,0 +1,19 @@ +const licenseMock = () => { + return { + data: { + expireAt: '2025-12-31T23:59:59Z', + id: '123', + name: 'license-name', + verified: true, + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default licenseMock; diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..f7b50194d2c47c3dbdf854c8b147ca526e0f029d --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution-steps.js @@ -0,0 +1,39 @@ +const getExecutionStepsMock = async (executionSteps, steps) => { + const data = executionSteps.map((executionStep) => { + const step = steps.find((step) => step.id === executionStep.stepId); + + return { + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + step: { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + }, + }; + }); + + return { + data: data, + meta: { + count: executionSteps.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'ExecutionStep', + }, + }; +}; + +export default getExecutionStepsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js new file mode 100644 index 0000000000000000000000000000000000000000..3957e9d81bc9fdb7772597bade566eaa60615d8f --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-execution.js @@ -0,0 +1,40 @@ +const getExecutionMock = async (execution, flow, steps) => { + const data = { + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + flow: { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: steps.map((step) => ({ + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + })), + }, + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Execution', + }, + }; +}; + +export default getExecutionMock; diff --git a/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js new file mode 100644 index 0000000000000000000000000000000000000000..21d36376ff8ec14b258988b7d8d5a0f4fee00e72 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/executions/get-executions.js @@ -0,0 +1,41 @@ +const getExecutionsMock = async (executions, flow, steps) => { + const data = executions.map((execution) => ({ + id: execution.id, + testRun: execution.testRun, + createdAt: execution.createdAt.getTime(), + updatedAt: execution.updatedAt.getTime(), + status: 'success', + flow: { + id: flow.id, + name: flow.name, + active: flow.active, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: steps.map((step) => ({ + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + })), + }, + })); + + return { + data: data, + meta: { + count: executions.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'Execution', + }, + }; +}; + +export default getExecutionsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..54032e1c8568d2765774b9d4e1b5afb27844c3da --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flow.js @@ -0,0 +1,34 @@ +const getFlowMock = async (flow, steps) => { + const data = { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: steps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + webhookUrl: step.webhookUrl, + })), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Flow', + }, + }; +}; + +export default getFlowMock; diff --git a/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js new file mode 100644 index 0000000000000000000000000000000000000000..0509aec32ae1067135161d654d887df5fae13c1b --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/flows/get-flows.js @@ -0,0 +1,38 @@ +const getFlowsMock = async (flows, steps) => { + const data = flows.map((flow) => { + const flowSteps = steps.filter((step) => step.flowId === flow.id); + + return { + active: flow.active, + id: flow.id, + name: flow.name, + status: flow.active ? 'published' : 'draft', + createdAt: flow.createdAt.getTime(), + updatedAt: flow.updatedAt.getTime(), + steps: flowSteps.map((step) => ({ + appKey: step.appKey, + iconUrl: step.iconUrl, + id: step.id, + key: step.key, + parameters: step.parameters, + position: step.position, + status: step.status, + type: step.type, + webhookUrl: step.webhookUrl, + })), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: 1, + isArray: true, + totalPages: 1, + type: 'Flow', + }, + }; +}; + +export default getFlowsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/payment/get-paddle-info.js b/packages/backend/test/mocks/rest/api/v1/payment/get-paddle-info.js new file mode 100644 index 0000000000000000000000000000000000000000..c7b8aec5468a9288704989dae1173f80285730ad --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/payment/get-paddle-info.js @@ -0,0 +1,17 @@ +const getPaddleInfoMock = async () => { + return { + data: { + sandbox: true, + vendorId: 'sampleVendorId', + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getPaddleInfoMock; diff --git a/packages/backend/test/mocks/rest/api/v1/payment/get-plans.js b/packages/backend/test/mocks/rest/api/v1/payment/get-plans.js new file mode 100644 index 0000000000000000000000000000000000000000..6cacbf827f50fab047188c06e050161dc9378e50 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/payment/get-plans.js @@ -0,0 +1,22 @@ +const getPaymentPlansMock = async () => { + return { + data: [ + { + limit: '10,000', + name: '10k - monthly', + price: '€20', + productId: '47384', + quota: 10000, + }, + ], + meta: { + count: 1, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getPaymentPlansMock; diff --git a/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.js b/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.js new file mode 100644 index 0000000000000000000000000000000000000000..3226e12e5e69bfc081d3041e1b583fc93b7f3235 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/saml-auth-providers/get-saml-auth-providers.js @@ -0,0 +1,23 @@ +const getSamlAuthProvidersMock = async (samlAuthProviders) => { + const data = samlAuthProviders.map((samlAuthProvider) => { + return { + id: samlAuthProvider.id, + name: samlAuthProvider.name, + loginUrl: samlAuthProvider.loginUrl, + issuer: samlAuthProvider.issuer, + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'SamlAuthProvider', + }, + }; +}; + +export default getSamlAuthProvidersMock; diff --git a/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js b/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js new file mode 100644 index 0000000000000000000000000000000000000000..e7b160577dd09b39afb5facf76f7c401210359e9 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/create-dynamic-fields.js @@ -0,0 +1,36 @@ +const createDynamicFieldsMock = async () => { + const data = [ + { + label: 'Bot name', + key: 'botName', + type: 'string', + required: true, + value: 'Automatisch', + description: + 'Specify the bot name which appears as a bold username above the message inside Slack. Defaults to Automatisch.', + variables: true, + }, + { + label: 'Bot icon', + key: 'botIcon', + type: 'string', + required: false, + description: + 'Either an image url or an emoji available to your team (surrounded by :). For example, https://example.com/icon_256.png or :robot_face:', + variables: true, + }, + ]; + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default createDynamicFieldsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..3f6c8abb18e9d164be96846195e8fee25e9df021 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-connection.js @@ -0,0 +1,27 @@ +const getConnectionMock = async (connection) => { + const data = { + id: connection.id, + key: connection.key, + verified: connection.verified, + reconnectable: connection.reconnectable, + appAuthClientId: connection.appAuthClientId, + formattedData: { + screenName: connection.formattedData.screenName, + }, + createdAt: connection.createdAt.getTime(), + updatedAt: connection.updatedAt.getTime(), + }; + + return { + data: data, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Connection', + }, + }; +}; + +export default getConnectionMock; diff --git a/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js new file mode 100644 index 0000000000000000000000000000000000000000..7b5515edcb7b1422b5667dbf55d3f9477b1e543c --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/steps/get-previous-steps.js @@ -0,0 +1,41 @@ +const getPreviousStepsMock = async (steps, executionSteps) => { + const data = steps.map((step) => { + const filteredExecutionSteps = executionSteps.filter( + (executionStep) => executionStep.stepId === step.id + ); + + return { + id: step.id, + type: step.type, + key: step.key, + appKey: step.appKey, + iconUrl: step.iconUrl, + webhookUrl: step.webhookUrl, + status: step.status, + position: step.position, + parameters: step.parameters, + executionSteps: filteredExecutionSteps.map((executionStep) => ({ + id: executionStep.id, + dataIn: executionStep.dataIn, + dataOut: executionStep.dataOut, + errorDetails: executionStep.errorDetails, + status: executionStep.status, + createdAt: executionStep.createdAt.getTime(), + updatedAt: executionStep.updatedAt.getTime(), + })), + }; + }); + + return { + data: data, + meta: { + count: data.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Step', + }, + }; +}; + +export default getPreviousStepsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-apps.js b/packages/backend/test/mocks/rest/api/v1/users/get-apps.js new file mode 100644 index 0000000000000000000000000000000000000000..79ac0e8a00353eb5ddb7fa37668d02d845d1d89b --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-apps.js @@ -0,0 +1,55 @@ +const getAppsMock = () => { + const appsData = [ + { + authDocUrl: 'https://automatisch.io/docs/apps/deepl/connection', + connectionCount: 1, + flowCount: 1, + iconUrl: 'http://localhost:3000/apps/deepl/assets/favicon.svg', + key: 'deepl', + name: 'DeepL', + primaryColor: '0d2d45', + supportsConnections: true, + }, + { + authDocUrl: 'https://automatisch.io/docs/apps/github/connection', + connectionCount: 1, + flowCount: 1, + iconUrl: 'http://localhost:3000/apps/github/assets/favicon.svg', + key: 'github', + name: 'GitHub', + primaryColor: '000000', + supportsConnections: true, + }, + { + authDocUrl: 'https://automatisch.io/docs/apps/slack/connection', + flowCount: 1, + iconUrl: 'http://localhost:3000/apps/slack/assets/favicon.svg', + key: 'slack', + name: 'Slack', + primaryColor: '4a154b', + supportsConnections: true, + }, + { + authDocUrl: 'https://automatisch.io/docs/apps/webhook/connection', + flowCount: 1, + iconUrl: 'http://localhost:3000/apps/webhook/assets/favicon.svg', + key: 'webhook', + name: 'Webhook', + primaryColor: '0059F7', + supportsConnections: false, + }, + ]; + + return { + data: appsData, + meta: { + count: appsData.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getAppsMock; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-current-user.js b/packages/backend/test/mocks/rest/api/v1/users/get-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..cfbd0f5444ed58dc2d064ff3e4e7aa5cd3d2e7e3 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-current-user.js @@ -0,0 +1,39 @@ +const getCurrentUserMock = (currentUser, role, permissions) => { + return { + data: { + createdAt: currentUser.createdAt.getTime(), + email: currentUser.email, + fullName: currentUser.fullName, + id: currentUser.id, + permissions: permissions.map((permission) => ({ + id: permission.id, + roleId: permission.roleId, + action: permission.action, + subject: permission.subject, + conditions: permission.conditions, + createdAt: permission.createdAt.getTime(), + updatedAt: permission.updatedAt.getTime(), + })), + role: { + createdAt: role.createdAt.getTime(), + description: null, + id: role.id, + isAdmin: role.isAdmin, + key: role.key, + name: role.name, + updatedAt: role.updatedAt.getTime(), + }, + trialExpiryDate: currentUser.trialExpiryDate.toISOString(), + updatedAt: currentUser.updatedAt.getTime(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'User', + }, + }; +}; + +export default getCurrentUserMock; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-invoices.ee.js b/packages/backend/test/mocks/rest/api/v1/users/get-invoices.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..a30c995be341e838b97b32ad0afb4277859b3d7e --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-invoices.ee.js @@ -0,0 +1,14 @@ +const getInvoicesMock = async (invoices) => { + return { + data: invoices, + meta: { + count: invoices.length, + currentPage: null, + isArray: true, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getInvoicesMock; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js b/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js new file mode 100644 index 0000000000000000000000000000000000000000..7c0d3bc56dff4b813c3c29548be8601d6306bfd1 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-subscription.js @@ -0,0 +1,27 @@ +const getSubscriptionMock = (subscription) => { + return { + data: { + id: subscription.id, + paddlePlanId: subscription.paddlePlanId, + paddleSubscriptionId: subscription.paddleSubscriptionId, + cancelUrl: subscription.cancelUrl, + updateUrl: subscription.updateUrl, + status: subscription.status, + nextBillAmount: subscription.nextBillAmount, + nextBillDate: subscription.nextBillDate.toISOString(), + lastBillDate: subscription.lastBillDate, + cancellationEffectiveDate: subscription.cancellationEffectiveDate, + createdAt: subscription.createdAt.getTime(), + updatedAt: subscription.updatedAt.getTime(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Subscription', + }, + }; +}; + +export default getSubscriptionMock; diff --git a/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js new file mode 100644 index 0000000000000000000000000000000000000000..7721aaf74843f918168ed46c48af8085a3750e69 --- /dev/null +++ b/packages/backend/test/mocks/rest/api/v1/users/get-user-trial.js @@ -0,0 +1,17 @@ +const getUserTrialMock = async (currentUser) => { + return { + data: { + inTrial: await currentUser.inTrial(), + expireAt: currentUser.trialExpiryDate.toISOString(), + }, + meta: { + count: 1, + currentPage: null, + isArray: false, + totalPages: null, + type: 'Object', + }, + }; +}; + +export default getUserTrialMock; diff --git a/packages/backend/test/setup/check-env-file.js b/packages/backend/test/setup/check-env-file.js new file mode 100644 index 0000000000000000000000000000000000000000..1250a63bf6aa4260b34a812fadee376502cdf914 --- /dev/null +++ b/packages/backend/test/setup/check-env-file.js @@ -0,0 +1,12 @@ +import path from 'path'; +import fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const testEnvFile = path.resolve(__dirname, '../../.env.test'); + +if (!fs.existsSync(testEnvFile)) { + throw new Error( + 'Test environment file (.env.test) not found! You can copy .env-example.test to .env.test and fill it with your own values.' + ); +} diff --git a/packages/backend/test/setup/global-hooks.js b/packages/backend/test/setup/global-hooks.js new file mode 100644 index 0000000000000000000000000000000000000000..7e4eee858473c6262ddec5f6e21e870f79aca6ec --- /dev/null +++ b/packages/backend/test/setup/global-hooks.js @@ -0,0 +1,32 @@ +import { Model } from 'objection'; +import { client as knex } from '../../src/config/database.js'; +import logger from '../../src/helpers/logger.js'; +import { vi } from 'vitest'; + +global.beforeAll(async () => { + global.knex = null; + logger.silent = true; + + // Remove default roles and permissions before running the test suite + await knex.raw('TRUNCATE TABLE config, roles, permissions CASCADE'); +}); + +global.beforeEach(async () => { + // It's assigned as global.knex for the convenience even though + // it's a transaction. It's rolled back after each test. + // by assigning to knex, we can use it as knex.table('example') in tests files. + global.knex = await knex.transaction(); + Model.knex(global.knex); +}); + +global.afterEach(async () => { + await global.knex.rollback(); + Model.knex(knex); + + vi.restoreAllMocks(); + vi.clearAllMocks(); +}); + +global.afterAll(async () => { + logger.silent = false; +}); diff --git a/packages/backend/test/setup/prepare-test-env.js b/packages/backend/test/setup/prepare-test-env.js new file mode 100644 index 0000000000000000000000000000000000000000..0fc0b9ed64d8119621629103a131d3a5ef143ae2 --- /dev/null +++ b/packages/backend/test/setup/prepare-test-env.js @@ -0,0 +1,26 @@ +import './check-env-file.js'; +import { createDatabaseAndUser } from '../../bin/database/utils.js'; +import { client as knex } from '../../src/config/database.js'; +import logger from '../../src/helpers/logger.js'; +import appConfig from '../../src/config/app.js'; + +const createAndMigrateDatabase = async () => { + if (!appConfig.CI) { + await createDatabaseAndUser(); + } + + const migrator = knex.migrate; + + await migrator.latest(); + + logger.info(`Completed database migrations for the test database.`); +}; + +createAndMigrateDatabase() + .then(() => { + process.exit(0); + }) + .catch((error) => { + logger.error(error); + process.exit(1); + }); diff --git a/packages/backend/vitest.config.js b/packages/backend/vitest.config.js new file mode 100644 index 0000000000000000000000000000000000000000..5f4ed9617d8c9989e12e32ebb278e94a1f5cd7f3 --- /dev/null +++ b/packages/backend/vitest.config.js @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + setupFiles: ['./test/setup/global-hooks.js'], + globals: true, + }, +}); diff --git a/packages/docs/package.json b/packages/docs/package.json new file mode 100644 index 0000000000000000000000000000000000000000..43e5b3b63cb119caad504cb137687421e4d4fd90 --- /dev/null +++ b/packages/docs/package.json @@ -0,0 +1,31 @@ +{ + "name": "@automatisch/docs", + "version": "0.10.0", + "license": "See LICENSE file", + "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", + "private": true, + "scripts": { + "dev": "vitepress dev pages --port 3002", + "build": "vitepress build pages", + "serve": "vitepress serve pages" + }, + "devDependencies": { + "sitemap": "^7.1.1", + "vitepress": "^1.0.0-alpha.21", + "vue": "^3.2.37" + }, + "contributors": [ + { + "name": "automatisch contributors", + "url": "https://github.com/automatisch/automatisch/graphs/contributors" + } + ], + "homepage": "https://github.com/automatisch/automatisch#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/automatisch/automatisch.git" + }, + "bugs": { + "url": "https://github.com/automatisch/automatisch/issues" + } +} diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js new file mode 100644 index 0000000000000000000000000000000000000000..219a1d51ecf5b3b0d10a8d97af1e680e98b623e4 --- /dev/null +++ b/packages/docs/pages/.vitepress/config.js @@ -0,0 +1,743 @@ +import { defineConfig } from 'vitepress'; +import { createWriteStream } from 'fs'; +import { resolve } from 'path'; +import { SitemapStream } from 'sitemap'; + +const BASE = process.env.BASE_URL || '/'; + +const links = []; +const PROD_BASE_URL = 'https://automatisch.io/docs'; + +export default defineConfig({ + base: BASE, + lang: 'en-US', + title: 'Automatisch Docs', + description: + 'Build workflow automation without spending time and money. No code is required.', + cleanUrls: 'with-subfolders', + ignoreDeadLinks: true, + themeConfig: { + siteTitle: 'Automatisch', + nav: [ + { + text: 'Guide', + link: '/', + activeMatch: '^/$|^/guide/', + }, + { + text: 'Apps', + link: '/apps/airtable/connection', + activeMatch: '/apps/', + }, + ], + sidebar: { + '/apps/': [ + { + text: 'Airtable', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/airtable/actions' }, + { text: 'Connection', link: '/apps/airtable/connection' }, + ], + }, + { + text: 'Appwrite', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/appwrite/triggers' }, + { text: 'Connection', link: '/apps/appwrite/connection' }, + ], + }, + { + text: 'Carbone', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/carbone/actions' }, + { text: 'Connection', link: '/apps/carbone/connection' }, + ], + }, + { + text: 'Datastore', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/datastore/actions' }, + { text: 'Connection', link: '/apps/datastore/connection' }, + ], + }, + { + text: 'DeepL', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/deepl/actions' }, + { text: 'Connection', link: '/apps/deepl/connection' }, + ], + }, + { + text: 'Delay', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/delay/actions' }, + { text: 'Connection', link: '/apps/delay/connection' }, + ], + }, + { + text: 'Discord', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/discord/actions' }, + { text: 'Connection', link: '/apps/discord/connection' }, + ], + }, + { + text: 'Disqus', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/disqus/triggers' }, + { text: 'Connection', link: '/apps/disqus/connection' }, + ], + }, + { + text: 'Dropbox', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/dropbox/actions' }, + { text: 'Connection', link: '/apps/dropbox/connection' }, + ], + }, + { + text: 'Filter', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/filter/actions' }, + { text: 'Connection', link: '/apps/filter/connection' }, + ], + }, + { + text: 'Flickr', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/flickr/triggers' }, + { text: 'Connection', link: '/apps/flickr/connection' }, + ], + }, + { + text: 'Formatter', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/formatter/actions' }, + { text: 'Connection', link: '/apps/formatter/connection' }, + ], + }, + { + text: 'Ghost', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/ghost/triggers' }, + { text: 'Connection', link: '/apps/ghost/connection' }, + ], + }, + { + text: 'GitHub', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/github/triggers' }, + { text: 'Actions', link: '/apps/github/actions' }, + { text: 'Connection', link: '/apps/github/connection' }, + ], + }, + { + text: 'GitLab', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/gitlab/triggers' }, + { text: 'Connection', link: '/apps/gitlab/connection' }, + ], + }, + { + text: 'Google Calendar', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/google-calendar/triggers' }, + { text: 'Connection', link: '/apps/google-calendar/connection' }, + ], + }, + { + text: 'Google Drive', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/google-drive/triggers' }, + { text: 'Connection', link: '/apps/google-drive/connection' }, + ], + }, + { + text: 'Google Forms', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/google-forms/triggers' }, + { text: 'Connection', link: '/apps/google-forms/connection' }, + ], + }, + { + text: 'Google Sheets', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/google-sheets/triggers' }, + { text: 'Actions', link: '/apps/google-sheets/actions' }, + { text: 'Connection', link: '/apps/google-sheets/connection' }, + ], + }, + { + text: 'Google Tasks', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/google-tasks/triggers' }, + { text: 'Actions', link: '/apps/google-tasks/actions' }, + { text: 'Connection', link: '/apps/google-tasks/connection' }, + ], + }, + { + text: 'HTTP Request', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/http-request/actions' }, + { text: 'Connection', link: '/apps/http-request/connection' }, + ], + }, + { + text: 'HubSpot', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/hubspot/actions' }, + { text: 'Connection', link: '/apps/hubspot/connection' }, + ], + }, + { + text: 'Invoice Ninja', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/invoice-ninja/triggers' }, + { text: 'Actions', link: '/apps/invoice-ninja/actions' }, + { text: 'Connection', link: '/apps/invoice-ninja/connection' }, + ], + }, + { + text: 'Mattermost', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/mattermost/actions' }, + { text: 'Connection', link: '/apps/mattermost/connection' }, + ], + }, + { + text: 'Miro', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/miro/actions' }, + { text: 'Connection', link: '/apps/miro/connection' }, + ], + }, + { + text: 'Notion', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/notion/triggers' }, + { text: 'Actions', link: '/apps/notion/actions' }, + { text: 'Connection', link: '/apps/notion/connection' }, + ], + }, + { + text: 'Ntfy', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/ntfy/actions' }, + { text: 'Connection', link: '/apps/ntfy/connection' }, + ], + }, + { + text: 'Odoo', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/odoo/actions' }, + { text: 'Connection', link: '/apps/odoo/connection' }, + ], + }, + { + text: 'OpenAI', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/openai/actions' }, + { text: 'Connection', link: '/apps/openai/connection' }, + ], + }, + { + text: 'Pipedrive', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/pipedrive/triggers' }, + { text: 'Actions', link: '/apps/pipedrive/actions' }, + { text: 'Connection', link: '/apps/pipedrive/connection' }, + ], + }, + { + text: 'Placetel', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/placetel/triggers' }, + { text: 'Connection', link: '/apps/placetel/connection' }, + ], + }, + { + text: 'PostgreSQL', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/postgresql/actions' }, + { text: 'Connection', link: '/apps/postgresql/connection' }, + ], + }, + { + text: 'Pushover', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/pushover/actions' }, + { text: 'Connection', link: '/apps/pushover/connection' }, + ], + }, + { + text: 'Reddit', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/reddit/triggers' }, + { text: 'Actions', link: '/apps/reddit/actions' }, + { text: 'Connection', link: '/apps/reddit/connection' }, + ], + }, + { + text: 'Remove.bg', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/removebg/actions' }, + { text: 'Connection', link: '/apps/removebg/connection' }, + ], + }, + { + text: 'RSS', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/rss/triggers' }, + { text: 'Connection', link: '/apps/rss/connection' }, + ], + }, + { + text: 'Salesforce', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/salesforce/triggers' }, + { text: 'Actions', link: '/apps/salesforce/actions' }, + { text: 'Connection', link: '/apps/salesforce/connection' }, + ], + }, + { + text: 'Scheduler', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/scheduler/triggers' }, + { text: 'Connection', link: '/apps/scheduler/connection' }, + ], + }, + { + text: 'SignalWire', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/signalwire/triggers' }, + { text: 'Actions', link: '/apps/signalwire/actions' }, + { text: 'Connection', link: '/apps/signalwire/connection' }, + ], + }, + { + text: 'Slack', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/slack/actions' }, + { text: 'Connection', link: '/apps/slack/connection' }, + ], + }, + { + text: 'SMTP', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/smtp/actions' }, + { text: 'Connection', link: '/apps/smtp/connection' }, + ], + }, + { + text: 'Spotify', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/spotify/actions' }, + { text: 'Connection', link: '/apps/spotify/connection' }, + ], + }, + { + text: 'Strava', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/strava/actions' }, + { text: 'Connection', link: '/apps/strava/connection' }, + ], + }, + { + text: 'Stripe', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/stripe/triggers' }, + { text: 'Connection', link: '/apps/stripe/connection' }, + ], + }, + { + text: 'Telegram', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/telegram-bot/actions' }, + { text: 'Connection', link: '/apps/telegram-bot/connection' }, + ], + }, + { + text: 'Todoist', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/todoist/triggers' }, + { text: 'Actions', link: '/apps/todoist/actions' }, + { text: 'Connection', link: '/apps/todoist/connection' }, + ], + }, + { + text: 'Trello', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/trello/actions' }, + { text: 'Connection', link: '/apps/trello/connection' }, + ], + }, + { + text: 'Twilio', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/twilio/triggers' }, + { text: 'Actions', link: '/apps/twilio/actions' }, + { text: 'Connection', link: '/apps/twilio/connection' }, + ], + }, + { + text: 'Twitter', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/twitter/triggers' }, + { text: 'Actions', link: '/apps/twitter/actions' }, + { text: 'Connection', link: '/apps/twitter/connection' }, + ], + }, + { + text: 'Typeform', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/typeform/triggers' }, + { text: 'Connection', link: '/apps/typeform/connection' }, + ], + }, + { + text: 'Vtiger CRM', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/vtiger-crm/triggers' }, + { text: 'Actions', link: '/apps/vtiger-crm/actions' }, + { text: 'Connection', link: '/apps/vtiger-crm/connection' }, + ], + }, + { + text: 'Webhooks', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/webhooks/triggers' }, + { text: 'Connection', link: '/apps/webhooks/connection' }, + ], + }, + { + text: 'WordPress', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/wordpress/triggers' }, + { text: 'Connection', link: '/apps/wordpress/connection' }, + ], + }, + { + text: 'Xero', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/xero/triggers' }, + { text: 'Connection', link: '/apps/xero/connection' }, + ], + }, + { + text: 'You Need A Budget', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/you-need-a-budget/triggers' }, + { text: 'Connection', link: '/apps/you-need-a-budget/connection' }, + ], + }, + { + text: 'Youtube', + collapsible: true, + collapsed: true, + items: [ + { text: 'Triggers', link: '/apps/youtube/triggers' }, + { text: 'Connection', link: '/apps/youtube/connection' }, + ], + }, + { + text: 'Zendesk', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/zendesk/actions' }, + { text: 'Connection', link: '/apps/zendesk/connection' }, + ], + }, + ], + '/': [ + { + text: 'Getting Started', + collapsible: true, + items: [ + { + text: 'What is Automatisch?', + link: '/', + activeMatch: '/', + }, + { text: 'Installation', link: '/guide/installation' }, + { text: 'Key concepts', link: '/guide/key-concepts' }, + { text: 'Create flow', link: '/guide/create-flow' }, + ], + }, + { + text: 'Integrations', + collapsible: true, + items: [ + { text: 'Available apps', link: '/guide/available-apps' }, + { + text: 'Request integration', + link: '/guide/request-integration', + }, + ], + }, + { + text: 'Advanced', + collapsible: true, + items: [ + { text: 'Configuration', link: '/advanced/configuration' }, + { text: 'Credentials', link: '/advanced/credentials' }, + { text: 'Telemetry', link: '/advanced/telemetry' }, + ], + }, + { + text: 'Contributing', + collapsible: true, + items: [ + { + text: 'Contribution guide', + link: '/contributing/contribution-guide', + }, + { + text: 'Development setup', + link: '/contributing/development-setup', + }, + { + text: 'Repository structure', + link: '/contributing/repository-structure', + }, + ], + }, + { + text: 'Build Integrations', + collapsible: true, + items: [ + { + text: 'Folder structure', + link: '/build-integrations/folder-structure', + }, + { + text: 'App', + link: '/build-integrations/app', + }, + { + text: 'Global variable', + link: '/build-integrations/global-variable', + }, + { + text: 'Auth', + link: '/build-integrations/auth', + }, + { + text: 'Triggers', + link: '/build-integrations/triggers', + }, + { + text: 'Actions', + link: '/build-integrations/actions', + }, + { + text: 'Examples', + link: '/build-integrations/examples', + }, + ], + }, + { + text: 'Other', + collapsible: true, + items: [ + { text: 'License', link: '/other/license' }, + { text: 'Community', link: '/other/community' }, + ], + }, + ], + }, + socialLinks: [ + { icon: 'github', link: 'https://github.com/automatisch/automatisch' }, + { icon: 'twitter', link: 'https://twitter.com/automatischio' }, + { icon: 'discord', link: 'https://discord.gg/dJSah9CVrC' }, + ], + editLink: { + pattern: + 'https://github.com/automatisch/automatisch/edit/main/packages/docs/pages/:path', + text: 'Edit this page on GitHub', + }, + footer: { + copyright: 'Copyright Β© 2022 Automatisch. All rights reserved.', + }, + algolia: { + appId: 'I7I8MRYC3P', + apiKey: '9325eb970bdd6a70b1e35528b39ed2fe', + indexName: 'automatisch', + }, + }, + + async transformHead(ctx) { + if (ctx.pageData.relativePath === '') return; // Skip 404 page. + + const isHomepage = ctx.pageData.relativePath === 'index.md'; + let canonicalUrl = PROD_BASE_URL; + + if (!isHomepage) { + canonicalUrl = + `${canonicalUrl}/` + ctx.pageData.relativePath.replace('.md', ''); + } + + // Added for logging purposes to check if there is something + // wrong with the canonical URL in the deployment pipeline. + console.log(''); + console.log('File path: ', ctx.pageData.relativePath); + console.log('Canonical URL: ', canonicalUrl); + + return [ + [ + 'link', + { + rel: 'canonical', + href: canonicalUrl, + }, + ], + [ + 'script', + { + defer: true, + 'data-domain': 'automatisch.io', + 'data-api': 'https://automatisch.io/data/api/event', + src: 'https://automatisch.io/data/js/script.js', + }, + ], + ]; + }, + + async transformHtml(_, id, { pageData }) { + if (!/[\\/]404\.html$/.test(id)) { + let url = pageData.relativePath.replace(/((^|\/)index)?\.md$/, '$2'); + + const isHomepage = url === ''; + + if (isHomepage) { + url = '/docs'; + } + + links.push({ + url, + lastmod: pageData.lastUpdated, + }); + } + }, + + async buildEnd({ outDir }) { + const sitemap = new SitemapStream({ + hostname: `${PROD_BASE_URL}/`, + }); + + const writeStream = createWriteStream(resolve(outDir, 'sitemap.xml')); + sitemap.pipe(writeStream); + links.forEach((link) => sitemap.write(link)); + sitemap.end(); + }, +}); diff --git a/packages/docs/pages/.vitepress/theme/CustomLayout.vue b/packages/docs/pages/.vitepress/theme/CustomLayout.vue new file mode 100644 index 0000000000000000000000000000000000000000..a2d391490841a006a81eba9a744d4ebc76a77bb2 --- /dev/null +++ b/packages/docs/pages/.vitepress/theme/CustomLayout.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/docs/pages/.vitepress/theme/custom.css b/packages/docs/pages/.vitepress/theme/custom.css new file mode 100644 index 0000000000000000000000000000000000000000..8ac3296cb226c2b1c574f07f5bd587b1779eb5fe --- /dev/null +++ b/packages/docs/pages/.vitepress/theme/custom.css @@ -0,0 +1,149 @@ +/** + * Colors + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-brand: #0059f7; + --vp-c-brand-light: #4789ff; + --vp-c-brand-lighter: #7eacff; + --vp-c-brand-lightest: #a5c5ff; + --vp-c-brand-dark: #001f52; + --vp-c-brand-darker: #001639; + --vp-c-brand-dimm: rgba(100, 108, 255, 0.08); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: var(--vp-c-brand-light); + --vp-button-brand-text: var(--vp-c-text-dark-1); + --vp-button-brand-bg: var(--vp-c-brand); + --vp-button-brand-hover-border: var(--vp-c-brand-light); + --vp-button-brand-hover-text: var(--vp-c-text-dark-1); + --vp-button-brand-hover-bg: var(--vp-c-brand-light); + --vp-button-brand-active-border: var(--vp-c-brand-light); + --vp-button-brand-active-text: var(--vp-c-text-dark-1); + --vp-button-brand-active-bg: var(--vp-button-brand-bg); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #bd34fe 30%, + #41d1ff + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #bd34fe 50%, + #47caff 50% + ); + --vp-home-hero-image-filter: blur(40px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(72px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: var(--vp-c-brand); + --vp-custom-block-tip-text: var(--vp-c-brand-darker); + --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); +} + +.dark { + --vp-custom-block-tip-border: var(--vp-c-brand); + --vp-custom-block-tip-text: var(--vp-c-brand-lightest); + --vp-custom-block-tip-bg: var(--vp-c-brand-dimm); +} + +/** + * Component: Algolia + * -------------------------------------------------------------------------- */ + +.DocSearch { + --docsearch-primary-color: var(--vp-c-brand) !important; +} + +/** + * VitePress: Custom fix + * -------------------------------------------------------------------------- */ + +/* + Use lighter colors for links in dark mode for a11y. + Also specify some classes twice to have higher specificity + over scoped class data attribute. +*/ +.dark .vp-doc a, +.dark .vp-doc a > code, +.dark .VPNavBarMenuLink.VPNavBarMenuLink:hover, +.dark .VPNavBarMenuLink.VPNavBarMenuLink.active, +.dark .link.link:hover, +.dark .link.link.active, +.dark .edit-link-button.edit-link-button, +.dark .pager-link .title { + color: var(--vp-c-brand-lighter); +} + +.dark .vp-doc a:hover, +.dark .vp-doc a > code:hover { + color: var(--vp-c-brand-lightest); + opacity: 1; +} + +/* Transition by color instead of opacity */ +.dark .vp-doc .custom-block a { + transition: color 0.25s; +} + +:root { + overflow-y: scroll; + + --announcement-bar-height: 50px; +} + +.VPTeamMembersItem .avatar-img { + top: 50%; + transform: translateY(-50%); +} + +header.VPNav { + margin-top: 50px; +} + +.VPNavScreen.VPNavScreen { + top: calc(var(--announcement-bar-height) + var(--vp-nav-height-mobile)); +} + +.VPLocalNav.VPLocalNav { + top: 50px; +} + +aside.VPSidebar { + margin-top: 50px; +} + +@media (min-width: 960px) { + #VPContent { + margin-top: 50px; + } +} diff --git a/packages/docs/pages/.vitepress/theme/index.js b/packages/docs/pages/.vitepress/theme/index.js new file mode 100644 index 0000000000000000000000000000000000000000..75eaece3b711b9da963005493820d4703763ac48 --- /dev/null +++ b/packages/docs/pages/.vitepress/theme/index.js @@ -0,0 +1,8 @@ +import DefaultTheme from 'vitepress/theme'; +import './custom.css'; +import CustomLayout from './CustomLayout.vue'; + +export default { + ...DefaultTheme, + Layout: CustomLayout, +}; diff --git a/packages/docs/pages/advanced/configuration.md b/packages/docs/pages/advanced/configuration.md new file mode 100644 index 0000000000000000000000000000000000000000..a6635034c716061e13d710c6c0de06e6b5e82015 --- /dev/null +++ b/packages/docs/pages/advanced/configuration.md @@ -0,0 +1,46 @@ +# Configuration + +## How to set environment variables? + +You can modify the `docker-compose.yml` file to override environment variables. Please do not forget to change in `main` and `worker` services of docker-compose since the following variables might be used in both. + +## Environment Variables + +:::warning +The default values for some environment variables might be different in our development setup but following table shows the default values for docker-compose setup, which is the recommended way to run the application. +::: + +:::danger +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. +::: + +| Variable Name | Type | Default Value | Description | +| ---------------------------- | ------- | ------------------ | ----------------------------------------------------------------------------------- | +| `HOST` | string | `localhost` | HTTP Host | +| `PROTOCOL` | string | `http` | HTTP Protocol | +| `PORT` | string | `3000` | HTTP Port | +| `APP_ENV` | string | `production` | Automatisch Environment | +| `WEB_APP_URL` | string | | Can be used to override connection URLs and CORS URL | +| `WEBHOOK_URL` | string | | Can be used to override webhook URL | +| `LOG_LEVEL` | string | `info` | Can be used to configure log level such as `error`, `warn`, `info`, `http`, `debug` | +| `POSTGRES_DATABASE` | string | `automatisch` | Database Name | +| `POSTGRES_SCHEMA` | string | `public` | Database Schema | +| `POSTGRES_PORT` | number | `5432` | Database Port | +| `POSTGRES_ENABLE_SSL` | boolean | `false` | Enable/Disable SSL for the database | +| `POSTGRES_HOST` | string | `postgres` | Database Host | +| `POSTGRES_USERNAME` | string | `automatisch_user` | Database User | +| `POSTGRES_PASSWORD` | string | | Password of Database User | +| `ENCRYPTION_KEY` | string | | Encryption Key to store credentials | +| `WEBHOOK_SECRET_KEY` | string | | Webhook Secret Key to verify webhook requests | +| `APP_SECRET_KEY` | string | | Secret Key to authenticate the user | +| `REDIS_HOST` | string | `redis` | Redis Host | +| `REDIS_PORT` | number | `6379` | Redis Port | +| `REDIS_USERNAME` | string | | Redis Username | +| `REDIS_PASSWORD` | string | | Redis Password | +| `REDIS_TLS` | boolean | `false` | Redis TLS | +| `TELEMETRY_ENABLED` | boolean | `true` | Enable/Disable Telemetry | +| `ENABLE_BULLMQ_DASHBOARD` | boolean | `false` | Enable BullMQ Dashboard | +| `BULLMQ_DASHBOARD_USERNAME` | string | | Username to login BullMQ Dashboard | +| `BULLMQ_DASHBOARD_PASSWORD` | string | | Password to login BullMQ Dashboard | +| `DISABLE_NOTIFICATIONS_PAGE` | boolean | `false` | Enable/Disable notifications page | +| `DISABLE_FAVICON` | boolean | `false` | Enable/Disable favicon | diff --git a/packages/docs/pages/advanced/credentials.md b/packages/docs/pages/advanced/credentials.md new file mode 100644 index 0000000000000000000000000000000000000000..e30a88a3b1093e297ec0d6d9b191a1dabff418ed --- /dev/null +++ b/packages/docs/pages/advanced/credentials.md @@ -0,0 +1,9 @@ +# Credentials + +We need to store your credentials in order to automatically communicate with third-party services to fetch and send data when you have connections. It's the nature of our software and how automation works, but we take extra measures to keep your third-party credentials safe and secure. + +Automatisch uses AES specification to encrypt and decrypt your credentials of third-party services. The Advanced Encryption Standard (AES) is a U.S. Federal Information Processing Standard (FIPS). It was selected after a 5-year process where 15 competing designs were evaluated. AES is now used worldwide to protect sensitive information. + +:::danger +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. +::: diff --git a/packages/docs/pages/advanced/telemetry.md b/packages/docs/pages/advanced/telemetry.md new file mode 100644 index 0000000000000000000000000000000000000000..d1bdb8e6b1f401e95166363728781d76cef3427d --- /dev/null +++ b/packages/docs/pages/advanced/telemetry.md @@ -0,0 +1,33 @@ +# Telemetry + +:::info +We want to be very transparent about the data we collect and how we use it. Therefore, we have abstracted all of the code we use with our telemetry system into a single, easily accessible place. You can check the code [here](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/helpers/telemetry/index.js) and let us know if you have any suggestions for changes. +::: + +Automatisch comes with a built-in telemetry system that collects anonymous usage data. This data is used to help us improve the product and to make sure we are focusing on the right features. While we're doing it, we don't collect any personal information. You can also disable the telemetry system by setting the `TELEMETRY_ENABLED` environment variable. See the [environment variables](/advanced/configuration#environment-variables) section for more information. + +## What Automatisch collects? + +- Flow, step, and connection data without any credentials. +- Execution and execution steps data without any payload or identifiable information. +- Organization and instance IDs. Those are random IDs we assign when you install Automatisch. They're helpful when we evaluate how many instances are running and how many organizations are using Automatisch. +- Diagnostic information + - Automatisch version + - Service type (main service or worker service) + - Operating system type and version + - CPU and memory information + +## What Automatisch do not collect? + +- Personal information +- Your credentials of third party services +- Email and password used with Automatisch +- Error payloads + +## How to disable telemetry? + +Telemetry is enabled by default. If you want to disable it, you can do so by setting the `TELEMETRY_ENABLED` environment variable to `false` in `docker-compose.yml` file. + +## How data collection works? + +Automatisch collects data with events associated with custom user actions. We send the data to our servers whenever the user triggers those custom actions. Apart from events that are triggered by user actions, we also collect diagnostic information every six hours. diff --git a/packages/docs/pages/apps/airtable/actions.md b/packages/docs/pages/apps/airtable/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..432e0d4656eff47ddb23ad2078c9d0467d1c9cc9 --- /dev/null +++ b/packages/docs/pages/apps/airtable/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/airtable.svg +items: + - name: Create record + desc: Creates a new record with fields that automatically populate. + - name: Find record + desc: Finds a record using simple field search or use Airtable's formula syntax to find a matching record. +--- + + + + diff --git a/packages/docs/pages/apps/airtable/connection.md b/packages/docs/pages/apps/airtable/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..57bcd787473b8376a226715fdd3a3e308629f238 --- /dev/null +++ b/packages/docs/pages/apps/airtable/connection.md @@ -0,0 +1,19 @@ +# Airtable + +:::info +This page explains the steps you need to follow to set up the Airtable +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to your [Airtable account](https://www.airtable.com/). +2. Go to this [link](https://airtable.com/create/oauth) and click on the **Register new OAuth integration**. +3. Fill the name field. +4. Copy **OAuth Redirect URL** from Automatisch to **OAuth redirect URL** field. +5. Click on the **Register integration** button. +6. In **Developer Details** section, click on the **Generate client secret**. +7. Check the checkboxes of **Scopes** section. +8. Click on the **Save changes** button. +9. Copy **Client ID** to **Client ID** field on Automatisch. +10. Copy **Client secret** to **Client secret** field on Automatisch. +11. Click **Submit** button on Automatisch. +12. Congrats! Start using your new Airtable connection within the flows. diff --git a/packages/docs/pages/apps/appwrite/connection.md b/packages/docs/pages/apps/appwrite/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..1a0d91c409cf31dfcbf300485b0c2c31fcca929a --- /dev/null +++ b/packages/docs/pages/apps/appwrite/connection.md @@ -0,0 +1,20 @@ +# Appwrite + +:::info +This page explains the steps you need to follow to set up the Appwrite +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to your Appwrite account: [https://appwrite.io/](https://appwrite.io/). +2. Go to your project's **Settings**. +3. In the Settings, click on the **View API Keys** button in **API credentials** section. +4. Click on the **Create API Key** button. +5. Fill the name field and select **Never** for the expiration date. +6. Click on the **Next** button. +7. Click on the **Select all** and then click on the **Create** button. +8. Now, copy your **API key secret** and paste the key into the **API Key** field in Automatisch. +9. Write any screen name to be displayed in Automatisch. +10. You can find your project ID next to your project name. Paste the id into **Project ID** field in Automatsich. +11. If you are using self-hosted Appwrite project, you can paste the instace url into **Appwrite instance URL** field in Automatisch. +12. Fill the host name field with the hostname of your instance URL. It's either `cloud.appwrite.io` or hostname of your instance URL. +13. Start using Appwrite integration with Automatisch! diff --git a/packages/docs/pages/apps/appwrite/triggers.md b/packages/docs/pages/apps/appwrite/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..2177a8a90a48b61198844f7d80e78ad0619a22c6 --- /dev/null +++ b/packages/docs/pages/apps/appwrite/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/appwrite.svg +items: + - name: New documets + desc: Triggers when a new document is created. +--- + + + + diff --git a/packages/docs/pages/apps/carbone/actions.md b/packages/docs/pages/apps/carbone/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..f67b5ae99b1fc94dcb531f896253d75b49bcf9ca --- /dev/null +++ b/packages/docs/pages/apps/carbone/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/carbone.svg +items: + - name: Add Template + desc: Adds a template in xml/html format to your Carbone account. +--- + + + + diff --git a/packages/docs/pages/apps/carbone/connection.md b/packages/docs/pages/apps/carbone/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..598e4dfa122501b642fd031963ea4a01fbc456fe --- /dev/null +++ b/packages/docs/pages/apps/carbone/connection.md @@ -0,0 +1,10 @@ +# Carbone + +:::info +This page explains the steps you need to follow to set up the Carbone +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to your Carbone account: [https://account.carbone.io/](https://account.carbone.io/). +2. Copy either `Test API key` or `Production API key` from the page to the `API Key` field on Automatisch. +3. Now, you can start using the Carbone connection with Automatisch. diff --git a/packages/docs/pages/apps/datastore/actions.md b/packages/docs/pages/apps/datastore/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..d1c3c36d21f3b6e620d782263d9b6a0fb957907c --- /dev/null +++ b/packages/docs/pages/apps/datastore/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/datastore.svg +items: + - name: Get value + desc: Get value from the persistent datastore. + - name: Set value + desc: Set value to the persistent datastore. +--- + + + + diff --git a/packages/docs/pages/apps/datastore/connection.md b/packages/docs/pages/apps/datastore/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..f8a593fa20598f1e5143ac164e7de4394c2bc743 --- /dev/null +++ b/packages/docs/pages/apps/datastore/connection.md @@ -0,0 +1,3 @@ +# Datastore + +Datastore is a persistent key-value storage system that allows you to store and retrieve data. Currently you can use it within the scope of the flow, meaning you can store and retrieve data within the same flow. diff --git a/packages/docs/pages/apps/deepl/actions.md b/packages/docs/pages/apps/deepl/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..3b042d6fa2983d3532bf194340c4a993d67fe941 --- /dev/null +++ b/packages/docs/pages/apps/deepl/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/deepl.svg +items: + - name: Translate text + desc: Translates text from one language to another. +--- + + + + diff --git a/packages/docs/pages/apps/deepl/connection.md b/packages/docs/pages/apps/deepl/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..e644fe58453cef7416e69c6bb73eb61c8e8291dc --- /dev/null +++ b/packages/docs/pages/apps/deepl/connection.md @@ -0,0 +1,8 @@ +# DeepL + +1. Go to [your account page](https://www.deepl.com/account/summary) on DeepL. +2. Scroll down and copy `Authentication Key for DeepL API`. +3. Paste the key into the `Authentication Key` field in Automatisch. +4. Write any screen name to be displayed in Automatisch. +5. Click `Save`. +6. Start using DeepL integration with Automatisch! diff --git a/packages/docs/pages/apps/delay/actions.md b/packages/docs/pages/apps/delay/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..6254b082b9c30e5fd2de0734710aa89d0058e523 --- /dev/null +++ b/packages/docs/pages/apps/delay/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/delay.svg +items: + - name: Delay for + desc: Delays the execution of the next action by a specified amount of time. + - name: Delay until + desc: Delays the execution of the next action until a specified date. +--- + + + + diff --git a/packages/docs/pages/apps/delay/connection.md b/packages/docs/pages/apps/delay/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..a2eec35fd993175ab1afd00a3e6c4e2e838ebb42 --- /dev/null +++ b/packages/docs/pages/apps/delay/connection.md @@ -0,0 +1,3 @@ +# Delay + +Delay is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Delay app. It can be used only as an action and it delays the execution of the next action by a specified amount of time. diff --git a/packages/docs/pages/apps/discord/actions.md b/packages/docs/pages/apps/discord/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..111a90e01879e43d8a253f6838095f9fc3b0b474 --- /dev/null +++ b/packages/docs/pages/apps/discord/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/discord.svg +items: + - name: Send a message to channel + desc: Sends a message to a specific channel you specify. + - name: Create a scheduled event + desc: Creates a scheduled event. +--- + + + + diff --git a/packages/docs/pages/apps/discord/connection.md b/packages/docs/pages/apps/discord/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..10e9ee8df9e62237b7ac2f9b802d277cb966b202 --- /dev/null +++ b/packages/docs/pages/apps/discord/connection.md @@ -0,0 +1,26 @@ +# Discord + +:::info +This page explains the steps you need to follow to set up the Discord +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://discord.com/developers/applications) to register a **new application** on Discord. +1. Fill **Name**. +1. Check the checkboxes. +1. Click on the **create** button. +1. Go to **OAuth2** > **General** page. +1. Copy the **Client ID** and save it to use later. +1. Reset the **Client secret** to get the initial client secret and copy it to use later. +1. Click the **Add Redirect** button to define a redirect URI. +1. Copy **OAuth Redirect URL** from Automatisch to **Redirect** field. +1. Save the changes. +1. Go to **Bot** page. +1. Click **Add Bot** button. +1. Acknowledge the warning and click **Yes, do it!**. +1. Click **Reset Token** to get the initial bot token and copy it to use later. +1. Fill the **Consumer key** field with the **Client ID** value we copied. +1. Fill the **Consumer secret** field with the **Client Secret** value we copied. +1. Fill the **Bot token** field with the **Bot Token** value we copied. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new Discord connection within the flows. diff --git a/packages/docs/pages/apps/disqus/connection.md b/packages/docs/pages/apps/disqus/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..b03539cb9e42f481f55a0db1e6a2bcc4b26bf520 --- /dev/null +++ b/packages/docs/pages/apps/disqus/connection.md @@ -0,0 +1,19 @@ +# Disqus + +:::info +This page explains the steps you need to follow to set up the Disqus +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to the [Disqus](https://disqus.com/). +2. Go to the [API applications page](https://disqus.com/api/applications/) and click on the **Register new application** button. +3. Fill the **Register Application** form fields. +4. Click on the **Register my application** button. +5. Go to the **Authentication** section and select **Read, Write, and Manage Forums** option. +6. Copy **OAuth Redirect URL** from Automatisch to **Callback URL** field. +7. Click on the **Save Changes** button. +8. Go to the **Details** tab. +9. Copy **API Key** to **API Key** field on Automatisch. +10. Copy **API Secret** to **API Secret** field on Automatisch. +11. Click **Submit** button on Automatisch. +12. Congrats! Start using your new Disqus connection within the flows. diff --git a/packages/docs/pages/apps/disqus/triggers.md b/packages/docs/pages/apps/disqus/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..a40273ab228c828982539794aef0e963b062d68f --- /dev/null +++ b/packages/docs/pages/apps/disqus/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/disqus.svg +items: + - name: New comments + desc: Triggers when a new comment is posted in a forum using Disqus. + - name: New flagged comments + desc: Triggers when a Disqus comment is marked with a flag. +--- + + + + diff --git a/packages/docs/pages/apps/dropbox/actions.md b/packages/docs/pages/apps/dropbox/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..4d5d4b3eaf735a4ccaafbb036d719b0b26e0876a --- /dev/null +++ b/packages/docs/pages/apps/dropbox/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/dropbox.svg +items: + - name: Create a folder + desc: Creates a new folder with the given parent folder and folder name. + - name: Rename a file + desc: Rename a file with the given file path and new name. +--- + + + + diff --git a/packages/docs/pages/apps/dropbox/connection.md b/packages/docs/pages/apps/dropbox/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..886961429a5ac647ca08bd65930829e5849f6df7 --- /dev/null +++ b/packages/docs/pages/apps/dropbox/connection.md @@ -0,0 +1,20 @@ +# Dropbox + +:::info +This page explains the steps you need to follow to set up the Dropbox +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.dropbox.com/developers/apps) to create a **new application** on Dropbox. +1. Choose the "Scoped access" option in the "Choose an API" section. +1. Choose the "Full Dropbox" option in the "Choose the type of access you need" section. +1. Name your application. +1. Click on the **Create app** button. +1. Copy **OAuth Redirect URL** from Automatisch to **Redirect URIs** field and add it. +1. Click on the **Scoped App** link in the "Permission type" section. +1. Check the checkbox for the "files.content.write" scope and click on the **Submit** button. +1. Go back to the "Settings" tab. +1. Copy **App key** to **App key** field on Automatisch. +1. Copy **App secret** to **App secret** field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new Dropbox connection within the flows. diff --git a/packages/docs/pages/apps/filter/actions.md b/packages/docs/pages/apps/filter/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..319e976129a1b1917d6db1afb63a28c55c911c36 --- /dev/null +++ b/packages/docs/pages/apps/filter/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/filter.svg +items: + - name: Continue if conditions match + desc: Let the execution continue if the conditions match. +--- + + + + diff --git a/packages/docs/pages/apps/filter/connection.md b/packages/docs/pages/apps/filter/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..742b0667b5bbd8980e35716710330fa521b3d1a3 --- /dev/null +++ b/packages/docs/pages/apps/filter/connection.md @@ -0,0 +1,12 @@ +# Filter + +Filter is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Filter app. It can be used as an action and it filters the flow based on the given conditions. Available conditions are: + +- is equal +- is not equal +- is greater than +- is less than +- is greater than or equal +- is less than or equal +- contains +- does not contain diff --git a/packages/docs/pages/apps/flickr/connection.md b/packages/docs/pages/apps/flickr/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..3558e94ad93bb04ab82b9bfab4d6bc900bd35d26 --- /dev/null +++ b/packages/docs/pages/apps/flickr/connection.md @@ -0,0 +1,22 @@ +# Flickr + +:::info +This page explains the steps you need to follow to set up the Flickr +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.flickr.com/services/apps/create/) to **create an + app** on Flickr API. +2. Click **Request an API key**. +3. Apply for a non-commercial key. +4. Fill the field of **What is the name of your app?**. +5. Fill the field of **What are you building?**. +6. Check the checkboxes. +7. Click on **Submit** button. +8. Copy **Key** and **Key Secret** values and save them to use later. +9. Click **Edit auth flow for this app** to configure "Callback URL". +10. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URL** field. +11. Click **Save changes**. +12. Paste **Key** and **Secret** values you have saved from the 8th step and paste them into Automatisch as **Consumer Key** and **Consumer Secret**, respectively. +13. Click **Submit** button on Automatisch. +14. Now, you can start using the Flickr connection with Automatisch. diff --git a/packages/docs/pages/apps/flickr/triggers.md b/packages/docs/pages/apps/flickr/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..6067900b62a7da17d7dda67d48a674c9ec700c39 --- /dev/null +++ b/packages/docs/pages/apps/flickr/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/flickr.svg +items: + - name: New albums + desc: Triggers when you create a new album. + - name: New favorite photos + desc: Triggers when you favorite a photo. + - name: New photos + desc: Triggers when you add a new photo. + - name: New photos in album + desc: Triggers when you add a new photo in an album. +--- + + + + diff --git a/packages/docs/pages/apps/formatter/actions.md b/packages/docs/pages/apps/formatter/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..acceaa49cc545d0931e150c5e5909e1d18dbfb85 --- /dev/null +++ b/packages/docs/pages/apps/formatter/actions.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/formatter.svg +items: + - name: Text + desc: Transform text data to capitalize, extract emails, apply default value, and much more. + - name: Numbers + desc: Transform numbers to perform math operations, generate random numbers, format numbers, and much more. + - name: Date / Time + desc: Perform date and time related transformations on your data. +--- + + + + diff --git a/packages/docs/pages/apps/formatter/connection.md b/packages/docs/pages/apps/formatter/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..772de8bb454510c55fbe3f05d52650cab83e1ad7 --- /dev/null +++ b/packages/docs/pages/apps/formatter/connection.md @@ -0,0 +1,26 @@ +# Formatter + +Formatter is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Formatter app. It can be used as an action, and you can use it to format the data from the previous steps. It can be used to format the data in the following ways. + +## Text + +- Capitalize +- Convert HTML to Markdown +- Convert Markdown to HTML +- Extract Email Address +- Extract Number +- Lowercase +- Pluralize +- Replace +- Trim Whitespace +- Use Default Value + +## Numbers + +- Perform Math Operation +- Random Number +- Format Number + +## Date / Time + +- Format Date / Time diff --git a/packages/docs/pages/apps/ghost/connection.md b/packages/docs/pages/apps/ghost/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..2de3528b49bc6d00fa84a77683003148dd0dc874 --- /dev/null +++ b/packages/docs/pages/apps/ghost/connection.md @@ -0,0 +1,13 @@ +# Ghost + +:::info +This page explains the steps you need to follow to set up the Ghost connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to your Ghost Admin panel. +2. Click on the **Integrations** button. +3. Click on the **Add custom integration** button and create Admin API key. +4. Add your Admin API Key in the **Admin API Key** field on Automatisch. +5. Add your API URL in the **Instance URL** field on Automatisch. +6. Click **Submit** button on Automatisch. +7. Congrats! Start using your new Ghost connection within the flows. diff --git a/packages/docs/pages/apps/ghost/triggers.md b/packages/docs/pages/apps/ghost/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..dbb88b3b1ed22ae1bcf0e3544fa5f7bfd4dfe1b7 --- /dev/null +++ b/packages/docs/pages/apps/ghost/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/ghost.svg +items: + - name: New post published + desc: Triggers when a new post is published. +--- + + + + diff --git a/packages/docs/pages/apps/github/actions.md b/packages/docs/pages/apps/github/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..b5ae61bdcf8d942da5639841f0edb370439d1359 --- /dev/null +++ b/packages/docs/pages/apps/github/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/github.svg +items: + - name: Create issue + desc: Creates a new issue. +--- + + + + diff --git a/packages/docs/pages/apps/github/connection.md b/packages/docs/pages/apps/github/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..28acfc1a05bce327a524aeb857ccbca28b291c25 --- /dev/null +++ b/packages/docs/pages/apps/github/connection.md @@ -0,0 +1,15 @@ +# Github + +:::info +This page explains the steps you need to follow to set up the Github +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://github.com/settings/applications/new) to register a **new OAuth application** on Github. +2. Fill **Application name** and **Homepage URL**. +3. Copy **OAuth Redirect URL** from Automatisch to **Authorization callback URL** field on Github page. +4. Click on the **Register application** button on the Github page. +5. Copy the **Client ID** value from the following page to the `Client ID` field on Automatisch. +6. Click **Generate a new client secret** on the Github page and copy generated value into the `Client Secret` field on Automatisch. +7. Click **Submit** button on Automatisch. +8. Congrats! Start using your new Github connection within the flows. diff --git a/packages/docs/pages/apps/github/triggers.md b/packages/docs/pages/apps/github/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..ec82d3b3a182a770450dcb40f1858f1012d08cbb --- /dev/null +++ b/packages/docs/pages/apps/github/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/github.svg +items: + - name: New issues + desc: Triggers when a new issue is created. + - name: New pull requests + desc: Triggers when a new pull request is created. + - name: New stargazers + desc: Triggers when a user stars a repository. + - name: New watchers + desc: Triggers when a user watches a repository. +--- + + + + diff --git a/packages/docs/pages/apps/gitlab/connection.md b/packages/docs/pages/apps/gitlab/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..d4523024b8ffd98fd8fcbe8ed5596db79b658a21 --- /dev/null +++ b/packages/docs/pages/apps/gitlab/connection.md @@ -0,0 +1,18 @@ +# Gitlab + +:::info +This page explains the steps you need to follow to set up the Gitlab +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://gitlab.com/-/profile/applications) to register a **new OAuth application** on Gitlab. +2. Fill application **Name**. +3. Copy **OAuth Redirect URL** from Automatisch to **Redirect URI** field on Gitlab page. +4. Mark the **Confidential** field on Gitlab page. +5. Mark the **api** and **read_user** in **Scopes** section on Gitlab page. +6. Click on the **Save application** button at the end of the form on Gitlab page. +7. Copy the **Application ID** value from the following page to the `Client ID` field on Automatisch. +8. Copy the **Secret** value from the same page to the `Client Secret` field on Automatisch. +9. Click **Continue** button on Gitlab page. +10. Click **Submit** button on Automatisch. +11. Congrats! Start using your new Github connection within the flows. diff --git a/packages/docs/pages/apps/gitlab/triggers.md b/packages/docs/pages/apps/gitlab/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..b2a2efb9941399a1e66a910181c06c150290a5a6 --- /dev/null +++ b/packages/docs/pages/apps/gitlab/triggers.md @@ -0,0 +1,36 @@ +--- +favicon: /favicons/gitlab.svg +items: + - name: Confidential issue event + desc: Triggers when a new confidential issue is created or an existing issue is updated, closed, or reopened. + - name: Confidential comment event + desc: Triggers when a new confidential comment is made on commits, merge requests, issues, and code snippets. + - name: Deployment event + desc: Triggers when a deployment starts, succeeds, fails or is canceled. + - name: Feature flag event + desc: Triggers when a feature flag is turned on or off. + - name: Issue event + desc: Triggers when a new issue is created or an existing issue is updated, closed, or reopened. + - name: Job event + desc: Triggers when the status of a job changes. + - name: Merge request event + desc: Triggers when merge request is created, updated, or closed. + - name: Comment event + desc: Triggers when a new comment is made on commits, merge requests, issues, and code snippets. + - name: Pipeline event + desc: Triggers when the status of a pipeline changes. + - name: Push event + desc: Triggers when you push to the repository. + - name: Release event + desc: Triggers when a release is created or updated. + - name: Tag event + desc: Triggers when you create or delete tags in the repository. + - name: Wiki page event + desc: Triggers when a wiki page is created, updated, or deleted. +--- + + + + diff --git a/packages/docs/pages/apps/google-calendar/connection.md b/packages/docs/pages/apps/google-calendar/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..6024e6d84446a65dec9fe90094f1ba6201b62609 --- /dev/null +++ b/packages/docs/pages/apps/google-calendar/connection.md @@ -0,0 +1,28 @@ +# Google Calendar + +:::info +This page explains the steps you need to follow to set up the Google Calendar +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **Google Calendar API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **People API**. +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Google Calendar connection within the flows. diff --git a/packages/docs/pages/apps/google-calendar/triggers.md b/packages/docs/pages/apps/google-calendar/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..4dc21a8945a04cbad3b9a99071c7387e8ee5dca6 --- /dev/null +++ b/packages/docs/pages/apps/google-calendar/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/google-calendar.svg +items: + - name: New calendar + desc: Triggers when a new calendar is created. + - name: New event + desc: Triggers when a new event is created. +--- + + + + diff --git a/packages/docs/pages/apps/google-drive/connection.md b/packages/docs/pages/apps/google-drive/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..ee783ba5a2a13a96e918696f4fb49653a10847bc --- /dev/null +++ b/packages/docs/pages/apps/google-drive/connection.md @@ -0,0 +1,28 @@ +# Google Drive + +:::info +This page explains the steps you need to follow to set up the Google Drive +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **People API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **Google Drive API** +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Google Drive connection within the flows. diff --git a/packages/docs/pages/apps/google-drive/triggers.md b/packages/docs/pages/apps/google-drive/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..b7a6704ac2018e5c1304777c43e53bb389ddee64 --- /dev/null +++ b/packages/docs/pages/apps/google-drive/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/google-drive.svg +items: + - name: New files + desc: Triggers when any new file is added (inside of any folder). + - name: New files in folder + desc: Triggers when a new file is added directly to a specified folder (but not its subfolder). + - name: New folders + desc: Triggers when a new folder is added directly to a specified folder (but not its subfolder). + - name: Updated files + desc: Triggers when a file is updated in a specified folder (but not its subfolder). +--- + + + + diff --git a/packages/docs/pages/apps/google-forms/connection.md b/packages/docs/pages/apps/google-forms/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..b782b44647294794a1316529e9cc0d11aee83997 --- /dev/null +++ b/packages/docs/pages/apps/google-forms/connection.md @@ -0,0 +1,28 @@ +# Google Forms + +:::info +This page explains the steps you need to follow to set up the Google Forms +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **People API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **Google Forms API** +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Google Forms connection within the flows. diff --git a/packages/docs/pages/apps/google-forms/triggers.md b/packages/docs/pages/apps/google-forms/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..15e1e581869a53199dd9341e1ac3efdba0aff085 --- /dev/null +++ b/packages/docs/pages/apps/google-forms/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/google-forms.svg +items: + - name: New form responses + desc: Triggers when a new form response is submitted. +--- + + + + diff --git a/packages/docs/pages/apps/google-sheets/actions.md b/packages/docs/pages/apps/google-sheets/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..63ae091061e562bcd166cf0b0739affe0900317d --- /dev/null +++ b/packages/docs/pages/apps/google-sheets/actions.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/google-sheets.svg +items: + - name: Create spreadsheet + desc: Create a blank spreadsheet or duplicate an existing spreadsheet. Optionally, provide headers. + - name: Create spreadsheet row + desc: Creates a new row in a specific spreadsheet. + - name: Create worksheet + desc: Create a blank worksheet with a title. Optionally, provide headers. +--- + + + + diff --git a/packages/docs/pages/apps/google-sheets/connection.md b/packages/docs/pages/apps/google-sheets/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..79afb68c20b8e702638f338afae7fa7c089c9d0b --- /dev/null +++ b/packages/docs/pages/apps/google-sheets/connection.md @@ -0,0 +1,28 @@ +# Google Sheets + +:::info +This page explains the steps you need to follow to set up the Google Sheets +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **People API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **Google Drive API** and **Google Sheets API**. +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Google Sheets connection within the flows. diff --git a/packages/docs/pages/apps/google-sheets/triggers.md b/packages/docs/pages/apps/google-sheets/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..88f27109a62417d88531081ab8b655fed3d6a100 --- /dev/null +++ b/packages/docs/pages/apps/google-sheets/triggers.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/google-sheets.svg +items: + - name: New spreadsheets + desc: Triggers when you create a new spreadsheet. + - name: New worksheets + desc: Triggers when you create a new worksheet in a spreadsheet. + - name: New spreadsheet rows + desc: Triggers when a new row is added to the bottom of a spreadsheet. +--- + + + + diff --git a/packages/docs/pages/apps/google-tasks/actions.md b/packages/docs/pages/apps/google-tasks/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..2ed2950bfa894129242e7fd2d51947927dc55838 --- /dev/null +++ b/packages/docs/pages/apps/google-tasks/actions.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/google-tasks.svg +items: + - name: Create task + desc: Creates a new task. + - name: Create task list + desc: Creates a new task list. + - name: Find task + desc: Looking for a specific task. + - name: Update task + desc: Updates an existing task. +--- + + + + diff --git a/packages/docs/pages/apps/google-tasks/connection.md b/packages/docs/pages/apps/google-tasks/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..c09c62df1ef0deb3929f60cc232834cedc678da9 --- /dev/null +++ b/packages/docs/pages/apps/google-tasks/connection.md @@ -0,0 +1,28 @@ +# Google Tasks + +:::info +This page explains the steps you need to follow to set up the Google Tasks +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **Google Tasks API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **People API**. +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Google Tasks connection within the flows. diff --git a/packages/docs/pages/apps/google-tasks/triggers.md b/packages/docs/pages/apps/google-tasks/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..3c6bf3b24094f4aa586da6ec2c384453b2b6781c --- /dev/null +++ b/packages/docs/pages/apps/google-tasks/triggers.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/google-tasks.svg +items: + - name: New completed tasks + desc: Triggers when a task is finished within a specified task list. + - name: New task lists + desc: Triggers when a new task list is created. + - name: New tasks + desc: Triggers when a new task is created. +--- + + + + diff --git a/packages/docs/pages/apps/http-request/actions.md b/packages/docs/pages/apps/http-request/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..ef1ec6571f50f67262817126f6ab3fc32cf4a0b8 --- /dev/null +++ b/packages/docs/pages/apps/http-request/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/http-request.svg +items: + - name: Custom request + desc: Makes a custom HTTP request by providing raw details. +--- + + + + diff --git a/packages/docs/pages/apps/http-request/connection.md b/packages/docs/pages/apps/http-request/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..53c9974a58dd7ed06f93c61e65e77b1cb179211c --- /dev/null +++ b/packages/docs/pages/apps/http-request/connection.md @@ -0,0 +1,3 @@ +# HTTP Request + +HTTP Request is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the HTTP Request app. diff --git a/packages/docs/pages/apps/hubspot/actions.md b/packages/docs/pages/apps/hubspot/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..f2ae774609f9d37c702d9c32665e9d2a3383155d --- /dev/null +++ b/packages/docs/pages/apps/hubspot/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/hubspot.svg +items: + - name: Create a contact + desc: Create a contact on user's account. +--- + + + + diff --git a/packages/docs/pages/apps/hubspot/connection.md b/packages/docs/pages/apps/hubspot/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..37864d08e90651e3d9913df7700391612266444b --- /dev/null +++ b/packages/docs/pages/apps/hubspot/connection.md @@ -0,0 +1,22 @@ +# HubSpot + +:::info +This page explains the steps you need to follow to set up the Hubspot connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [HubSpot Developer page](https://developers.hubspot.com/). +2. Login into your developer account. +3. Click on the **Manage apps** button. +4. Click on the **Create app** button. +5. Fill the **Public app name** field with the name of your API app. +6. Go to the **Auth** tab. +7. Fill the **Redirect URL(s)** field with the OAuth Redirect URL from the Automatisch connection creation page. +8. Go to the **Scopes** tab. +9. Select the scopes you want to use with Automatisch. +10. Click on the **Create App** button. +11. Go back to the **Auth** tab. +12. Copy the **Client ID** and **Client Secret** values. +13. Paste the **Client ID** value into Automatisch as **Client ID**, respectively. +14. Paste the **Client Secret** value into Automatisch as **Client Secret**, respectively. +15. Click the **Submit** button on Automatisch. +16. Now, you can start using the HubSpot connection with Automatisch. diff --git a/packages/docs/pages/apps/invoice-ninja/actions.md b/packages/docs/pages/apps/invoice-ninja/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..d96f53104bd1beca72b76939749038445827d418 --- /dev/null +++ b/packages/docs/pages/apps/invoice-ninja/actions.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/invoice-ninja.svg +items: + - name: Create client + desc: Creates a new client. + - name: Create invoice + desc: Creates a new invoice. + - name: Create payment + desc: Creates a new payment. + - name: Create product + desc: Creates a new product. +--- + + + + diff --git a/packages/docs/pages/apps/invoice-ninja/connection.md b/packages/docs/pages/apps/invoice-ninja/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..e72bbe224ef4b2cc08cea0f6022bbe7c7943b294 --- /dev/null +++ b/packages/docs/pages/apps/invoice-ninja/connection.md @@ -0,0 +1,16 @@ +# Invoice Ninja + +:::info +This page explains the steps you need to follow to set up the Invoice Ninja connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Invoice Ninja](https://invoiceninja.com/). +2. Login into your account. +3. Click on the your company name. +4. Click on the **Account Management** option. +5. Click on the **Integrations** tab. +6. Click on the **API Tokens**. (You need to have a paid account to be able to see that.) +7. Click on the **New Token** button and create a new api token. +8. Copy **Token** field and paste it to the **API Token** field in Automatisch connection creation page. +9. Click the **Submit** button on Automatisch. +10. Now, you can start using the Invoice Ninja connection with Automatisch. diff --git a/packages/docs/pages/apps/invoice-ninja/triggers.md b/packages/docs/pages/apps/invoice-ninja/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..f9d6e9dd35d8d2377f0ab4b9680ce0aa7f479d93 --- /dev/null +++ b/packages/docs/pages/apps/invoice-ninja/triggers.md @@ -0,0 +1,22 @@ +--- +favicon: /favicons/invoice-ninja.svg +items: + - name: New clients + desc: Triggers when a new client is added. + - name: New credits + desc: Triggers when a new credit is added. + - name: New invoices + desc: Triggers when a new invoice is added. + - name: New payments + desc: Triggers when a new payment is added. + - name: New projects + desc: Triggers when a new project is added. + - name: New quotes + desc: Triggers when a new quote is added. +--- + + + + diff --git a/packages/docs/pages/apps/mattermost/actions.md b/packages/docs/pages/apps/mattermost/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..b4ed063e72af772352bf86a17559554359c31d77 --- /dev/null +++ b/packages/docs/pages/apps/mattermost/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/mattermost.svg +items: + - name: Send a message to channel + desc: Sends a message to a channel you specify. +--- + + + + diff --git a/packages/docs/pages/apps/mattermost/connection.md b/packages/docs/pages/apps/mattermost/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..9fa6187cbb16b67ac3d4d07187c6e030b1d2e641 --- /dev/null +++ b/packages/docs/pages/apps/mattermost/connection.md @@ -0,0 +1,19 @@ +# Mattermost + +:::info +This page explains the steps you need to follow to set up the Mattermost +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the `/integrations/oauth2-apps/add` page of your Mattermost server to register a **new OAuth application**. + - You can find details about registering new Mattermost oAuth application at https://docs.mattermost.com/integrations/cloud-oauth-2-0-applications.html#register-your-application-in-mattermost. +2. Fill in the **Display Name** field. +3. Fill in the **Description** field. +4. Fill in the **Homepage** field. +5. Copy **OAuth Redirect URL** from Automatisch to the **Callback URLs** field on Mattermost page. +6. Click on the **Save** button at the end of the form on Mattermost page. +7. Copy the **Client ID** value from the following page to the `Client ID` field on Automatisch. +8. Copy the **Client Secret** value from the same page to the `Client Secret` field on Automatisch. +9. Click **Done** button on MAttermost page. +10. Click **Submit** button on Automatisch. +11. Congrats! Start using your new Mattermost connection within the flows. diff --git a/packages/docs/pages/apps/miro/actions.md b/packages/docs/pages/apps/miro/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..6cb8da69f158df8eb3eb4fd5ad038e8480c5cdfc --- /dev/null +++ b/packages/docs/pages/apps/miro/actions.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/miro.svg +items: + - name: Create board + desc: Creates a new board. + - name: Copy board + desc: Creates a copy of an existing board. + - name: Create card widget + desc: Creates a new card widget on an existing board. +--- + + + + diff --git a/packages/docs/pages/apps/miro/connection.md b/packages/docs/pages/apps/miro/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..6508637c852f174b757fb9d30e488f1a0a4fc5a5 --- /dev/null +++ b/packages/docs/pages/apps/miro/connection.md @@ -0,0 +1,19 @@ +# Miro + +:::info +This page explains the steps you need to follow to set up the Miro +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to [link](https://miro.com/signup/) to create a user account in Miro. +2. After signin in, go to [link](https://miro.com/app/dashboard/?createDevTeam=1) to create a developer team. +3. In the **Create new team** modal, select the checkbox and then click **Create team** button. +4. After that, click **Create new app** in Your app section. +5. Fill the field of **App Name**. +6. Select the **Expire user authorization token** checkbox and click the **Create app**. +7. Copy **OAuth Redirect URL** from Automatisch to the **Redirect URI for OAuth2.0** field. +8. Give permissions for **boards**, **identity**, and **team** scopes in Permissions field. +9. Copy the **Client ID** value to the `Client ID` field on Automatisch. +10. Copy the **Client secret** value to the `Client Secret` field on Automatisch. +11. Click **Submit** button on Automatisch. +12. Congrats! Start using your new Miro connection within the flows. diff --git a/packages/docs/pages/apps/notion/actions.md b/packages/docs/pages/apps/notion/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..d168016d74a7a41bc67aa15c72556606a0be8ac6 --- /dev/null +++ b/packages/docs/pages/apps/notion/actions.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/notion.svg +items: + - name: Create database item + desc: Creates an item in a database. + - name: Create page + desc: Creates a page inside a parent page. + - name: Find database item + desc: Searches for an item in a database by property. +--- + + + + diff --git a/packages/docs/pages/apps/notion/connection.md b/packages/docs/pages/apps/notion/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..363b0c0a392923a39fd4a6407d51f43fcd0efcac --- /dev/null +++ b/packages/docs/pages/apps/notion/connection.md @@ -0,0 +1,22 @@ +# Notion + +:::info +This page explains the steps you need to follow to set up the Notion +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.notion.so/my-integrations) to **create an + integration** on Notion API. +1. Fill out the Name field. +1. Click on the **Submit** button. +1. Go to the **Capabilities** page via the sidebar. +1. Select the **Read user information without email addresses** option under the **User Capabilities** section and then save the changes. +1. Go to the **Distribution** page via the sidebar. +1. Make the integration public by enabling the checkbox. +1. Fill out the necessary fields under the **Organization Information** section. +1. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Redirect URIs** field. +1. Click on the **Submit** button. +1. Accept making the integration public by clicking on the **Continue** button in the dialog. +1. Copy **OAuth client ID** and **OAuth client secret** values and paste them into Automatisch as **Client ID** and **Client Secret**, respectively. +1. Click **Submit** button on Automatisch. +1. Now, you can start using the Notion connection with Automatisch. diff --git a/packages/docs/pages/apps/notion/triggers.md b/packages/docs/pages/apps/notion/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..5d687974769edac3c7490bc65342df017f8c5c81 --- /dev/null +++ b/packages/docs/pages/apps/notion/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/notion.svg +items: + - name: New database items + desc: Triggers when a new database item is created. + - name: Updated database items + desc: Triggers when there is an update to an item in a chosen database. +--- + + + + diff --git a/packages/docs/pages/apps/ntfy/actions.md b/packages/docs/pages/apps/ntfy/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..efc1bfbc2370989d97848b511cde9f2bed004482 --- /dev/null +++ b/packages/docs/pages/apps/ntfy/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/ntfy.svg +items: + - name: Send a message + desc: Sends a message to a topic you specify. +--- + + + + diff --git a/packages/docs/pages/apps/ntfy/connection.md b/packages/docs/pages/apps/ntfy/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..d2d81ac5013efac623a2cdfb24c63efbe389b636 --- /dev/null +++ b/packages/docs/pages/apps/ntfy/connection.md @@ -0,0 +1,10 @@ +# Ntfy + +:::info +This page explains the steps you need to follow to set up the Ntfy +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +If you use ntfy.sh, the official public server for this service, you do not need to set up a connection with a custom configuration. It's enough to create one with the default server URL. + +However, if you have a ntfy installation, that's different than ntfy.sh, you need to specify your server URL on Automatisch while creating a connection. Additionally, you may need to provide your username and password if your installation requires authentication. diff --git a/packages/docs/pages/apps/odoo/actions.md b/packages/docs/pages/apps/odoo/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..e22fdb843f14dbb608e1fc64545972fbd5231066 --- /dev/null +++ b/packages/docs/pages/apps/odoo/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/odoo.svg +items: + - name: Create a lead or opportunity + desc: Creates a new CRM record as a lead or opportunity. +--- + + + + \ No newline at end of file diff --git a/packages/docs/pages/apps/odoo/connection.md b/packages/docs/pages/apps/odoo/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..49b0d270078098ff3bdb782a568f12f3fa941fad --- /dev/null +++ b/packages/docs/pages/apps/odoo/connection.md @@ -0,0 +1,16 @@ +# Odoo + +:::info +This page explains the steps you need to follow to set up the Odoo +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +To create a connection, you need to supply the following information: + +1. Fill the **Host Name** field with the Odoo host. +1. Fill the **Port** field with the Odoo port. +1. Fill the **Database Name** field with the Odoo database. +1. Fill the **Email Address** field with the email address of the account that will be intereacting with the database. +1. Fill the **API Key** field with the API key for your Odoo account. + +Odoo's [API documentation](https://www.odoo.com/documentation/latest/developer/reference/external_api.html#api-keys) explains how to create API keys. diff --git a/packages/docs/pages/apps/openai/actions.md b/packages/docs/pages/apps/openai/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..be3d2ca50c5aaefbe3bb3bfbaa0951b8685c55ad --- /dev/null +++ b/packages/docs/pages/apps/openai/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/openai.svg +items: + - name: Check moderation + desc: Checks for hate, hate/threatening, self-harm, sexual, sexual/minors, violence, or violence/graphic content in text. + - name: Send prompt + desc: Creates a completion for the provided prompt and parameters. +--- + + + + diff --git a/packages/docs/pages/apps/openai/connection.md b/packages/docs/pages/apps/openai/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..f6967eda7d6857fb37c8e6e744da945b6403cca9 --- /dev/null +++ b/packages/docs/pages/apps/openai/connection.md @@ -0,0 +1,8 @@ +# OpenAI + +1. Go to [API Keys page](https://beta.openai.com/account/api-keys) on OpenAI. +2. Create a new secret key. +3. Paste the key into the `API Key` field in Automatisch. +4. Write any screen name to be displayed in Automatisch. +5. Click `Save`. +6. Start using OpenAI integration with Automatisch! diff --git a/packages/docs/pages/apps/pipedrive/actions.md b/packages/docs/pages/apps/pipedrive/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..c27b131f2f12385cbeace3ec17ef7d716d146255 --- /dev/null +++ b/packages/docs/pages/apps/pipedrive/actions.md @@ -0,0 +1,22 @@ +--- +favicon: /favicons/pipedrive.svg +items: + - name: Create activity + desc: Creates a new activity. + - name: Create deal + desc: Creates a new deal. + - name: Create lead + desc: Creates a new lead. + - name: Create note + desc: Creates a new note. + - name: Create organization + desc: Creates a new organization. + - name: Create person + desc: Creates a new person. +--- + + + + diff --git a/packages/docs/pages/apps/pipedrive/connection.md b/packages/docs/pages/apps/pipedrive/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..aa94cfbc03baa39a6d75b09c1c60c033e17a987d --- /dev/null +++ b/packages/docs/pages/apps/pipedrive/connection.md @@ -0,0 +1,17 @@ +# Pipedrive + +:::info +This page explains the steps you need to follow to set up the Pipedrive +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to [Pipedrive developers page](https://developers.pipedrive.com/). +2. Sign up for a **Sandbox account** in order to create an app. +3. Click create an app button and then choose **Create private app** option. +4. Write any app name to be displayed in Automatisch. +5. Copy **OAuth Redirect URL** from Automatisch to **Callback URL** field, and click on the **Save** button. +6. Check all options in **OAuth & Access scopes** with full access. +7. Click on the **Save** button. +8. Copy the **Client ID** value to the `Client ID` field on Automatisch. +9. Copy the **Client Secret** value to the `Client Secret` field on Automatisch. +10. Start using Pipedrive integration with Automatisch! diff --git a/packages/docs/pages/apps/pipedrive/triggers.md b/packages/docs/pages/apps/pipedrive/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..55ac537089a4a986520f7eec58c8320890c5623c --- /dev/null +++ b/packages/docs/pages/apps/pipedrive/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/pipedrive.svg +items: + - name: New activities + desc: Triggers when a new activity is created. + - name: New deals + desc: Triggers when a new deal is created. + - name: New leads + desc: Triggers when a new lead is created. + - name: New notes + desc: Triggers when a new note is created. +--- + + + + diff --git a/packages/docs/pages/apps/placetel/connection.md b/packages/docs/pages/apps/placetel/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..a0f0728ee75afaf3cb77e43ea89cd02e7d232983 --- /dev/null +++ b/packages/docs/pages/apps/placetel/connection.md @@ -0,0 +1,7 @@ +# Placetel + +1. Go to [AppStore page](https://web.placetel.de/integrations) on Placetel. +2. Search for `Web API` and click to `Jetzt buchen`. +3. Click to `Neuen API-Token erstellen` button and copy the API Token. +4. Paste the copied API Token into the `API Token` field in Automatisch. +5. Now, you can start using Placetel integration with Automatisch! diff --git a/packages/docs/pages/apps/placetel/triggers.md b/packages/docs/pages/apps/placetel/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..99c86c1ce839cecc577ef943f91af5680eda55de --- /dev/null +++ b/packages/docs/pages/apps/placetel/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/placetel.svg +items: + - name: Hungup call + desc: Triggers when a call is hungup. +--- + + + + diff --git a/packages/docs/pages/apps/postgresql/actions.md b/packages/docs/pages/apps/postgresql/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..e877edb6539b49ce7fab859824d4bfd3a754e659 --- /dev/null +++ b/packages/docs/pages/apps/postgresql/actions.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/postgres.svg +items: + - name: Insert + desc: Create a new row in a table in specified schema. + - name: Update + desc: Update rows found based on the given where clause entries. + - name: Delete + desc: Delete rows found based on the given where clause entries. + - name: SQL query + desc: Executes the given SQL statement. +--- + + + + diff --git a/packages/docs/pages/apps/postgresql/connection.md b/packages/docs/pages/apps/postgresql/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..c733db85688cd4730bfb3989c0ceb0c113859a87 --- /dev/null +++ b/packages/docs/pages/apps/postgresql/connection.md @@ -0,0 +1,19 @@ +# PostgreSQL + +:::info +This page explains the steps you need to follow to set up the Postgres +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +PostgreSQL is an open-source relational database management system (RDBMS) known for its robustness, reliability, and feature-richness. +It is a powerful and reliable database management system suitable for a wide range of applications, from small projects to enterprise-level systems. + +1. Fill postgreSQL version field with the version that you are using. +2. Fill host address field with the postgres host address. +3. Fill port field with the postgres port. +4. Select wheather to use ssl or not. +5. Fill database name field with the postgres database name. +6. Fill database username field with the postgres username. +7. Fill password field with the postgres password. +8. Click **Submit** button on Automatisch. +9. Now, you can start using the PostgreSQL connection with Automatisch. \ No newline at end of file diff --git a/packages/docs/pages/apps/pushover/actions.md b/packages/docs/pages/apps/pushover/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..8e799a170c5ab95c363b52adeebd84b26ce22aa1 --- /dev/null +++ b/packages/docs/pages/apps/pushover/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/pushover.svg +items: + - name: Send a Pushover Notification + desc: Generates a Pushover notification on the devices you have subscribed to. +--- + + + + diff --git a/packages/docs/pages/apps/pushover/connection.md b/packages/docs/pages/apps/pushover/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..0281097abde83767b4fd250bd39a0d2f7bf84d25 --- /dev/null +++ b/packages/docs/pages/apps/pushover/connection.md @@ -0,0 +1,9 @@ +# Pushover + +1. Login to [your account page](https://pushover.net/login) on Pushover. +2. Copy the **Your User Key** value to the **User Key** field in Automatisch connection page. +3. Create a new application from [here](https://pushover.net/apps/build) on Pushover. +4. Copy the **API Token/Key** value to the **API Token** field in Automatisch connection page. +5. Write any screen name to be displayed in Automatisch. +6. Click `Submit`. +7. Start using Pushover integration with Automatisch! diff --git a/packages/docs/pages/apps/reddit/actions.md b/packages/docs/pages/apps/reddit/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..757bda6f2b54f1133c2d3abf33c54982b4aa801d --- /dev/null +++ b/packages/docs/pages/apps/reddit/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/reddit.svg +items: + - name: Create link post + desc: Create a new link post within a subreddit. +--- + + + + diff --git a/packages/docs/pages/apps/reddit/connection.md b/packages/docs/pages/apps/reddit/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..3cebb99942735f5eeee324b4d11ead6ee16a19ed --- /dev/null +++ b/packages/docs/pages/apps/reddit/connection.md @@ -0,0 +1,15 @@ +# Reddit + +:::info +This page explains the steps you need to follow to set up the Reddit +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to [Reddit apps page](https://www.reddit.com/prefs/apps). +2. Click on the **"are you a developer? create an app..."** button in order to create an app. +3. Fill the **Name** field and choose **web app**. +4. Copy **OAuth Redirect URL** from Automatisch to **redirect uri** field. +5. Click on the **create app** button. +6. Copy the client id below **web app** text to the `Client ID` field on Automatisch. +7. Copy the **secret** value to the `Client Secret` field on Automatisch. +8. Start using Reddit integration with Automatisch! diff --git a/packages/docs/pages/apps/reddit/triggers.md b/packages/docs/pages/apps/reddit/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..3fed70fa07d7c41c9c04831123175d264da34152 --- /dev/null +++ b/packages/docs/pages/apps/reddit/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/reddit.svg +items: + - name: New posts matching search + desc: Triggers when a search string matches a new post. +--- + + + + diff --git a/packages/docs/pages/apps/removebg/actions.md b/packages/docs/pages/apps/removebg/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..954ddc3e41f579d162888b1b9598618dbb189d36 --- /dev/null +++ b/packages/docs/pages/apps/removebg/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/removebg.svg +items: + - name: Remove Image Background + desc: Remove backgrounds 100% automatically in 5 seconds with one click. +--- + + + + \ No newline at end of file diff --git a/packages/docs/pages/apps/removebg/connection.md b/packages/docs/pages/apps/removebg/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..d3ff1ec0fa87ba66a86d87b9599231fe0ea0eed8 --- /dev/null +++ b/packages/docs/pages/apps/removebg/connection.md @@ -0,0 +1,11 @@ +# Remove.bg + +:::info +This page explains the steps you need to follow to set up the remove.bg +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Login to your remove.bg account: [https://www.remove.bg/](https://www.remove.bg/). +2. Create a new api key: [https://www.remove.bg/dashboard#api-key](https://www.remove.bg/dashboard#api-key). +3. Copy the `API Key` from the page to the `API Key` field on Automatisch. +4. Now, you can start using the remove.bg connection with Automatisch. diff --git a/packages/docs/pages/apps/rss/connection.md b/packages/docs/pages/apps/rss/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..ce12caac025eae837a04bcd10e42b68643b716bb --- /dev/null +++ b/packages/docs/pages/apps/rss/connection.md @@ -0,0 +1,3 @@ +# RSS + +RSS is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the RSS app. diff --git a/packages/docs/pages/apps/rss/triggers.md b/packages/docs/pages/apps/rss/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..f18e3549f4c4f74debbcf419fb6c55e4c6c276c5 --- /dev/null +++ b/packages/docs/pages/apps/rss/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/rss.svg +items: + - name: New items in feed + desc: Triggers on new RSS feed item. +--- + + + + diff --git a/packages/docs/pages/apps/salesforce/actions.md b/packages/docs/pages/apps/salesforce/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..106ca5fbf68eff2f77eead4fb30731352be00714 --- /dev/null +++ b/packages/docs/pages/apps/salesforce/actions.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/salesforce.svg +items: + - name: Create attachment + desc: Creates an attachment of a specified object by given parent ID. + - name: Find record + desc: Finds a record of a specified object by a field and value. + - name: Execute query + desc: Executes a SOQL query in Salesforce. +--- + + + + diff --git a/packages/docs/pages/apps/salesforce/connection.md b/packages/docs/pages/apps/salesforce/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..c9e5823399103afbf15eba3f35ef2c63b20cb164 --- /dev/null +++ b/packages/docs/pages/apps/salesforce/connection.md @@ -0,0 +1,25 @@ +# Salesforce + +:::info +This page explains the steps you need to follow to set up the Salesforce +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to your Salesforce dasboard. +1. Click on the gear icon in the top right corner and click **Setup** from the dropdown. +1. In the **Platform Tools** section of the sidebar, click **Apps** > **App Manager**. +1. Click the **New Connected App** button. +1. Enter necessary information in the form. +1. Check **Enable OAuth Settings** checkbox. +1. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URL** field. +1. Add any scopes you plan to use in the **Selected OAuth Scopes** section. We suggest `full` and `refresh_token, offline_access` scopes. +1. Uncheck "Require Proof Key for Code Exchange (PKCE) Extension for Supported Authorization Flows" checkbox. +1. Check "Enable Authorization Code and Credentials Flow" checkbox +1. Click on the **Save** button at the bottom of the page. +1. Acknowledge the information and click on the **Continue** button. +1. In the **API (Enable OAuth Settings)** section, click the **Manager Consumer Details** button. +1. You may be asked to verify your identity. To see the consumer key and secret, verify and proceed. +1. Copy the **Consumer Key** value from the page to the `Consumer Key` field on Automatisch. +1. Copy the **Consumer Secret** value from the page to the `Consumer Secret` field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Now, you can start using the Salesforce connection with Automatisch. diff --git a/packages/docs/pages/apps/salesforce/triggers.md b/packages/docs/pages/apps/salesforce/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..0ede2442c4b1cb2b245062e9791c79aeda119437 --- /dev/null +++ b/packages/docs/pages/apps/salesforce/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/salesforce.svg +items: + - name: Updated field in records + desc: Triggers when a field is updated in a record. +--- + + + + diff --git a/packages/docs/pages/apps/scheduler/connection.md b/packages/docs/pages/apps/scheduler/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..46e8cfa4a7e55b9272b8e18a5cab148e92a56a70 --- /dev/null +++ b/packages/docs/pages/apps/scheduler/connection.md @@ -0,0 +1,3 @@ +# Scheduler + +Scheduler is a built-in app shipped with Automatisch, and it doesn't need to talk with any other external service to run. So there are no additional steps to use the Scheduler app. diff --git a/packages/docs/pages/apps/scheduler/triggers.md b/packages/docs/pages/apps/scheduler/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..d4a0f90cf3c9488a587940135be84eb970da0f99 --- /dev/null +++ b/packages/docs/pages/apps/scheduler/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/scheduler.svg +items: + - name: Every hour + desc: Triggers every hour. + - name: Every day + desc: Triggers every day. + - name: Every week + desc: Triggers every week. + - name: Every month + desc: Triggers every month. +--- + + + + diff --git a/packages/docs/pages/apps/signalwire/actions.md b/packages/docs/pages/apps/signalwire/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..f0d726e8661b4ed95f84504b348133f08578e543 --- /dev/null +++ b/packages/docs/pages/apps/signalwire/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/signalwire.svg +items: + - name: Send an SMS + desc: Sends an SMS. +--- + + + + diff --git a/packages/docs/pages/apps/signalwire/connection.md b/packages/docs/pages/apps/signalwire/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..20a61005490f1572c9c23a022d253d7c0383bbb2 --- /dev/null +++ b/packages/docs/pages/apps/signalwire/connection.md @@ -0,0 +1,16 @@ +# SignalWire + +:::info +This page explains the steps you need to follow to set up a SignalWire connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the SignalWire API page in your respective project (https://{space}.signalwire.com/credentials) +2. Copy **Project ID** and paste it to the **Project ID** field on the + Automatisch connection creation page. +3. Create/Copy **API Token** and paste it to the **API Token** field on the + Automatisch connection creation page. +4. Select your **Region** (US for most users). +5. Provide your **Space Name** from the URL and paste it to the **Space NAME** field on the + Automatisch connection creation page. +6. Click **Submit** button on Automatisch. +7. Now you can start using the new SignalWire connection! diff --git a/packages/docs/pages/apps/signalwire/triggers.md b/packages/docs/pages/apps/signalwire/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..42083cc8d480eb746fce495a980a510cf8d71fce --- /dev/null +++ b/packages/docs/pages/apps/signalwire/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/signalwire.svg +items: + - name: Receive SMS + desc: Triggers when a new SMS is received. +--- + + + + diff --git a/packages/docs/pages/apps/slack/actions.md b/packages/docs/pages/apps/slack/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..90de241a754fdec8964faf6f48c04a1e3c95c3c3 --- /dev/null +++ b/packages/docs/pages/apps/slack/actions.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/slack.svg +items: + - name: Find a message + desc: Finds a message using the Slack search feature. + - name: Find user by email + desc: Finds a user by email. + - name: Send a message to channel + desc: Sends a message to a channel you specify. + - name: Send a direct message + desc: Sends a direct message to a user or yourself from the Slackbot. +--- + + + + diff --git a/packages/docs/pages/apps/slack/connection.md b/packages/docs/pages/apps/slack/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..d0049944e6722972bac498dbf260eae1dbf1cc0f --- /dev/null +++ b/packages/docs/pages/apps/slack/connection.md @@ -0,0 +1,25 @@ +# Slack + +:::info +This page explains the steps you need to follow to set up the Slack connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://api.slack.com/apps?new_app=1) to **create an app** + on Slack API. +1. Select **From scratch**. +1. Enter **App name**. +1. Pick the workspace you would like to use with the Slack connection. +1. Click on **Create App** button. +1. Copy **Client ID** and **Client Secret** values and save them to use later. +1. Go to **OAuth & Permissions** page. +1. Copy **OAuth Redirect URL** from Automatisch and add it in Redirect URLs. Don't forget to save it after adding it by clicking **Save URLs** button! +1. Go to **Bot Token Scopes** and add `chat:write.customize` along with `chat:write` scope to enable the bot functionality. + +:::warning HTTPS required! + +Slack does **not** allow non-secure URLs in redirect URLs. Therefore, you will need to serve Automatisch via HTTPS protocol. +::: + +10. Paste **Client ID** and **Client Secret** values you have saved earlier and paste them into Automatisch as **Consumer Key** and **Consumer Secret**, respectively. +1. Click **Submit** button on Automatisch. +1. Now, you can start using the Slack connection with Automatisch. diff --git a/packages/docs/pages/apps/smtp/actions.md b/packages/docs/pages/apps/smtp/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..b6feb75b5337eb4c4510e2a6072888c8d5df606c --- /dev/null +++ b/packages/docs/pages/apps/smtp/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/smtp.svg +items: + - name: Send an email + desc: Sends an email. +--- + + + + diff --git a/packages/docs/pages/apps/smtp/connection.md b/packages/docs/pages/apps/smtp/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..5b4f692db5fbc275daa7723c89fb8369bff95122 --- /dev/null +++ b/packages/docs/pages/apps/smtp/connection.md @@ -0,0 +1,16 @@ +# SMTP + +:::info +This page explains the steps you need to follow to set up the SMTP connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +SMTP is a protocol that allows you to send emails. It's a very common protocol and it's used by many email providers. You need to provide the following information to send emails from Automatisch by using SMTP connection. + +1. Fill host address field with the SMTP host address. +2. Fill username field with the SMTP username. +3. Fill password field with the SMTP password. +4. Select wheather to use TLS or not. +5. Fill port field with the SMTP port. +6. Fill from field with the email address you want to use as the sender. +7. Click **Submit** button on Automatisch. +8. Now, you can start using the SMTP connection with Automatisch. diff --git a/packages/docs/pages/apps/spotify/actions.md b/packages/docs/pages/apps/spotify/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..607264d525ff4d480051ee61a7c6187b1b98fd2f --- /dev/null +++ b/packages/docs/pages/apps/spotify/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/spotify.svg +items: + - name: Create playlist + desc: Create a playlist on user's account. +--- + + + + diff --git a/packages/docs/pages/apps/spotify/connection.md b/packages/docs/pages/apps/spotify/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..dad852f99002590b996a6b1b76da7bb9ffe723a0 --- /dev/null +++ b/packages/docs/pages/apps/spotify/connection.md @@ -0,0 +1,20 @@ +# Spotify + +:::info +This page explains the steps you need to follow to set up the Spotify connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://developer.spotify.com/dashboard/applications) to **create an app** + on Spotify API. +1. Click login button if you're not logged in. +1. Click **Create an app** button. +1. Enter **App name** and **App description**. +1. Click on **Create App** button. +1. **Client ID** will be visible on the screen. +1. Click **Show Client Secret** button to see client secret. +1. Copy **Client ID** and **Client Secret** values and save them to use later. +1. Click **Edit settings** button. +1. Copy **OAuth Redirect URL** from Automatisch and add it in Redirect URLs. Don't forget to save it after adding it by clicking **Add** button! +1. Paste **Client ID** and **Client Secret** values you have saved earlier and paste them into Automatisch as **Client Id** and **Client Secret**, respectively. +1. Click **Submit** button on Automatisch. +1. Now, you can start using the Spotify connection with Automatisch. diff --git a/packages/docs/pages/apps/strava/actions.md b/packages/docs/pages/apps/strava/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..8e877fb47abb3474b0041910ed08f1291b7e03aa --- /dev/null +++ b/packages/docs/pages/apps/strava/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/strava.svg +items: + - name: Create totals and stats report + desc: Creates a report with recent, year to date, and all time stats of your activities. +--- + + + + diff --git a/packages/docs/pages/apps/strava/connection.md b/packages/docs/pages/apps/strava/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..0060fc06378d52316a2afe99dfff5bb75e4b842d --- /dev/null +++ b/packages/docs/pages/apps/strava/connection.md @@ -0,0 +1,14 @@ +# Strava + +:::info +This page explains the steps you need to follow to set up the Strava connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.strava.com/settings/api) to create an app on Strava API. +1. Click on **Upload** button to upload your APP icon. +1. Click on **Edit** button. +1. Copy **OAuth Redirect URL** from Automatisch and paste it in **Authorization Callback Domain** +1. Click on **Save** button. +1. Copy **Client ID** from Strava and paste it in **Client ID** field on Automatisch. +1. Copy **Client Secret** from Strava and paste it in **Client Secret** field on Automatisch. +1. Now, you can start using the Strava connection with Automatisch. diff --git a/packages/docs/pages/apps/stripe/connection.md b/packages/docs/pages/apps/stripe/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..0cd174eb71449e8dd5f2f12f9a88b4cf6872355e --- /dev/null +++ b/packages/docs/pages/apps/stripe/connection.md @@ -0,0 +1,14 @@ +# Stripe + +:::info +This page explains the steps you need to follow to set up the Stripe connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +:::info +You are free to use the **Testing secret key** instead of the productive secret key as well. +::: + +1. Go to the [Stripe Dashboard > Developer > API keys](https://dashboard.stripe.com/apikeys) +2. Click on **Reveal live key** in the table row **Secret key** and copy the now shown secret key +3. Paste the **Secret key** in the named field in Automatisch and assign a display name for the connection. +4. Congrats! You can start using the new Stripe connection! diff --git a/packages/docs/pages/apps/stripe/triggers.md b/packages/docs/pages/apps/stripe/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..1e79a20c57f8f23fb8a0b244076826e667641d41 --- /dev/null +++ b/packages/docs/pages/apps/stripe/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/stripe.svg +items: + - name: New payouts + desc: Triggers when stripe sent a payout to a third-party bank account or vice versa. + org: Stripe documentation + orgLink: https://stripe.com/docs/api/payouts/object + - name: New balance transactions + desc: Triggers when a fund has been moved through your stripe account. + org: Stripe documentation + orgLink: https://stripe.com/docs/api/balance_transactions/object +--- + + + + diff --git a/packages/docs/pages/apps/telegram-bot/actions.md b/packages/docs/pages/apps/telegram-bot/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..261d241da50135c156cb81e8b62bd5568a22c03b --- /dev/null +++ b/packages/docs/pages/apps/telegram-bot/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/telegram-bot.svg +items: + - name: Send a message + desc: Sends a message to a chat you specify. +--- + + + + diff --git a/packages/docs/pages/apps/telegram-bot/connection.md b/packages/docs/pages/apps/telegram-bot/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..4a015cc92d9c20ec70582efba16a28f842509139 --- /dev/null +++ b/packages/docs/pages/apps/telegram-bot/connection.md @@ -0,0 +1,14 @@ +# Telegram + +:::info +This page explains the steps you need to follow to set up the Telegram +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Start a chat with [Botfather](https://telegram.me/BotFather). +1. Enter `/newbot`. +1. Enter a name for your bot. +1. Enter a username for your bot. +1. Copy the **token** value from the answer to the **Bot token** field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new Telegram connection within the flows. diff --git a/packages/docs/pages/apps/todoist/actions.md b/packages/docs/pages/apps/todoist/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..5dcceed03bb838fa572a0350c7f25984bf8c297d --- /dev/null +++ b/packages/docs/pages/apps/todoist/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/todoist.svg +items: + - name: Create task + desc: Creates a task in Todoist. +--- + + + + diff --git a/packages/docs/pages/apps/todoist/connection.md b/packages/docs/pages/apps/todoist/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..1027483f09e74fabf201cb3395b5a1c9847ce697 --- /dev/null +++ b/packages/docs/pages/apps/todoist/connection.md @@ -0,0 +1,14 @@ +# Todoist + +:::info +This page explains the steps you need to follow to set up the Todoist connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the account [App Management page](https://developer.todoist.com/appconsole.html) to register a **new OAuth application** on Todoist. +1. Fill **App name** and **App service URL**. +1. Copy **OAuth Redirect URL** from Automatisch to **OAuth redirect URL** field on Todoist page. +1. Click on the **Save settings** button on the Todoist page. +1. Copy the **Client ID** and **Client secret** values from the Todoist page to the corresponding fields on Automatisch. +1. Enter a memorable name for your connection in the **Screen Name** field. +1. Click the **Submit** button on Automatisch. +1. Congrats! Start using your new Todoist connection within the flows. diff --git a/packages/docs/pages/apps/todoist/triggers.md b/packages/docs/pages/apps/todoist/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..a1c6f46ba2bfd6856dfb6a04bd94a8a06791c4b3 --- /dev/null +++ b/packages/docs/pages/apps/todoist/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/todoist.svg +items: + - name: Get tasks + desc: Finds tasks in Todoist, optionally matching specified parameters. +--- + + + + diff --git a/packages/docs/pages/apps/trello/actions.md b/packages/docs/pages/apps/trello/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..8ece515f37240708bf622830580822a93466d8d0 --- /dev/null +++ b/packages/docs/pages/apps/trello/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/trello.svg +items: + - name: Create card + desc: Creates a new card within a specified board and list. +--- + + + + diff --git a/packages/docs/pages/apps/trello/connection.md b/packages/docs/pages/apps/trello/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..8a60acc9096e815b3b272d2a66f386a046316897 --- /dev/null +++ b/packages/docs/pages/apps/trello/connection.md @@ -0,0 +1,16 @@ +# Trello + +:::info +This page explains the steps you need to follow to set up the Trello +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://trello.com/power-ups/admin) in order to create a Trello Power-Up. +2. Click on the **New** button. +3. Fill the form fields and click the **Create** button. +4. Click on the **Generate a new API key** button. +5. A popup will open. Click the **Generate a new API key** button again. +6. Copy **OAuth Redirect URL** from Automatisch and paste it in **Allowed origins** and click on the **Add** button. +7. Copy the **API key** value to the **API key** field on Automatisch. +8. Click **Submit** button on Automatisch. +9. Congrats! Start using your new Trello connection within the flows. diff --git a/packages/docs/pages/apps/twilio/actions.md b/packages/docs/pages/apps/twilio/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..168afe24508539c898f7cf0ec7081f1151f5e903 --- /dev/null +++ b/packages/docs/pages/apps/twilio/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/twilio.svg +items: + - name: Send an SMS + desc: Sends an SMS. +--- + + + + diff --git a/packages/docs/pages/apps/twilio/connection.md b/packages/docs/pages/apps/twilio/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..50460bf3af8387b151ece1b48680334ce120c7a7 --- /dev/null +++ b/packages/docs/pages/apps/twilio/connection.md @@ -0,0 +1,13 @@ +# Twilio + +:::info +This page explains the steps you need to follow to set up the Twilio connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the Twilio [console page](https://console.twilio.com) +2. Copy **Account SID** and paste it to **Account SID** field on the + Automatisch connection creation page. +3. Copy **Auth Token** and paste it to **Auth Token** field on the + Automatisch connection creation page. +4. Click **Submit** button on Automatisch. +5. Now you can start using the new Twilio connection! diff --git a/packages/docs/pages/apps/twilio/triggers.md b/packages/docs/pages/apps/twilio/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..5199832be214cfb420aa4cad68c52679e8978ef6 --- /dev/null +++ b/packages/docs/pages/apps/twilio/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/twilio.svg +items: + - name: Receive SMS + desc: Triggers when a new SMS is received. +--- + + + + diff --git a/packages/docs/pages/apps/twitter/actions.md b/packages/docs/pages/apps/twitter/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..6e12ef93af21136bfea154114b4c826870c7b585 --- /dev/null +++ b/packages/docs/pages/apps/twitter/actions.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/twitter.svg +items: + - name: Create tweet + desc: Create a tweet. + - name: Search user + desc: Search a user. +--- + + + + diff --git a/packages/docs/pages/apps/twitter/connection.md b/packages/docs/pages/apps/twitter/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..85ab3073a6fbfe68f323537febbf359ae5201165 --- /dev/null +++ b/packages/docs/pages/apps/twitter/connection.md @@ -0,0 +1,25 @@ +# Twitter + +:::info +This page explains the steps you need to follow to set up the Twitter connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Twitter Developer Portal](https://developer.twitter.com/en/portal/projects-and-apps), complete the questionnaire and click the **Let's do this** button. +2. Accept terms & conditions on the following page and click **Submit**. + +:::warning +If you see an error saying `There was a problem completing your request. User must have a verified phone number on file prior to submitting application.` Go to the [phone settings page](https://twitter.com/settings/phone) and set up your phone number to be able to continue on step 2. +::: + +3. You will get a verification email from Twitter. Click on **Confirm your email** button. +4. Fill out the **App name** field and click on the **Get keys** button. +5. Copy **API Key** and **API Key Secret** values and save them to use later. +6. Click **Dashboard** and **Yes, I saved them** buttons, respectively. +7. Go to the **App settings** link in the project section you have created. +8. Go to the **User authentication settings** section and click **Set up**. +9. Enable **OAuth 1.0a** on the following page. +10. In the **OAuth 1.0A Settings** section, select **Read and write** option. +11. Copy **OAuth Redirect URL** from Automatisch and paste it to the **Callback URI / Redirect URL** field. +12. Fill **Website URL** and click **Save**. +13. Paste **API Key** and **API Key Secret** values you have saved from the 5th step and paste them into Automatisch as **API Key** and **API Secret**, respectively. +14. Congrats! You can start using the new Twitter connection! diff --git a/packages/docs/pages/apps/twitter/triggers.md b/packages/docs/pages/apps/twitter/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..0494ea18b2e0ee1aba54c1e14383a7f49d718b25 --- /dev/null +++ b/packages/docs/pages/apps/twitter/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/twitter.svg +items: + - name: My tweets + desc: Triggers when you tweet something new. + - name: New followers of me + desc: Triggers when you have a new follower. + - name: Search tweets + desc: Triggers when there is a new tweet containing a specific keyword, phrase, username or hashtag. + - name: User tweets + desc: Triggers when a specific user tweet something new. +--- + + + + diff --git a/packages/docs/pages/apps/typeform/connection.md b/packages/docs/pages/apps/typeform/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..f3db56b2be308f9b226800e659a92b00a0666b9f --- /dev/null +++ b/packages/docs/pages/apps/typeform/connection.md @@ -0,0 +1,14 @@ +# Typeform + +:::info +This page explains the steps you need to follow to set up the Typeform connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://admin.typeform.com/user) and click on **Developer apps** in the sidebar. +2. Click on the **Register a new app** button. +3. Fill **App name** and **App website**, and **Developer email** fields. +4. Copy **OAuth Redirect URL** from Automatisch to **Redirect URI(s)** field on the Typeform page. +5. Click on the **Register app** button. +6. Copy **Client ID** and **Client Secret** values from Typeform to Automatisch. +7. Click **Submit** button on Automatisch. +8. Congrats! Typeform connection is created. diff --git a/packages/docs/pages/apps/typeform/triggers.md b/packages/docs/pages/apps/typeform/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..2bfab4d54157cf43b169089785ed79038308aa9c --- /dev/null +++ b/packages/docs/pages/apps/typeform/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/typeform.svg +items: + - name: New entry + desc: Triggers when a new form is submitted. +--- + + + + diff --git a/packages/docs/pages/apps/vtiger-crm/actions.md b/packages/docs/pages/apps/vtiger-crm/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..b4985053456e7c736322e3510202f0db1ff405b8 --- /dev/null +++ b/packages/docs/pages/apps/vtiger-crm/actions.md @@ -0,0 +1,20 @@ +--- +favicon: /favicons/vtiger-crm.svg +items: + - name: Create case + desc: Create a new case. + - name: Create contact + desc: Create a new contact. + - name: Create opportunity + desc: Create a new opportunity. + - name: Create todo + desc: Create a new todo. + - name: Create lead + desc: Create a new lead. +--- + + + + diff --git a/packages/docs/pages/apps/vtiger-crm/connection.md b/packages/docs/pages/apps/vtiger-crm/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..101b8760d9efcbd5b95d42eb23b4c9ee8cf51e55 --- /dev/null +++ b/packages/docs/pages/apps/vtiger-crm/connection.md @@ -0,0 +1,13 @@ +# Vtiger CRM + +:::info +This page explains the steps you need to follow to set up the Vtiger CRM connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.vtiger.com/) and create an account. +2. Go to **My Preferences** of your account. +3. Copy **Access Key** value from Vtiger CRM to Automatisch. +4. Fill **Username** field as your Vtiger CRM account email on Automatisch. +5. Fill **Domain** field as if your dashboard url is `https://acmeco.od1.vtiger.com`, paste `acmeco.od1` to the field on Automatisch. +6. Click **Submit** button on Automatisch. +7. Congrats! Vtiger CRM connection is created. diff --git a/packages/docs/pages/apps/vtiger-crm/triggers.md b/packages/docs/pages/apps/vtiger-crm/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..57e88698ce4c74305a97ff4b755a1bd8cf571bda --- /dev/null +++ b/packages/docs/pages/apps/vtiger-crm/triggers.md @@ -0,0 +1,22 @@ +--- +favicon: /favicons/vtiger-crm.svg +items: + - name: New cases + desc: Triggers when a new case is created. + - name: New contacts + desc: Triggers when a new contact is created. + - name: New invoices + desc: Triggers when a new invoice is created. + - name: New leads + desc: Triggers when a new lead is created. + - name: New opportunities + desc: Triggers when a new opportunity is created. + - name: New todos + desc: Triggers when a new todo is created. +--- + + + + diff --git a/packages/docs/pages/apps/webhooks/connection.md b/packages/docs/pages/apps/webhooks/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..a38b7e93f44f90fae1e4fa021d160b50a669766c --- /dev/null +++ b/packages/docs/pages/apps/webhooks/connection.md @@ -0,0 +1,7 @@ +# Webhooks + +Webhooks is a built-in app shipped with Automatisch, and it doesn't need to authenticate with any other external service to run. + +## How to use + +You will be given a webhook URL in the test substep on the editor page, and you can use it to send a GET, POST, PUT, or PATCH request to Automatisch to trigger the flow. diff --git a/packages/docs/pages/apps/webhooks/triggers.md b/packages/docs/pages/apps/webhooks/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..9dda3c35139b4ce9007320feef09562760c6f1a9 --- /dev/null +++ b/packages/docs/pages/apps/webhooks/triggers.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/webhooks.svg +items: + - name: Catch raw webhook + desc: Triggers when the webhook receives a request. +--- + + + + diff --git a/packages/docs/pages/apps/wordpress/connection.md b/packages/docs/pages/apps/wordpress/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..9cce51bbce7fc2d4d25a68d16df4f05e42f041a6 --- /dev/null +++ b/packages/docs/pages/apps/wordpress/connection.md @@ -0,0 +1,9 @@ +# WordPress + +:::info +This page explains the steps you need to follow to set up the WordPress connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Add your WordPress public URL (without any path in the address) in the **WordPress instance URL** field on Automatisch. +1. Click **Submit** button on Automatisch. +1. Congrats! Start using your new WordPress connection within the flows. diff --git a/packages/docs/pages/apps/wordpress/triggers.md b/packages/docs/pages/apps/wordpress/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..cee4ebf4447bf737dcf34e0d50058dd22c8e427f --- /dev/null +++ b/packages/docs/pages/apps/wordpress/triggers.md @@ -0,0 +1,16 @@ +--- +favicon: /favicons/wordpress.svg +items: + - name: New comment + desc: Triggers when a new comment is created. + - name: New page + desc: Triggers when a new page is created. + - name: New post + desc: Triggers when a new post is created. +--- + + + + diff --git a/packages/docs/pages/apps/xero/connection.md b/packages/docs/pages/apps/xero/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..d9a43d3ba5dc585ad29bcc2a848c8ff5084d735e --- /dev/null +++ b/packages/docs/pages/apps/xero/connection.md @@ -0,0 +1,18 @@ +# Xero + +:::info +This page explains the steps you need to follow to set up the Xero +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to [Xero developers page](https://developer.xero.com/app/manage). +2. Click **New app** button in order to create an app. +3. Fill the **Name** field and choose **Web App**. +4. Fill the **Company or application URL** field. +5. Copy **OAuth Redirect URL** from Automatisch to **Redirect URL** field. +6. Check the terms and conditions checkbox. +7. Click on the **Create app** button. +8. Go to **Configuration** page and click the **Generate Client Secret** button. +9. Copy the **Client id** value to the `Client ID` field on Automatisch. +10. Copy the **Client secret 1** value to the `Client Secret` field on Automatisch. +11. Start using Xero integration with Automatisch! diff --git a/packages/docs/pages/apps/xero/triggers.md b/packages/docs/pages/apps/xero/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..23e5eb91a8f3748b989d2679a246caab82487891 --- /dev/null +++ b/packages/docs/pages/apps/xero/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/xero.svg +items: + - name: New bank transactions + desc: Triggers when a new bank transaction occurs. + - name: New payments + desc: Triggers when a new payment is received. +--- + + + + diff --git a/packages/docs/pages/apps/you-need-a-budget/connection.md b/packages/docs/pages/apps/you-need-a-budget/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..be7307d26ef69124f344692d39e8f39af3207d89 --- /dev/null +++ b/packages/docs/pages/apps/you-need-a-budget/connection.md @@ -0,0 +1,19 @@ +# You Need A Budget + +:::info +This page explains the steps you need to follow to set up the You Need A Budget +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [link](https://www.ynab.com/) and login your account. +2. Click on the account name in the top left and go to **Account Settings**. +3. Click on the **Developer Settings**. +4. Click on the **New Application** under the **OAuth Applications** section. +5. Fill the new application form. +6. Copy **OAuth Redirect URL** from Automatisch and paste it in **Redirect URI(s)** section. +7. Enable the option **Enable default budget selection when users authorize this application**. +8. Click on the **Save Application** button. +9. Copy the **Client ID** value to the **Client ID** field on Automatisch. +10. Copy the **Client Secret** value to the **Client Secret** field on Automatisch. +11. Fill the **Screen Name** field on Automatisch. +12. Congrats! Start using your new You Need A Budget connection within the flows. diff --git a/packages/docs/pages/apps/you-need-a-budget/triggers.md b/packages/docs/pages/apps/you-need-a-budget/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..4394dd0b133487ce9f5b761c83261c794382eb9a --- /dev/null +++ b/packages/docs/pages/apps/you-need-a-budget/triggers.md @@ -0,0 +1,18 @@ +--- +favicon: /favicons/you-need-a-budget.svg +items: + - name: Category overspent + desc: Triggers when a category exceeds its budget, resulting in a negative balance. + - name: Goal completed + desc: Triggers when a goal is completed. + - name: Low account balance + desc: Triggers when the balance of a Checking or Savings account falls below a specified amount within a given month. + - name: New transactions + desc: Triggers when a new transaction is created. +--- + + + + diff --git a/packages/docs/pages/apps/youtube/connection.md b/packages/docs/pages/apps/youtube/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..3f3f7cdea28250515abb8ed081b1e9506a6d17a8 --- /dev/null +++ b/packages/docs/pages/apps/youtube/connection.md @@ -0,0 +1,28 @@ +# Youtube + +:::info +This page explains the steps you need to follow to set up the Youtube +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Go to the [Google Cloud Console](https://console.cloud.google.com) to create a project. +2. Click on the project drop-down menu at the top of the page, and click on the **New Project** button. +3. Enter a name for your project and click on the **Create** button. +4. Go to [API Library](https://console.cloud.google.com/apis/library) in Google Cloud console. +5. Search for **Youtube Data API** in the search bar and click on it. +6. Click on the **Enable** button to enable the API. +7. Repeat steps 5 and 6 for the **People API**. +8. Go to [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in Google Cloud console. +9. Select **External** here for starting your app in testing mode at first. Click on the **Create** button. +10. Fill **App Name**, **User Support Email**, and **Developer Contact Information**. Click on the **Save and Continue** button. +11. Skip adding or removing scopes and click on the **Save and Continue** button. +12. Click on the **Add Users** button and add a test email because only test users can access the app while publishing status is set to "Testing". +13. Click on the **Save and Continue** button and now you have configured the consent screen. +14. Go to [Credentials](https://console.cloud.google.com/apis/credentials) in Google Cloud console. +15. Click on the **Create Credentials** button and select the **OAuth client ID** option. +16. Select the application type as **Web application** and fill the **Name** field. +17. Copy **OAuth Redirect URL** from Automatisch to **Authorized redirect URIs** field, and click on the **Create** button. +18. Copy the **Your Client ID** value from the following popup to the `Client ID` field on Automatisch. +19. Copy the **Your Client Secret** value from the following popup to the `Client Secret` field on Automatisch. +20. Click **Submit** button on Automatisch. +21. Congrats! Start using your new Youtube connection within the flows. diff --git a/packages/docs/pages/apps/youtube/triggers.md b/packages/docs/pages/apps/youtube/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..f0a80c1b8fbb0097dd05a23f3ef74091be59b1f4 --- /dev/null +++ b/packages/docs/pages/apps/youtube/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/youtube.svg +items: + - name: New video in channel + desc: Triggers when a new video is published to a specific Youtube channel. + - name: New video by search + desc: Triggers when a new video is uploaded that matches a specific search string. +--- + + + + diff --git a/packages/docs/pages/apps/zendesk/actions.md b/packages/docs/pages/apps/zendesk/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..4a1f3bbe1b917c11310f89140b5f1d96755a77d2 --- /dev/null +++ b/packages/docs/pages/apps/zendesk/actions.md @@ -0,0 +1,22 @@ +--- +favicon: /favicons/zendesk.svg +items: + - name: Create ticket + desc: Creates a new ticket. + - name: Create user + desc: Creates a new user. + - name: Delete ticket + desc: Deletes an existing ticket. + - name: Delete user + desc: Deletes an existing user. + - name: Find ticket + desc: Finds an existing ticket. + - name: Update ticket + desc: Modify the status of an existing ticket or append comments. +--- + + + + diff --git a/packages/docs/pages/apps/zendesk/connection.md b/packages/docs/pages/apps/zendesk/connection.md new file mode 100644 index 0000000000000000000000000000000000000000..08d074dcf61f182e9622658de2e14f3672f44916 --- /dev/null +++ b/packages/docs/pages/apps/zendesk/connection.md @@ -0,0 +1,21 @@ +# Zendesk + +:::info +This page explains the steps you need to follow to set up the Zendesk +connection in Automatisch. If any of the steps are outdated, please let us know! +::: + +1. Fill `Zendesk Subdomain URL` with your dashboard URL, for example: `https://yourcompany.zendesk.com`. +2. Go to your Zendesk dashboard. +3. Click on **Zendesk Products** at the top right corner and click **Admin Center** from the dropdown. +4. Enter **App and integrations** section. +5. Click on **Zendesk API** from the sidebar. +6. Click on **OAuth Clients** tab. +7. Click on **Add OAuth Client** button. +8. Enter necessary information in the form. +9. Copy **OAuth Redirect URL** from Automatisch to **Redirect URLs** field in the form. +10. Enter your preferred client ID value in **Unique Identifier** field. +11. Save the form to complete creating the OAuth client. +12. Copy the `Unique identifier` value from the page to the `Client ID` field on Automatisch. +13. Copy the `Secret` value from the page to the `Client Secret` field on Automatisch. +14. Now, you can start using the Zendesk connection with Automatisch. diff --git a/packages/docs/pages/apps/zendesk/triggers.md b/packages/docs/pages/apps/zendesk/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..0846d7b9475e2c6cace08f96ece7875c36fc1561 --- /dev/null +++ b/packages/docs/pages/apps/zendesk/triggers.md @@ -0,0 +1,14 @@ +--- +favicon: /favicons/zendesk.svg +items: + - name: New tickets + desc: Triggers when a new ticket is created in a specific view. + - name: New users + desc: Triggers upon the creation of a new user. +--- + + + + diff --git a/packages/docs/pages/assets/flow-900.png b/packages/docs/pages/assets/flow-900.png new file mode 100644 index 0000000000000000000000000000000000000000..25f6d1a358ca49cc715727f88e5a0009b8f1755d Binary files /dev/null and b/packages/docs/pages/assets/flow-900.png differ diff --git a/packages/docs/pages/build-integrations/actions.md b/packages/docs/pages/build-integrations/actions.md new file mode 100644 index 0000000000000000000000000000000000000000..e7328999a7687ebb50ecbbd95b92a8fde04aa0ad --- /dev/null +++ b/packages/docs/pages/build-integrations/actions.md @@ -0,0 +1,127 @@ +# Actions + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +## Add actions to the app. + +Open the `thecatapi/index.js` file and add the highlighted lines for actions. + +```javascript{4,17} +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; +import actions from './actions/index.js'; + +export default defineApp({ + name: 'The cat API', + key: 'thecatapi', + iconUrl: '{BASE_URL}/apps/thecatapi/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/thecatapi/connection', + supportsConnections: true, + baseUrl: 'https://thecatapi.com', + apiBaseUrl: 'https://api.thecatapi.com', + primaryColor: '000000', + auth, + triggers + actions +}); +``` + +## Define actions + +Create the `actions/index.js` file inside of the `thecatapi` folder. + +```javascript +import markCatImageAsFavorite from './mark-cat-image-as-favorite/index.js'; + +export default [markCatImageAsFavorite]; +``` + +:::tip +If you add new actions, you need to add them to the actions/index.js file and export all actions as an array. +::: + +## Add metadata + +Create the `actions/mark-cat-image-as-favorite/index.js` file inside the `thecatapi` folder. + +```javascript +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + name: 'Mark the cat image as favorite', + key: 'markCatImageAsFavorite', + description: 'Marks the cat image as favorite.', + arguments: [ + { + label: 'Image ID', + key: 'imageId', + type: 'string', + required: true, + description: 'The ID of the cat image you want to mark as favorite.', + variables: true, + }, + ], + + async run($) { + // TODO: Implement action! + }, +}); +``` + +Let's briefly explain what we defined here. + +- `name`: The name of the action. +- `key`: The key of the action. This is used to identify the action in Automatisch. +- `description`: The description of the action. +- `arguments`: The arguments of the action. These are the values that the user provides when using the action. +- `run`: The function that is executed when the action is executed. + +## Implement the action + +Open the `actions/mark-cat-image-as-favorite.js` file and add the highlighted lines. + +```javascript{7-20} +import defineAction from '../../../../helpers/define-action.js'; + +export default defineAction({ + // ... + + async run($) { + const requestPath = '/v1/favourites'; + const imageId = $.step.parameters.imageId; + + const headers = { + 'x-api-key': $.auth.data.apiKey, + }; + + const response = await $.http.post( + requestPath, + { image_id: imageId }, + { headers } + ); + + $.setActionItem({ raw: response.data }); + }, +}); +``` + +In this action, we send a request to the cat API to mark the cat image as favorite. We used the `$.http.post` method to send the request. The request body contains the image ID as it's required by the API. + +`$.setActionItem` is used to set the result of the action, so we set the response data as the action item. This is used to display the result of the action in the Automatisch UI and can be used in the next steps of the workflow. + +## Test the action + +Go to the flows page of Automatisch and create a new flow. Add the `Search cat images` as a trigger in the flow. Add the `Mark the cat image as favorite` action to the flow as a second step. Add one of the image IDs you got from the cat API as `Image ID` argument to the action. Click `Test & Continue` button. If you a see JSON response in the user interface, it means that both the trigger and the action we built are working properly. diff --git a/packages/docs/pages/build-integrations/app.md b/packages/docs/pages/build-integrations/app.md new file mode 100644 index 0000000000000000000000000000000000000000..6002c7cf0070762ebd08846dd7910221d79439cd --- /dev/null +++ b/packages/docs/pages/build-integrations/app.md @@ -0,0 +1,77 @@ +# App + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +Let's start building our first app by using [TheCatApi](https://thecatapi.com/) service. It's a service that provides cat images and allows you to vote or favorite a specific cat image. It's an excellent example to demonstrate how Automatisch works with an API that has authentication and data fetching with pagination. + +We will build an app with the `Search cat images` trigger and `Mark the cat image as favorite` action. So we will learn how to build both triggers and actions. + +## Define the app + +The first thing we need to do is to create a folder inside of the apps in the backend package. + +```bash +cd packages/backend/src/apps +mkdir thecatapi +``` + +We need to create an `index.js` file inside of the `thecatapi` folder. + +```bash +cd thecatapi +touch index.js +``` + +Then let's define the app inside of the `index.js` file as follows: + +```javascript +import defineApp from '../../helpers/define-app.js'; + +export default defineApp({ + name: 'The cat API', + key: 'thecatapi', + iconUrl: '{BASE_URL}/apps/thecatapi/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/thecatapi/connection', + supportsConnections: true, + baseUrl: 'https://thecatapi.com', + apiBaseUrl: 'https://api.thecatapi.com', + primaryColor: '000000', +}); +``` + +- `name` is the displayed name of the app in Automatisch. +- `key` is the unique key of the app. It's used to identify the app in Automatisch. +- `iconUrl` is the URL of the app icon. It's used in Automatisch to display the app icon. You can use `{BASE_URL}` placeholder to refer to the base URL of the app. We expect you to place the SVG icon as `assets/favicon.svg` file. +- `authDocUrl` is the URL of the documentation page that describes how to connect to the app. It's used in Automatisch to display the documentation link on the connection page. +- `supportsConnections` is a boolean that indicates whether the app supports connections or not. If it's `true`, Automatisch will display the connection page for the app. Some apps like RSS and Scheduler do not support connections since they do not have authentication. +- `baseUrl` is the base URL of the third-party service. +- `apiBaseUrl` is the API URL of the third-party service. +- `primaryColor` is the primary color of the app. It's used in Automatisch to generate the app icon if it does not provide an icon. You can put any hex color code that reflects the branding of the third-party service. + +## Create the favicon + +Even though we have defined the `iconUrl` inside the app definition, we still need to create the icon file. Let's create the `assets` folder inside the `thecatapi` folder and save [this SVG file](../public/example-app/cat.svg) as `favicon.svg` inside of the `assets` folder. + +:::tip +If you're looking for SVG icons for third-party services, you can use the following repositories. + +- [gilbarbara/logos](https://github.com/gilbarbara/logos) +- [edent/SuperTinyIcons](https://github.com/edent/SuperTinyIcons) + +::: + +## Test the app definition + +Now, you can go to the `My Apps` page on Automatisch and click on `Add connection` button, and then you will see `The cat API` service with the icon. diff --git a/packages/docs/pages/build-integrations/auth.md b/packages/docs/pages/build-integrations/auth.md new file mode 100644 index 0000000000000000000000000000000000000000..a63734e8aea98d825dda7e82980c5066bc704244 --- /dev/null +++ b/packages/docs/pages/build-integrations/auth.md @@ -0,0 +1,201 @@ +# Auth + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +## Sign up for the cat API + +Go to the [sign up page](https://thecatapi.com/signup) of the cat API and register your account. It allows you to have 10k requests per month with a free account. You will get an API key by email after the registration. We will use this API key for authentication later on. + +## The cat API docs + +You can find detailed documentation of the cat API [here](https://docs.thecatapi.com). You need to revisit this page while building the integration. + +## Add auth to the app + +Open the `thecatapi/index.js` file and add the highlighted lines for authentication. + +```javascript{2,13} +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; + +export default defineApp({ + name: 'The cat API', + key: 'thecatapi', + iconUrl: '{BASE_URL}/apps/thecatapi/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/thecatapi/connection', + supportsConnections: true, + baseUrl: 'https://thecatapi.com', + apiBaseUrl: 'https://api.thecatapi.com', + primaryColor: '000000', + auth, +}); +``` + +## Define auth fields + +Let's create the `auth/index.js` file inside of the `thecatapi` folder. + +```bash +mkdir auth +touch auth/index.js +``` + +Then let's start with defining fields the auth inside of the `auth/index.js` file as follows: + +```javascript +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'API key of the cat API service.', + clickToCopy: false, + }, + ], +}; +``` + +We have defined two fields for the auth. + +- The `apiKey` field will be used to authenticate the requests to the cat API. +- The `screenName` field will be used to identify the connection on the Automatisch UI. + +:::warning +You have to add a screen name field in case there is no API endpoint where you can get the username or any other information about the user that you can use as a screen name. Some of the APIs have an endpoint for this purpose like `/me` or `/users/me`, but in our example, the cat API doesn't have such an endpoint. +::: + +:::danger +If the third-party service you use provides both an API key and OAuth for the authentication, we expect you to use OAuth instead of an API key. Please consider that when you create a pull request for a new integration. Otherwise, we might ask you to have changes to use OAuth. To see apps with OAuth implementation, you can check [examples](/build-integrations/examples#_3-legged-oauth). +::: + +## Verify credentials + +So until now, we integrated auth folder with the app definition and defined the auth fields. Now we need to verify the credentials that the user entered. We will do that by defining the `verifyCredentials` method. + +Start with adding the `verifyCredentials` method to the `auth/index.js` file. + +```javascript{1,8} +import verifyCredentials from './verify-credentials.js'; + +export default { + fields: [ + // ... + ], + + verifyCredentials, +}; +``` + +Let's create the `verify-credentials.js` file inside the `auth` folder. + +```javascript +const verifyCredentials = async ($) => { + // TODO: Implement verification of the credentials +}; + +export default verifyCredentials; +``` + +We generally use the `users/me` endpoint or any other endpoint that we can validate the API key or any other credentials that the user provides. For our example, we don't have a specific API endpoint to check whether the credentials are correct or not. So we will randomly pick one of the API endpoints, which will be the `GET /v1/images/search` endpoint. We will send a request to this endpoint with the API key. If the API key is correct, we will get a response from the API. If the API key is incorrect, we will get an error response from the API. + +Let's implement the authentication logic that we mentioned above in the `verify-credentials.js` file. + +```javascript +const verifyCredentials = async ($) => { + await $.http.get('/v1/images/search'); + + await $.auth.set({ + screenName: $.auth.data.screenName, + }); +}; + +export default verifyCredentials; +``` + +Here we send a request to the `/v1/images/search` endpoint with the API key. If we get a response from the API, we will set the screen name to the auth data. If we get an error response from the API, it will throw an error. + +:::warning +You must always provide a `screenName` field to auth data in the `verifyCredentials` method. Otherwise, the connection will not have a name and it will not function properly in the user interface. +::: + +## Is still verified? + +We have implemented the `verifyCredentials` method. Now we need to check whether the credentials are still valid or not for the test connection functionality in Automatisch. We will do that by defining the `isStillVerified` method. + +Start with adding the `isStillVerified` method to the `auth/index.js` file. + +```javascript{2,10} +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + // ... + ], + + verifyCredentials, + isStillVerified, +}; +``` + +Let's create the `is-still-verified.js` file inside the `auth` folder. + +```javascript +import verifyCredentials from './verify-credentials.js'; + +const isStillVerified = async ($) => { + await verifyCredentials($); + return true; +}; + +export default isStillVerified; +``` + +:::info +`isStillVerified` method needs to return the `truthy` value if the credentials are still valid. +::: + +We will use the `verifyCredentials` method to check whether the credentials are still valid or not. If the credentials are still valid, we will return `true`. Otherwise, it will throw an error which will automatically be handled by Automatisch. + +:::warning +You might be wondering why we need to have two separate functions even though we use only one of them behind the scenes in this scenario. That might be true in our example or any other APIs similar to the cat API but there are some other third-party APIs that we can't use the same functionality directly to check whether the credentials are still valid or not. So we need to have two separate functions for verifying the credentials and checking whether the credentials are still valid or not. +::: + +:::tip +If your integration requires you to connect through the authorization URL of the third-party service, you need to use the `generateAuthUrl` method together with the `verifyCredentials` and the `isStillVerified` methods. Check [3-legged OAuth](/build-integrations/examples#_3-legged-oauth) examples to see how to implement them. +::: + +## Test the authentication + +Now we have completed the authentication of the cat API. Go to the `My Apps` page in Automatisch, try to add a new connection, select `The Cat API` and use the `API Key` you got with an email. Then you can also check the test connection and reconnect functionality there. + +Let's move on to the next page to build a trigger. diff --git a/packages/docs/pages/build-integrations/examples.md b/packages/docs/pages/build-integrations/examples.md new file mode 100644 index 0000000000000000000000000000000000000000..d8c89ccd7b623579fab2eaa53aec5aa11a5b99c4 --- /dev/null +++ b/packages/docs/pages/build-integrations/examples.md @@ -0,0 +1,80 @@ +# Examples + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +## Authentication + +### 3-legged OAuth + +- [Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/auth/index.js) +- [Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/auth/index.js) +- [Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/auth/index.js) +- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js) +- [Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/auth/index.js) +- [Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/auth/index.js) + +### OAuth with the refresh token + +- [Salesforce](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/salesforce/auth/index.js) + +### API key + +- [DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/auth/index.js) +- [Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/auth/index.js) +- [SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/auth/index.js) +- [SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/auth/index.js) + +### Without authentication + +- [RSS](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/rss/index.js) +- [Scheduler](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/scheduler/index.js) + +## Triggers + +### Polling-based triggers + +- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js) +- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js) + +### Webhook-based triggers + +:::warning +If you are developing a webhook-based trigger, you need to ensure that the webhook is publicly accessible. You can use [ngrok](https://ngrok.com) for this purpose and override the webhook URL by setting the **WEBHOOK_URL** environment variable. +::: + +- [New entry - Typeform](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/typeform/triggers/new-entry/index.js) + +### Pagination with descending order + +- [Search tweets - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/triggers/search-tweets/index.js) +- [New issues - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-issues/index.js) +- [Receive SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/triggers/receive-sms/index.js) +- [Receive SMS - SignalWire](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/signalwire/triggers/receive-sms/index.js) +- [New photos - Flickr](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/flickr/triggers/new-photos/index.js) + +### Pagination with ascending order + +- [New stargazers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-stargazers/index.js) +- [New watchers - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/triggers/new-watchers/index.js) + +## Actions + +- [Send a message to channel - Slack](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/index.js) +- [Send SMS - Twilio](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twilio/actions/send-sms/index.js) +- [Send a message to channel - Discord](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/discord/actions/send-message-to-channel/index.js) +- [Create issue - Github](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/github/actions/create-issue/index.js) +- [Send an email - SMTP](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/smtp/actions/send-email/index.js) +- [Create tweet - Twitter](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/twitter/actions/create-tweet/index.js) +- [Translate text - DeepL](https://github.com/automatisch/automatisch/tree/main/packages/backend/src/apps/deepl/actions/translate-text/index.js) diff --git a/packages/docs/pages/build-integrations/folder-structure.md b/packages/docs/pages/build-integrations/folder-structure.md new file mode 100644 index 0000000000000000000000000000000000000000..bfaa49e3f410d6f3f2fbd54bec0f5c99698fb6f6 --- /dev/null +++ b/packages/docs/pages/build-integrations/folder-structure.md @@ -0,0 +1,68 @@ +# Folder Structure + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +:::warning +If you still need to set up the development environment, please go back to the [development setup](/contributing/development-setup) page and follow the instructions. +::: + +:::tip +We will use the terms **integration** and **app** interchangeably in the documentation. +::: + +Before diving into how to build an integration for Automatisch, it's better to check the folder structure of the apps and give you some idea about how we place different parts of the app. + +## Folder structure of an app + +Here, you can see the folder structure of an example app. We will briefly walk through the folders, explain what they are used for, and dive into the details in the following pages. + +``` +. +β”œβ”€β”€ actions +β”œβ”€β”€ assets +β”œβ”€β”€ auth +β”œβ”€β”€ common +β”œβ”€β”€ dynamic-data +β”œβ”€β”€ index.js +└── triggers +``` + +## App + +The `index.js` file is the entry point of the app. It contains the definition of the app and the app's metadata. It also includes the list of triggers, actions, and data sources that the app provides. So, whatever you build inside the app, you need to associate it within the `index.js` file. + +## Auth + +We ask users to authenticate with their third-party service accounts (we also document how they can accomplish this for each app.), and we store the encrypted credentials in our database. Later on, we use the credentials to make requests to the third-party service when we use them within triggers and actions. Auth folder is responsible for getting those credentials and saving them as connections for later use. + +## Triggers + +Triggers are the starting points of the flows. The first step in the flow always has to be a trigger. Triggers are responsible for fetching data from the third-party service and sending it to the next steps of the flow, which are actions. + +## Actions + +As mentioned above, actions are the steps we place after a trigger. Actions are responsible for getting data from their previous steps and taking action with that data. For example, when a new issue is created in GitHub, which is working with a trigger, we can send a message to the Slack channel, which will happen with an action from the Slack application. + +## Common + +The common folder is where you can put utilities or shared functionality used by other folders like triggers, actions, auth, etc. + +## Dynamic data + +Sometimes you need to get some dynamic data with the user interface to set up the triggers or actions. For example, to use the new issues trigger from the GitHub app, we need to select the repository we want to track for the new issues. This selection should load the repository list from GitHub. This is where the data folder comes into play. You can put your data fetching logic here when it doesn't belong to triggers or actions but is used to set up triggers or actions in the Automatisch user interface. + +## Assets + +It is the folder we designed to put the app's static files, but currently we support serving only the `favicon.svg` file from the folder. diff --git a/packages/docs/pages/build-integrations/global-variable.md b/packages/docs/pages/build-integrations/global-variable.md new file mode 100644 index 0000000000000000000000000000000000000000..df0e564bba05e1099e9bf0207bca29f6778bb14d --- /dev/null +++ b/packages/docs/pages/build-integrations/global-variable.md @@ -0,0 +1,106 @@ +# Global Variable + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +Before handling authentication and building a trigger and an action, it's better to explain the `global variable` concept in Automatisch. Automatisch provides you the global variable that you need to use with authentication, triggers, actions, and basically all the stuff you will build for the integration. + +The global variable is represented as `$` variable in the codebase, and it's a JS object that contains the following properties: + +## $.auth.set + +```javascript +await $.auth.set({ + key: 'value', +}); +``` + +It's used to set the authentication data, and you can use this method with multiple pairs. The data will be stored in the database and can be retrieved later by using `$.auth.data` property. The data you set with this method will not override its current value but expands it. We use this method when we store the credentials of the third-party service. Note that Automatisch encrypts the data before storing it in the database. + +## $.auth.data + +```javascript +$.auth.data; // { key: 'value' } +``` + +It's used to retrieve the authentication data that we set with `$.auth.set()`. The data will be retrieved from the database. We use the data property with the key name when we need to get one specific value from the data object. + +## $.app.baseUrl + +```javascript +$.app.baseUrl; // https://thecatapi.com +``` + +It's used to retrieve the base URL of the app that we defined previously. In our example, it returns `https://thecatapi.com`. We use this property when we need to use the base URL of the third-party service. + +## $.app.apiBaseUrl + +```javascript +$.app.apiBaseUrl; // https://api.thecatapi.com +``` + +It's used to retrieve the API base URL of the app that we defined previously. In our example, it returns `https://api.thecatapi.com`. We use this property when we need to use the API base URL of the third-party service. + +## $.app.auth.fields + +```javascript +$.app.auth.fields; +``` + +It's used to retrieve the fields that we defined in the `auth` section of the app. We use this property when we need to get the fields of the authentication section of the app. + +## $.http + +It's an HTTP client to be used for making HTTP requests. It's a wrapper around the [axios](https://axios-http.com) library. We use this property when we need to make HTTP requests to the third-party service. The `apiBaseUrl` field we set up in the app will be used as the base URL for the HTTP requests. For example, to search the cat images, we can use the following code: + +```javascript +await $.http.get('/v1/images/search?order=DESC', { + headers: { + 'x-api-key': $.auth.data.apiKey, + }, +}); +``` + +Keep in mind that the HTTP client handles the error with the status code that falls out of the range of 2xx. So, you don't need to handle the error manually. It will be processed with the error message or error payload that you can check on the execution details page in Automatisch. + +## $.step.parameters + +```javascript +$.step.parameters; // { key: 'value' } +``` + +It refers to the parameters that are set by users in the UI. We use this property when we need to get the parameters for corresponding triggers and actions. For example [Send a message to channel](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/apps/slack/actions/send-a-message-to-channel/post-message.js) action from Slack integration, we have a step parameter called `message` that we need to use in the action. We can use `$.step.parameters.message` to get the value of the message to send a message to the Slack channel. + +## $.pushTriggerItem + +```javascript +$.pushTriggerItem({ + raw: resourceData, + meta: { + id: resourceData.id, + }, +}); +``` + +It's used to push trigger data to be processed by Automatisch. It must reflect the data that we get from the third-party service. Let's say for search tweets trigger the `resourceData` will be the JSON that represents the single tweet object. + +## $.setActionItem + +```javascript +$.setActionItem({ + raw: resourceData, +}); +``` + +It's used to set the action data to be processed by Automatisch. For actions, it reflects the response data that we get from the third-party service. Let's say for create tweet action it will be the JSON that represents the response payload we get while creating a tweet. diff --git a/packages/docs/pages/build-integrations/triggers.md b/packages/docs/pages/build-integrations/triggers.md new file mode 100644 index 0000000000000000000000000000000000000000..7b82f4fd3300986dd978ba20d62bafda6b957db3 --- /dev/null +++ b/packages/docs/pages/build-integrations/triggers.md @@ -0,0 +1,157 @@ +# Triggers + +:::info + +The build integrations section is best understood when read from beginning to end. To get the most value out of it, start from the first page and read through page by page. + +1. [Folder structure](/build-integrations/folder-structure) +2. [App](/build-integrations/app) +3. [Global variable](/build-integrations/global-variable) +4. [Auth](/build-integrations/auth) +5. [Triggers](/build-integrations/triggers) +6. [Actions](/build-integrations/actions) +7. [Examples](/build-integrations/examples) + +::: + +:::warning +We used a polling-based HTTP trigger in our example but if you need to use a webhook-based trigger, you can check the [examples](/build-integrations/examples#webhook-based-triggers) page. +::: + +## Add triggers to the app + +Open the `thecatapi/index.js` file and add the highlighted lines for triggers. + +```javascript{3,15} +import defineApp from '../../helpers/define-app.js'; +import auth from './auth/index.js'; +import triggers from './triggers/index.js'; + +export default defineApp({ + name: 'The cat API', + key: 'thecatapi', + iconUrl: '{BASE_URL}/apps/thecatapi/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/thecatapi/connection', + supportsConnections: true, + baseUrl: 'https://thecatapi.com', + apiBaseUrl: 'https://api.thecatapi.com', + primaryColor: '000000', + auth, + triggers +}); +``` + +## Define triggers + +Create the `triggers/index.js` file inside of the `thecatapi` folder. + +```javascript +import searchCatImages from './search-cat-images/index.js'; + +export default [searchCatImages]; +``` + +:::tip +If you add new triggers, you need to add them to the `triggers/index.js` file and export all triggers as an array. The order of triggers in this array will be reflected in the Automatisch user interface. +::: + +## Add metadata + +Create the `triggers/search-cat-images/index.js` file inside of the `thecatapi` folder. + +```javascript +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + name: 'Search cat images', + key: 'searchCatImages', + pollInterval: 15, + description: 'Triggers when there is a new cat image.', + + async run($) { + // TODO: Implement trigger! + }, +}); +``` + +Let's briefly explain what we defined here. + +- `name`: The name of the trigger. +- `key`: The key of the trigger. This is used to identify the trigger in Automatisch. +- `pollInterval`: The interval in minutes in which the trigger should be polled. Even though we allow to define `pollInterval` field, it's not used in Automatisch at the moment. Currently, the default is 15 minutes and it's not possible to change it. +- `description`: The description of the trigger. +- `run`: The function that is executed when the trigger is triggered. + +## Implement the trigger + +:::danger + +- Automatisch expects you to push data in **reverse-chronological order** otherwise, the trigger will not work properly. +- You have to push the `unique identifier` (it can be IDs or any field that can be used to identify the data) as `internalId`. This is used to prevent duplicate data. + +::: + +Implement the `run` function by adding highlighted lines. + +```javascript{1,7-30} +import defineTrigger from '../../../../helpers/define-trigger.js'; + +export default defineTrigger({ + // ... + async run($) { + let page = 0; + let response; + + const headers = { + 'x-api-key': $.auth.data.apiKey, + }; + + do { + let requestPath = `/v1/images/search?page=${page}&limit=10&order=DESC`; + response = await $.http.get(requestPath, { headers }); + + response.data.forEach((image) => { + const dataItem = { + raw: image, + meta: { + internalId: image.id + }, + }; + + $.pushTriggerItem(dataItem); + }); + + page += 1; + } while (response.data.length >= 10); + }, +}); +``` + +We are using the `$.http` object to make HTTP requests. Our API is paginated, so we are making requests until we get less than 10 items, which means the last page. + +We do not have to return anything from the `run` function. We are using the `$.pushTriggerItem` function to push the data to Automatisch. $.pushTriggerItem accepts an object with the following fields: + +- `raw`: The raw data that you want to push to Automatisch. +- `meta`: The metadata of the data. It has to have the `internalId` field. + +:::tip + +`$.pushTriggerItem` is smart enough to understand if the data is already pushed to Automatisch or not. If the data is already pushed and processed, it will stop the trigger, otherwise, it will continue to fetch new data. The check is done by comparing the `internalId` field with the `internalId` field of the data that is already processed. The control of whether the data is already processed or not is scoped by the flow. + +::: + +:::tip + +`$.pushTriggerItem` also understands whether the trigger is executed with `Test & Continue` button in the user interface or it's a trigger from a published flow. If the trigger is executed with `Test & Continue` button, it will push only one item regardless of whether we already processed the data or not and early exits the process, otherwise, it will fetch the remaining data. + +::: + +:::tip + +Let's say the trigger started to execute. It fetched the first five pages of data from the third-party API with five different HTTP requests and you still need to get the next page but you started to get an API rate limit error. In this case, Automatisch will not lose the data that is already fetched from the first five requests. It stops the trigger when it got the error the first time but processes all previously fetched data. + +::: + +## Test the trigger + +Go to the flows page of Automatisch and create a new flow. Choose `The cat API` app and the `Search cat images` trigger and click `Test & Continue` button. If you a see JSON response in the user interface, it means that the trigger is working properly. diff --git a/packages/docs/pages/components/CustomListing.vue b/packages/docs/pages/components/CustomListing.vue new file mode 100644 index 0000000000000000000000000000000000000000..d9bdca124fd15ac0cbc436ee08b22075cee504b0 --- /dev/null +++ b/packages/docs/pages/components/CustomListing.vue @@ -0,0 +1,24 @@ + + + diff --git a/packages/docs/pages/contributing/contribution-guide.md b/packages/docs/pages/contributing/contribution-guide.md new file mode 100644 index 0000000000000000000000000000000000000000..dd84098695e1960581b4e6ff0b162d268edc2c9e --- /dev/null +++ b/packages/docs/pages/contributing/contribution-guide.md @@ -0,0 +1,25 @@ +# Contribution Guide + +We are happy that you want to contribute to Automatisch. We will assist you in the contribution process. This guide will help you to get started. + +## We develop with GitHub + +We use GitHub to host code, track issues, and feature requests, as well as accept pull requests. You can follow those steps to contribute to the project: + +1. Fork the repository and create your branch from the `main`. +2. Create your feature branch (`git checkout -b feature/feature-description`) +3. If you've added code that should be documented, update the documentation. +4. Make sure to use the linter by running `yarn lint` command in the project root folder. +5. Create a pull request! + +## Use conventional commit messages + +We use [conventional commit messages](https://www.conventionalcommits.org) to generate changelogs and release notes. Therefore, please follow the guidelines when writing commit messages. + +## Report bugs using GitHub issues + +We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/automatisch/automatisch/issues/new). + +## License + +By contributing, you agree that your contributions will be licensed under the AGPL-3.0 license. diff --git a/packages/docs/pages/contributing/development-setup.md b/packages/docs/pages/contributing/development-setup.md new file mode 100644 index 0000000000000000000000000000000000000000..2863c2d311c70677c0a9ae612228d562e016faa3 --- /dev/null +++ b/packages/docs/pages/contributing/development-setup.md @@ -0,0 +1,90 @@ +# Development Setup + +Clone main branch of Automatisch. + +```bash +git clone git@github.com:automatisch/automatisch.git +``` + +Then, install the dependencies. + +```bash +cd automatisch +yarn install +``` + +## Backend + +Make sure that you have **PostgreSQL** and **Redis** installed and running. + +:::warning +Scripts we have prepared for Automatisch work with PostgreSQL version 14. If you have a different version, you might have some problems with the database setup. +::: + +Create a `.env` file in the backend package: + +```bash +cd packages/backend +cp .env-example .env +``` + +Create the development database in the backend folder. + +```bash +yarn db:create +``` + +:::warning +`yarn db:create` commands expect that you have the `postgres` superuser. If not, you can create a superuser called `postgres` manually or you can create the database manually by checking PostgreSQL-related default values from the [app config](https://github.com/automatisch/automatisch/blob/main/packages/backend/src/config/app.js). +::: + +Run the database migrations in the backend folder. + +```bash +yarn db:migrate +``` + +Create a seed user with `user@automatisch.io` email and `sample` password. + +```bash +yarn db:seed:user +``` + +Start the main backend server. + +```bash +yarn dev +``` + +Start the worker server in another terminal tab. + +```bash +yarn worker +``` + +## Frontend + +Create a `.env` file in the web package: + +```bash +cd packages/web +cp .env-example .env +``` + +Start the frontend server in another terminal tab. + +```bash +cd packages/web +yarn dev +``` + +It will automatically open [http://localhost:3001](http://localhost:3001) in your browser. Then, use the `user@automatisch.io` email address and `sample` password to login. + +## Docs server + +```bash +cd packages/docs +yarn dev +``` + +You can check the docs server via [http://localhost:3002](http://localhost:3002). diff --git a/packages/docs/pages/contributing/repository-structure.md b/packages/docs/pages/contributing/repository-structure.md new file mode 100644 index 0000000000000000000000000000000000000000..23533cca9d838f2e01f50b82ccc0c78ac8111748 --- /dev/null +++ b/packages/docs/pages/contributing/repository-structure.md @@ -0,0 +1,21 @@ +# Repository Structure + +We use `lerna` with `yarn workspaces` to manage the mono repository. We have the following packages: + +``` +. +β”œβ”€β”€ packages +β”‚Β Β  β”œβ”€β”€ backend +β”‚Β Β  β”œβ”€β”€ cli +β”‚Β Β  β”œβ”€β”€ docs +β”‚Β Β  β”œβ”€β”€ e2e-tests +β”‚Β Β  β”œβ”€β”€ types +β”‚Β Β  └── web +``` + +- `backend` - The backend package contains the backend application and all integrations. +- `cli` - The cli package contains the CLI application of Automatisch. +- `docs` - The docs package contains the documentation website. +- `e2e-tests` - The e2e-tests package contains the end-to-end tests for the internal usage. +- `types` - The types package contains the shared types for both the backend and web packages. +- `web` - The web package contains the frontend application of Automatisch. diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md new file mode 100644 index 0000000000000000000000000000000000000000..ad2b4c0c4ebc71432c67e2863fe47c527e812f70 --- /dev/null +++ b/packages/docs/pages/guide/available-apps.md @@ -0,0 +1,61 @@ +# Available Apps + +The following integrations are currently supported by Automatisch. + +- [Airtable](/apps/airtable/actions) +- [Appwrite](/apps/appwrite/triggers) +- [Carbone](/apps/carbone/actions) +- [Datastore](/apps/datastore/actions) +- [DeepL](/apps/deepl/actions) +- [Delay](/apps/delay/actions) +- [Discord](/apps/discord/actions) +- [Disqus](/apps/disqus/triggers) +- [Dropbox](/apps/dropbox/actions) +- [Filter](/apps/filter/actions) +- [Flickr](/apps/flickr/triggers) +- [Formatter](/apps/formatter/actions) +- [Ghost](/apps/ghost/triggers) +- [GitHub](/apps/github/triggers) +- [GitLab](/apps/gitlab/triggers) +- [Google Calendar](/apps/google-calendar/triggers) +- [Google Drive](/apps/google-drive/triggers) +- [Google Forms](/apps/google-forms/triggers) +- [Google Sheets](/apps/google-sheets/triggers) +- [Google Tasks](/apps/google-tasks/actions) +- [HTTP Request](/apps/http-request/actions) +- [HubSpot](/apps/hubspot/actions) +- [Invoice Ninja](/apps/invoice-ninja/triggers) +- [Mattermost](/apps/mattermost/actions) +- [Miro](/apps/miro/actions) +- [Notion](/apps/notion/triggers) +- [Ntfy](/apps/ntfy/actions) +- [Odoo](/apps/odoo/actions) +- [OpenAI](/apps/openai/actions) +- [Pipedrive](/apps/pipedrive/triggers) +- [Placetel](/apps/placetel/triggers) +- [PostgreSQL](/apps/postgresql/actions) +- [Pushover](/apps/pushover/actions) +- [Reddit](/apps/reddit/triggers) +- [Remove.bg](/apps/removebg/actions) +- [RSS](/apps/rss/triggers) +- [Salesforce](/apps/salesforce/triggers) +- [Scheduler](/apps/scheduler/triggers) +- [SignalWire](/apps/signalwire/triggers) +- [Slack](/apps/slack/actions) +- [SMTP](/apps/smtp/actions) +- [Spotify](/apps/spotify/actions) +- [Strava](/apps/strava/actions) +- [Stripe](/apps/stripe/triggers) +- [Telegram](/apps/telegram-bot/actions) +- [Todoist](/apps/todoist/triggers) +- [Trello](/apps/trello/actions) +- [Twilio](/apps/twilio/triggers) +- [Twitter](/apps/twitter/triggers) +- [Typeform](/apps/typeform/triggers) +- [Vtiger CRM](/apps/vtiger-crm/triggers) +- [Webhooks](/apps/webhooks/triggers) +- [WordPress](/apps/wordpress/triggers) +- [Xero](/apps/xero/triggers) +- [You Need A Budget](/apps/you-need-a-budget/triggers) +- [Youtube](/apps/youtube/triggers) +- [Zendesk](/apps/zendesk/actions) diff --git a/packages/docs/pages/guide/create-flow.md b/packages/docs/pages/guide/create-flow.md new file mode 100644 index 0000000000000000000000000000000000000000..4f7e8499100d576c2a17c2486cfb6b255c0eb72d --- /dev/null +++ b/packages/docs/pages/guide/create-flow.md @@ -0,0 +1,53 @@ +# Create Flow + +To understand how we can create a flow, it's better to start with a real use case. Let's say we want to create a flow that will fetch new submissions from Typeform and then send them to a Slack channel. To do that, we will use [Typeform](/apps/typeform/triggers) and [Slack](/apps/slack/actions) apps. Let's start with creating connections for these apps. + +## Typeform connection + +- Go to the **My Apps** page in Automatisch and click on **Add connection** button. +- Select the **Typeform** app from the list. +- It will ask you `Client ID` and `Client Secret` from Typeform and there is an information box above the fields. +- Click on **our documentation** link in the information box and follow the instructions to get the `Client ID` and `Client Secret` from Typeform. + +:::tip +Whenever you want to create a connection for an app, you can click on **our documentation** link in the information box to learn how to create a connection for that specific app. +::: + +- After you get the `Client ID` and `Client Secret` from Typeform, you can paste them to the fields in Automatisch and click on **Submit** button. + +## Slack connection + +- Go to the **My Apps** page in Automatisch and click on **Add connection** button. +- Select the **Slack** app from the list. +- It will ask you `API Key` and `API Secret` values from Slack and there is an information box above the fields. +- Click on **our documentation** link in the information box and follow the instructions to get the `API Key` and `API Secret` from Slack. +- After you get the `API Key` and `API Secret` from Slack, you can paste them into the fields in Automatisch and click on **Submit** button. + +## Build the flow + +### Trigger step + +- Go to the **Flows** page in Automatisch and click on **Create flow** button. +- It will give you empty trigger and action steps. +- For the trigger step (1st step), select the **Typeform** app from `Choose an app` dropdown. +- Select the **New entry** as the trigger event and click on the **Continue** button. +- It will ask you to select the connection you created for the Typeform app. Select the connection you have just created and click on the **Continue** button. +- Select the form you want to get the new entries from and click on the **Continue** button. +- Click on **Test & Continue** button to test the trigger step. If you see the data that reflects the recent submission in the form, you can continue to the next (action) step. + +### Action step + +- For the action step (2nd step), select the **Slack** app from `Choose an app` dropdown. +- Select the **Send a message to channel** as the action event and click on the **Continue** button. +- It will ask you to select the connection you created for the Slack app. Select the connection you have just created and click on the **Continue** button. +- Select the channel you want to send the message to. +- Write the message you want to send to the channel. You can use variables in the message from the trigger step. +- Select `Yes` for the `Send as a bot` option. +- Give a name for the bot and click on the **Continue** button. +- Click on **Test & Continue** button to test the action step. If you see the message in the Slack channel you selected, we can say that the flow is working as expected and is ready to be published. + +### Publish the flow + +- Click on the **Publish** button to publish the flow. +- Published flows will be executed automatically when the trigger event happens or at intervals of 15 minutes depending on the trigger type. +- You can not change the flow after it's published. If you want to change the flow, you need to unpublish it first and then make the changes. diff --git a/packages/docs/pages/guide/installation.md b/packages/docs/pages/guide/installation.md new file mode 100644 index 0000000000000000000000000000000000000000..a61d5e0588d11e8b633f9dd64363849d82f766de --- /dev/null +++ b/packages/docs/pages/guide/installation.md @@ -0,0 +1,113 @@ +# Installation + +:::info +We have installation guides for docker compose and docker setup at the moment, but if you need another installation type, let us know by [creating a GitHub issue](https://github.com/automatisch/automatisch/issues/new). +::: + +:::tip + +You can use `user@automatisch.io` email address and `sample` password to login to Automatisch. Please do not forget to change your email and password from the settings page. + +::: + +:::danger +Please be careful with the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. They are used to encrypt your credentials from third-party services and verify webhook requests. If you change them, your existing connections and flows will not continue to work. +::: + +## Docker Compose + +```bash +# Clone the repository +git clone git@github.com:automatisch/automatisch.git + +# Go to the repository folder +cd automatisch + +# Start +docker compose up +``` + +✌️ That's it; you have Automatisch running. Let's check it out by browsing [http://localhost:3000](https://localhost:3000) + +### Upgrade with Docker Compose + +If you want to upgrade the Automatisch version with docker compose, first you need to pull the main branch of Automatisch repository. + +```bash +git pull origin main +``` + +Then you can run the following command to rebuild the containers with the new images. + +```bash +docker compose up --force-recreate --build +``` + +## Docker + +Automatisch comes with two services which are `main` and `worker`. They both use the same image and need to have the same environment variables except for the `WORKER` environment variable which is set to `true` for the worker service. + +::: warning +We give the sample environment variable files for the setup but you should adjust them to include your own values. +::: + +To run the main: + +```bash +docker run --env-file=./.env automatischio/automatisch +``` + +To run the worker: + +```bash +docker run --env-file=./.env -e WORKER=true automatischio/automatisch +``` + +::: details .env + +```bash +APP_ENV=production +HOST= +PROTOCOL= +PORT= +ENCRYPTION_KEY= +WEBHOOK_SECRET_KEY= +APP_SECRET_KEY= +POSTGRES_HOST= +POSTGRES_PORT= +POSTGRES_DATABASE= +POSTGRES_USERNAME= +POSTGRES_PASSWORD= +POSTGRES_ENABLE_SSL= +REDIS_HOST= +REDIS_PORT= +REDIS_USERNAME= +REDIS_PASSWORD= +REDIS_TLS= +``` + +::: + +::: info +You can use the `openssl rand -base64 36` command in your terminal to generate a random string for the `ENCRYPTION_KEY` and `WEBHOOK_SECRET_KEY` environment variables. +::: + +## Render + + + Deploy to Render + + +:::info + +We use default values of render plans with the `render.yaml` file, if you want to use the free plan or change the plan, you can change the `render.yaml` file in your fork and use your repository URL while creating a blueprint in Render. + +::: + +## Production setup + +If you need to change any other environment variables for your production setup, let's check out the [environment variables](/advanced/configuration#environment-variables) section of the configuration page. + +## Let's discover! + +If you see any problems while installing Automatisch, let us know via [github issues](https://github.com/automatisch/automatisch/issues) or our [discord server](https://discord.gg/dJSah9CVrC). diff --git a/packages/docs/pages/guide/key-concepts.md b/packages/docs/pages/guide/key-concepts.md new file mode 100644 index 0000000000000000000000000000000000000000..41b63b111883189119227d485f6a7e9ac7261b8a --- /dev/null +++ b/packages/docs/pages/guide/key-concepts.md @@ -0,0 +1,28 @@ +# Key Concepts + +We will cover four main terms of Automatisch before creating our first flow. + +## App + +πŸ‘‰ Apps are the third-party services you can use with Automatisch, like Twitter, Github and Slack. You can check the complete list of available apps [here](/guide/available-apps). Automatisch aims to connect those apps to help you build workflows. So whenever you work with other concepts of Automatisch, you will use apps. + +:::tip + +You can request a new integration [here](/guide/request-integration). We will collect all the requests and prioritize the most requested ones. + +::: + +## Connection + +πŸ“ͺ To use an app, you need to add a connection first. Connection is essentially the place where you pass the credentials of the specified service, like consumer key, consumer secret, etc., to let Automatisch connect third-party apps on your behalf. When you click "Add connection" and choose an app, you'll be prompted for the required fields for the connection. You can also add multiple connections if you have more than one account for the same app. + +## Flow + +πŸ› οΈ Flow is the most crucial part of Automatisch. It's a place to arrange the business workflow by connecting multiple steps. So, for example, we can define a flow that does: + +- **Search tweets** for the "Automatisch" keyword. +- **Send a message to channel** which posts found tweets to the specified Slack channel. + +## Step + +πŸ“„ Steps are the individual items in the flow. In our example, **searching tweets** and **sending a message to channel** are both steps in our flow. Steps have two different types, which are trigger and action. Trigger steps are the ones that start any flow you would like to build with Automatisch, like "search tweets". You can think them as starting points. Action steps are the following steps that define what you would do with the incoming data from previous steps, like "sending a message to channel" in our example. Flows can also have more than two steps. The first step of each flow should be the trigger step, and the following steps should be action steps. diff --git a/packages/docs/pages/guide/request-integration.md b/packages/docs/pages/guide/request-integration.md new file mode 100644 index 0000000000000000000000000000000000000000..263cb832011e0f655606cc00529a807528169e70 --- /dev/null +++ b/packages/docs/pages/guide/request-integration.md @@ -0,0 +1,15 @@ +# Request Integration + +You can request a new integration by using [Github issues](https://github.com/automatisch/automatisch/issues). + +:::info + +While we are working hard to add as many integrations as possible, it might take some time to see your request is being done. It's because we prefer to collect all integration requests and prioritize the most requested ones. + +::: + +:::tip + +If there is already an integration request for the service you'd like, it's still crucial to upvote or comment on that issue for us to analyze the potential audience around the integration. + +::: diff --git a/packages/docs/pages/index.md b/packages/docs/pages/index.md new file mode 100644 index 0000000000000000000000000000000000000000..864a34ff2cc5c8b48cbb08721b1e573e4630485c --- /dev/null +++ b/packages/docs/pages/index.md @@ -0,0 +1,43 @@ + + +# What is Automatisch? + +:::warning +Automatisch is still in the early phase of development. We try our best not to introduce breaking changes, but be cautious until v1 is released. +::: + +![Automatisch Flow Page](./assets/flow-900.png) + +🧐 Automatisch is a **business automation** tool that lets you connect different services like Twitter, Slack, and **[more](/guide/available-apps)** to automate your business processes. + +πŸ’Έ Automating your workflows doesn't have to be a difficult or expensive process. You also **don't need** any programming knowledge to use Automatisch. + +## How it works? + +Automatisch is a software designed to help streamline your workflows by integrating the different services you use. This way, you can avoid spending extra time and money on building integrations or hiring someone to do it for you. + +For example, you can create a workflow for your team by specifying two steps: "search all tweets that include the `Automatisch` keyword" and "post those tweets into a slack channel specified." It is one of the internal workflows we use to test our product. This example only includes Twitter and Slack services, but many more possibilities exist. You can check the list of integrations [here](/guide/available-apps). + +You need to prepare the workflow once, and it will run continuously until you stop it or the connected account gets unlinked. Currently, workflows run at intervals of 15 minutes, but we're planning to extend this behavior and support instant updates if it's available with the third-party service. + +## Advantages + +There are other existing solutions in the market, like Zapier and Integromat, so you might be wondering why you should use Automatisch. + +βœ… One of the main benefits of using Automatisch is that it allows you to **store your data on your own servers**, which is essential for businesses that handle sensitive user information and cannot risk sharing it with external cloud services. This is especially relevant for industries such as healthcare and finance, as well as for European companies that must adhere to the General Data Protection Regulation (GDPR). + +πŸ€“ Your contributions are vital to the development of Automatisch. As an **open-source software**, anyone can have an impact on how it is being developed. + +πŸ’™ **No vendor lock-in**. If you ever decide that Automatisch is no longer helpful for your business, you can switch to any other provider, which will be easier than switching from the one cloud provider to another since you have all data and flexibility. + +## Let's start! + +Visit our [installation guide](/guide/installation) to setup Automatisch. It's recommended to read through all the getting started sections in the sidebar and [create your first flow](/guide/create-flow). + +## Something missing? + +If you find issues with the documentation or have suggestions on how to improve the documentation or the project in general, please [file an issue](https://github.com/automatisch/automatisch/issues) for us, or send a tweet mentioning the [@automatischio](https://twitter.com/automatischio) Twitter account. diff --git a/packages/docs/pages/other/community.md b/packages/docs/pages/other/community.md new file mode 100644 index 0000000000000000000000000000000000000000..b6c852989e06b1d1af3ebfc4f351b85c8554f351 --- /dev/null +++ b/packages/docs/pages/other/community.md @@ -0,0 +1,7 @@ +# Community + +We believe in the power of open source and the community that surrounds it. We're constantly amazed by the creativity and collaboration that takes place in the open source world, and we're proud to be a part of it. We believe that open source is the future of software development, and we're committed to helping make that future a reality. If you would like to join our community, you can use the following channels to collaborate and have an impact on how we build Automatisch. + +- [Github](https://github.com/automatisch/automatisch) +- [Discord](https://discord.gg/dJSah9CVrC) +- [Twitter](https://twitter.com/automatischio) diff --git a/packages/docs/pages/other/license.md b/packages/docs/pages/other/license.md new file mode 100644 index 0000000000000000000000000000000000000000..acb385ef47353194e1078b59ac2976c51b35b18b --- /dev/null +++ b/packages/docs/pages/other/license.md @@ -0,0 +1,11 @@ +# License + +Automatisch Community Edition (Automatisch CE) is an open-source software with the [AGPL-3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.agpl). + +Automatisch Enterprise Edition (Automatisch EE) is a commercial offering with the [Enterprise license](https://github.com/automatisch/automatisch/blob/main/LICENSE.enterprise). + +The Automatisch repository contains both AGPL-licensed and Enterprise-licensed files. We maintain a single repository to make development easier. + +All files that contain ".ee." in their name fall under the [Enterprise license](https://github.com/automatisch/automatisch/blob/main/LICENSE.enterprise). All other files fall under the [AGPL-3.0 license](https://github.com/automatisch/automatisch/blob/main/LICENSE.agpl). + +See the [LICENSE](https://github.com/automatisch/automatisch/blob/main/LICENSE) file for more information. diff --git a/packages/docs/pages/public/example-app/cat.svg b/packages/docs/pages/public/example-app/cat.svg new file mode 100644 index 0000000000000000000000000000000000000000..b44cf96c0a429e9ef123ccfbfa159736cc4b46e3 --- /dev/null +++ b/packages/docs/pages/public/example-app/cat.svg @@ -0,0 +1,34 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/airtable.svg b/packages/docs/pages/public/favicons/airtable.svg new file mode 100644 index 0000000000000000000000000000000000000000..867c3b5aef65f41fac7721df1d2f975454ff9fad --- /dev/null +++ b/packages/docs/pages/public/favicons/airtable.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/appwrite.svg b/packages/docs/pages/public/favicons/appwrite.svg new file mode 100644 index 0000000000000000000000000000000000000000..63bf0f237c8c6017e7a1a58ca6b96f9ac7597411 --- /dev/null +++ b/packages/docs/pages/public/favicons/appwrite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/carbone.svg b/packages/docs/pages/public/favicons/carbone.svg new file mode 100644 index 0000000000000000000000000000000000000000..cadf2c90003436e83d8967bacf2ac499935db5cf --- /dev/null +++ b/packages/docs/pages/public/favicons/carbone.svg @@ -0,0 +1,444 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/datastore.svg b/packages/docs/pages/public/favicons/datastore.svg new file mode 100644 index 0000000000000000000000000000000000000000..c45032f5b6f876429e62ede590b46196abdefffa --- /dev/null +++ b/packages/docs/pages/public/favicons/datastore.svg @@ -0,0 +1,13 @@ + + + + + + datastore + + + + + + + diff --git a/packages/docs/pages/public/favicons/deepl.svg b/packages/docs/pages/public/favicons/deepl.svg new file mode 100644 index 0000000000000000000000000000000000000000..7b96b43ee8d0617a100f3b1145fcc1941583c24d --- /dev/null +++ b/packages/docs/pages/public/favicons/deepl.svg @@ -0,0 +1,39 @@ + + image/svg+xml + + + + + + + background + + + + Layer 1 + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/delay.svg b/packages/docs/pages/public/favicons/delay.svg new file mode 100644 index 0000000000000000000000000000000000000000..af5da4d3ac1944569407c76734276dfbae3b85bc --- /dev/null +++ b/packages/docs/pages/public/favicons/delay.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/docs/pages/public/favicons/discord.svg b/packages/docs/pages/public/favicons/discord.svg new file mode 100644 index 0000000000000000000000000000000000000000..307c77037f801b176f0099ad9c5692f0dd7a0a92 --- /dev/null +++ b/packages/docs/pages/public/favicons/discord.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/docs/pages/public/favicons/disqus.svg b/packages/docs/pages/public/favicons/disqus.svg new file mode 100644 index 0000000000000000000000000000000000000000..66ffeb34fe4ba22b20370c51b8fe356db018d683 --- /dev/null +++ b/packages/docs/pages/public/favicons/disqus.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/dropbox.svg b/packages/docs/pages/public/favicons/dropbox.svg new file mode 100644 index 0000000000000000000000000000000000000000..59f3862655f672491be1f279241fa273eda0f6b3 --- /dev/null +++ b/packages/docs/pages/public/favicons/dropbox.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/filter.svg b/packages/docs/pages/public/favicons/filter.svg new file mode 100644 index 0000000000000000000000000000000000000000..4ea1aa96ca79b7c834b4dcfc7f0293fe699c82ce --- /dev/null +++ b/packages/docs/pages/public/favicons/filter.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/docs/pages/public/favicons/flickr.svg b/packages/docs/pages/public/favicons/flickr.svg new file mode 100644 index 0000000000000000000000000000000000000000..f8499a7a99386655ecd47ac866d660411170e154 --- /dev/null +++ b/packages/docs/pages/public/favicons/flickr.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/docs/pages/public/favicons/formatter.svg b/packages/docs/pages/public/favicons/formatter.svg new file mode 100644 index 0000000000000000000000000000000000000000..858aed39134e68e7a4381675d7205df875bc2093 --- /dev/null +++ b/packages/docs/pages/public/favicons/formatter.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/docs/pages/public/favicons/ghost.svg b/packages/docs/pages/public/favicons/ghost.svg new file mode 100644 index 0000000000000000000000000000000000000000..e98fb6fd106a6f940e6dff55298e10744d52f1a2 --- /dev/null +++ b/packages/docs/pages/public/favicons/ghost.svg @@ -0,0 +1,60 @@ + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/github.svg b/packages/docs/pages/public/favicons/github.svg new file mode 100644 index 0000000000000000000000000000000000000000..b49b4e23d6f561da61fdd95aab9be07c35cc929c --- /dev/null +++ b/packages/docs/pages/public/favicons/github.svg @@ -0,0 +1,6 @@ + diff --git a/packages/docs/pages/public/favicons/gitlab.svg b/packages/docs/pages/public/favicons/gitlab.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c7cb0719db6ab4898b89ac3b0d87077f0344111 --- /dev/null +++ b/packages/docs/pages/public/favicons/gitlab.svg @@ -0,0 +1 @@ + diff --git a/packages/docs/pages/public/favicons/google-calendar.svg b/packages/docs/pages/public/favicons/google-calendar.svg new file mode 100644 index 0000000000000000000000000000000000000000..14b505ab8f71be5d04cb93d0fe91655888aab268 --- /dev/null +++ b/packages/docs/pages/public/favicons/google-calendar.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/google-drive.svg b/packages/docs/pages/public/favicons/google-drive.svg new file mode 100644 index 0000000000000000000000000000000000000000..a8cefd5b28bf56133b0cb8fb440072e5832ca6ad --- /dev/null +++ b/packages/docs/pages/public/favicons/google-drive.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/google-forms.svg b/packages/docs/pages/public/favicons/google-forms.svg new file mode 100644 index 0000000000000000000000000000000000000000..d1836fd0ef4395e466d3aed3b32fdcb69df0fdd7 --- /dev/null +++ b/packages/docs/pages/public/favicons/google-forms.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/google-sheets.svg b/packages/docs/pages/public/favicons/google-sheets.svg new file mode 100644 index 0000000000000000000000000000000000000000..9bdc9721f46fa626400b12e0265d322c6ed9b9c0 --- /dev/null +++ b/packages/docs/pages/public/favicons/google-sheets.svg @@ -0,0 +1,89 @@ + + + + Sheets-icon + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/google-tasks.svg b/packages/docs/pages/public/favicons/google-tasks.svg new file mode 100644 index 0000000000000000000000000000000000000000..1de5d7ab3e8fa819ad553863f613ee88a867419c --- /dev/null +++ b/packages/docs/pages/public/favicons/google-tasks.svg @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/http-request.svg b/packages/docs/pages/public/favicons/http-request.svg new file mode 100644 index 0000000000000000000000000000000000000000..87c7dae5ef2e6f47c586c6d80758299484783fa9 --- /dev/null +++ b/packages/docs/pages/public/favicons/http-request.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/hubspot.svg b/packages/docs/pages/public/favicons/hubspot.svg new file mode 100644 index 0000000000000000000000000000000000000000..c21891fb37ddcfa5b0dbf15e0e3aa62d1b289fcc --- /dev/null +++ b/packages/docs/pages/public/favicons/hubspot.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/docs/pages/public/favicons/invoice-ninja.svg b/packages/docs/pages/public/favicons/invoice-ninja.svg new file mode 100644 index 0000000000000000000000000000000000000000..59d3f11fbcfb9db3b64086f7927f4da569f52d78 --- /dev/null +++ b/packages/docs/pages/public/favicons/invoice-ninja.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/mattermost.svg b/packages/docs/pages/public/favicons/mattermost.svg new file mode 100644 index 0000000000000000000000000000000000000000..47da0ba18a5615c557de9952c3ec2acf9c16e8f3 --- /dev/null +++ b/packages/docs/pages/public/favicons/mattermost.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/docs/pages/public/favicons/miro.svg b/packages/docs/pages/public/favicons/miro.svg new file mode 100644 index 0000000000000000000000000000000000000000..b87ea9a1c8d7accee9bb6163db6af662c4f07a06 --- /dev/null +++ b/packages/docs/pages/public/favicons/miro.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/notion.svg b/packages/docs/pages/public/favicons/notion.svg new file mode 100644 index 0000000000000000000000000000000000000000..ebcbe81614fd5b66206f43c798c84ceb71f3d4e0 --- /dev/null +++ b/packages/docs/pages/public/favicons/notion.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/docs/pages/public/favicons/ntfy.svg b/packages/docs/pages/public/favicons/ntfy.svg new file mode 100644 index 0000000000000000000000000000000000000000..9e5b5136fd9740a5ed0c6e40b7c305d6d62c3183 --- /dev/null +++ b/packages/docs/pages/public/favicons/ntfy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/odoo.svg b/packages/docs/pages/public/favicons/odoo.svg new file mode 100644 index 0000000000000000000000000000000000000000..aeb5dd77231be423d39beec6e5f30c60800a553a --- /dev/null +++ b/packages/docs/pages/public/favicons/odoo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/openai.svg b/packages/docs/pages/public/favicons/openai.svg new file mode 100644 index 0000000000000000000000000000000000000000..b62b84eb144e7679e9ad93882da71d38730c2ade --- /dev/null +++ b/packages/docs/pages/public/favicons/openai.svg @@ -0,0 +1,6 @@ + + OpenAI + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/pipedrive.svg b/packages/docs/pages/public/favicons/pipedrive.svg new file mode 100644 index 0000000000000000000000000000000000000000..5efad428c07210934d2a9190a337f981bfc92ff7 --- /dev/null +++ b/packages/docs/pages/public/favicons/pipedrive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/placetel.svg b/packages/docs/pages/public/favicons/placetel.svg new file mode 100644 index 0000000000000000000000000000000000000000..6df467ad37a4711786f72c28c9dac7fbab8ef8c0 --- /dev/null +++ b/packages/docs/pages/public/favicons/placetel.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/docs/pages/public/favicons/postgres.svg b/packages/docs/pages/public/favicons/postgres.svg new file mode 100644 index 0000000000000000000000000000000000000000..0bdb3e3e7c04c2aa393d3e4b75b902aa568879b6 --- /dev/null +++ b/packages/docs/pages/public/favicons/postgres.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/pushover.svg b/packages/docs/pages/public/favicons/pushover.svg new file mode 100644 index 0000000000000000000000000000000000000000..28492a9ec5f50a4d889deb5318c33d5c2c11abe3 --- /dev/null +++ b/packages/docs/pages/public/favicons/pushover.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/docs/pages/public/favicons/reddit.svg b/packages/docs/pages/public/favicons/reddit.svg new file mode 100644 index 0000000000000000000000000000000000000000..e41ae322d45d447a59d9c82d87138baf08f9213c --- /dev/null +++ b/packages/docs/pages/public/favicons/reddit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/removebg.svg b/packages/docs/pages/public/favicons/removebg.svg new file mode 100644 index 0000000000000000000000000000000000000000..80197555613146af25e1105f4044709d0ce6a019 --- /dev/null +++ b/packages/docs/pages/public/favicons/removebg.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/rss.svg b/packages/docs/pages/public/favicons/rss.svg new file mode 100644 index 0000000000000000000000000000000000000000..0c3e5023e19cf51241db63af28d434df1ff054cf --- /dev/null +++ b/packages/docs/pages/public/favicons/rss.svg @@ -0,0 +1,6 @@ + diff --git a/packages/docs/pages/public/favicons/salesforce.svg b/packages/docs/pages/public/favicons/salesforce.svg new file mode 100644 index 0000000000000000000000000000000000000000..e82db677c67d19fd553e1703869a66842ac09de5 --- /dev/null +++ b/packages/docs/pages/public/favicons/salesforce.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/scheduler.svg b/packages/docs/pages/public/favicons/scheduler.svg new file mode 100644 index 0000000000000000000000000000000000000000..d80c10100e626e180fdabe37cb760a702e55ff08 --- /dev/null +++ b/packages/docs/pages/public/favicons/scheduler.svg @@ -0,0 +1 @@ + diff --git a/packages/docs/pages/public/favicons/signalwire.svg b/packages/docs/pages/public/favicons/signalwire.svg new file mode 100644 index 0000000000000000000000000000000000000000..1dda203761aaf207ca0bfd072536601d0ca15e63 --- /dev/null +++ b/packages/docs/pages/public/favicons/signalwire.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/slack.svg b/packages/docs/pages/public/favicons/slack.svg new file mode 100644 index 0000000000000000000000000000000000000000..81629c7cd5ee94ef658b0d69d0ebee483546a1c1 --- /dev/null +++ b/packages/docs/pages/public/favicons/slack.svg @@ -0,0 +1,6 @@ + diff --git a/packages/docs/pages/public/favicons/smtp.svg b/packages/docs/pages/public/favicons/smtp.svg new file mode 100644 index 0000000000000000000000000000000000000000..57f0fa58d4bb8c40cc87188680c24c175f1d5bc1 --- /dev/null +++ b/packages/docs/pages/public/favicons/smtp.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/docs/pages/public/favicons/spotify.svg b/packages/docs/pages/public/favicons/spotify.svg new file mode 100644 index 0000000000000000000000000000000000000000..f84a03c6d453a154a1ab681ea647dab44e824f5d --- /dev/null +++ b/packages/docs/pages/public/favicons/spotify.svg @@ -0,0 +1,6 @@ + + Spotify + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/strava.svg b/packages/docs/pages/public/favicons/strava.svg new file mode 100644 index 0000000000000000000000000000000000000000..ddd7c85581073318bb69e4aea40c094167318b3f --- /dev/null +++ b/packages/docs/pages/public/favicons/strava.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/stripe.svg b/packages/docs/pages/public/favicons/stripe.svg new file mode 100644 index 0000000000000000000000000000000000000000..25d00aaa5e21c13ef5a6eb71c917e01e1102ab56 --- /dev/null +++ b/packages/docs/pages/public/favicons/stripe.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/telegram-bot.svg b/packages/docs/pages/public/favicons/telegram-bot.svg new file mode 100644 index 0000000000000000000000000000000000000000..8f16fb17ea7a18e4e2613c325a3507a2c9f30c20 --- /dev/null +++ b/packages/docs/pages/public/favicons/telegram-bot.svg @@ -0,0 +1,14 @@ + + + Telegram + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/todoist.svg b/packages/docs/pages/public/favicons/todoist.svg new file mode 100644 index 0000000000000000000000000000000000000000..679cdc6315139df8b030f040f3dbd0734facbbfe --- /dev/null +++ b/packages/docs/pages/public/favicons/todoist.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/trello.svg b/packages/docs/pages/public/favicons/trello.svg new file mode 100644 index 0000000000000000000000000000000000000000..7c63adb974aa2dfc498b23ac0f413e5c54794909 --- /dev/null +++ b/packages/docs/pages/public/favicons/trello.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/twilio.svg b/packages/docs/pages/public/favicons/twilio.svg new file mode 100644 index 0000000000000000000000000000000000000000..7c20e190d990a553757072507ad0681330d59e54 --- /dev/null +++ b/packages/docs/pages/public/favicons/twilio.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/packages/docs/pages/public/favicons/twitter.svg b/packages/docs/pages/public/favicons/twitter.svg new file mode 100644 index 0000000000000000000000000000000000000000..752cdc8d4b1a920a0b4f6bc067648b72bd8a54d2 --- /dev/null +++ b/packages/docs/pages/public/favicons/twitter.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/docs/pages/public/favicons/typeform.svg b/packages/docs/pages/public/favicons/typeform.svg new file mode 100644 index 0000000000000000000000000000000000000000..f0fabb1cbec52e51e852d6e6cf9e2960b0a03e1e --- /dev/null +++ b/packages/docs/pages/public/favicons/typeform.svg @@ -0,0 +1,4 @@ + + Typeform + + diff --git a/packages/docs/pages/public/favicons/vtiger-crm.svg b/packages/docs/pages/public/favicons/vtiger-crm.svg new file mode 100644 index 0000000000000000000000000000000000000000..0d95870c39314fe3eebbbf68b5db18e39fa01bf2 --- /dev/null +++ b/packages/docs/pages/public/favicons/vtiger-crm.svg @@ -0,0 +1,925 @@ + + + + diff --git a/packages/docs/pages/public/favicons/webhooks.svg b/packages/docs/pages/public/favicons/webhooks.svg new file mode 100644 index 0000000000000000000000000000000000000000..894b13e55220becbbe3af4b75f2226796be15475 --- /dev/null +++ b/packages/docs/pages/public/favicons/webhooks.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/docs/pages/public/favicons/wordpress.svg b/packages/docs/pages/public/favicons/wordpress.svg new file mode 100644 index 0000000000000000000000000000000000000000..39be6e125d8d54c92cd8cff6188003a4f10e06b8 --- /dev/null +++ b/packages/docs/pages/public/favicons/wordpress.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/xero.svg b/packages/docs/pages/public/favicons/xero.svg new file mode 100644 index 0000000000000000000000000000000000000000..e1cd725fc04a3a94e8eeff0c70f0a54084708e16 --- /dev/null +++ b/packages/docs/pages/public/favicons/xero.svg @@ -0,0 +1 @@ +Xero homepageBeautiful business \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/you-need-a-budget.svg b/packages/docs/pages/public/favicons/you-need-a-budget.svg new file mode 100644 index 0000000000000000000000000000000000000000..c83333c8821a630b01cad266167ad36a969198ed --- /dev/null +++ b/packages/docs/pages/public/favicons/you-need-a-budget.svg @@ -0,0 +1,25 @@ + + + + My Budget + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/youtube.svg b/packages/docs/pages/public/favicons/youtube.svg new file mode 100644 index 0000000000000000000000000000000000000000..e54d503dd5f4ac4132df24a2fb726c72051c4754 --- /dev/null +++ b/packages/docs/pages/public/favicons/youtube.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/docs/pages/public/favicons/zendesk.svg b/packages/docs/pages/public/favicons/zendesk.svg new file mode 100644 index 0000000000000000000000000000000000000000..882b45164c556e9c1eb20ab7ee7dbf8274270b15 --- /dev/null +++ b/packages/docs/pages/public/favicons/zendesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/e2e-tests/.gitignore b/packages/e2e-tests/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..16bda59e8de37fe073ba73886893e8230e1f35a4 --- /dev/null +++ b/packages/e2e-tests/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/playwright/.cache/ +/output diff --git a/packages/e2e-tests/README.md b/packages/e2e-tests/README.md new file mode 100644 index 0000000000000000000000000000000000000000..9b097e9cb5c631aaf9e6918cd7b0d098a9b56fa3 --- /dev/null +++ b/packages/e2e-tests/README.md @@ -0,0 +1,51 @@ +# Setting up the test environment +In order to get tests running, there are a few requirements + +1. Setting up the development/test environment +2. Installing playwright +3. Installing the test browsers +4. Setting up environment variables for testing +5. Running in vscode + +## Setting up the development/test environment + +Following the instructions found in the [development documentation](https://automatisch.io/docs/contributing/development-setup) is a great place to start. Note there is one **caveat** + +> You should have the backend server be running off of a **non-production database**. This is because the test suite will actively **drop the database and reset** between test runs in order to ensure repeatability of tests. + +## Installing playwright and test browsers + +You can install all the required packages by going into the tests package + +```sh +cd packages/e2e-tests +``` + +and installing the required dependencies with + +```sh +yarn install +``` + +At the end of installation, this should display a CLI for installing the test browsers. For more information, check out the [Playwright documentation](https://playwright.dev/docs/intro#installing-playwright). + +### Installing the test browsers + +If you find you need to install the browsers for running tests at a later time, you can run + +```sh +npx playwright install +``` + +and it should install the associated browsers for the test running. For more information, check out the [Playwright documentation](https://playwright.dev/docs/browsers#install-browsers). + + +## Running in VSCode + +We recommend using [Playwright Test for VSCode](https://marketplace.visualstudio.com/items?itemName=ms-playwright.playwright) maintained by Microsoft. This lets you run playwright tests from within the code editor, giving you access to additional tools, such as easily running subsets of tests. + +# Test failures + +If there are failing tests in the test suite, this can be caused by a myriad of reasons, but one of the best places to start is either running the test in a headed browser, looking at the associated trace file for the failed test, or checking out the output of a failed GitHub Action. + +Playwright has their [own documentation]() on the trace viewer which is very helpful for reviewing the exact browser steps made during a failed test execution. diff --git a/packages/e2e-tests/fixtures/admin/create-role-page.js b/packages/e2e-tests/fixtures/admin/create-role-page.js new file mode 100644 index 0000000000000000000000000000000000000000..3426e520fd61a9d4d43a678696d40fd23d9ea8b1 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/create-role-page.js @@ -0,0 +1,107 @@ +const { AuthenticatedPage } = require('../authenticated-page'); +const { RoleConditionsModal } = require('./role-conditions-modal'); + +export class AdminCreateRolePage extends AuthenticatedPage { + screenshotPath = '/admin/create-role'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.nameInput = page.getByTestId('name-input'); + this.descriptionInput = page.getByTestId('description-input'); + this.createButton = page.getByTestId('create-button'); + this.connectionRow = page.getByTestId('Connection-permission-row'); + this.executionRow = page.getByTestId('Execution-permission-row'); + this.flowRow = page.getByTestId('Flow-permission-row'); + this.pageTitle = page.getByTestId('create-role-title'); + } + + /** + * @param {('Connection'|'Execution'|'Flow')} subject + */ + getRoleConditionsModal(subject) { + return new RoleConditionsModal(this.page, subject); + } + + async getPermissionConfigs() { + const subjects = ['Connection', 'Flow', 'Execution']; + const permissionConfigs = []; + for (let subject of subjects) { + const row = this.getSubjectRow(subject); + const actionInputs = await this.getRowInputs(row); + Object.keys(actionInputs).forEach((action) => { + permissionConfigs.push({ + action, + locator: actionInputs[action], + subject, + row, + }); + }); + } + return permissionConfigs; + } + + /** + * + * @param {( + * 'Connection' | 'Flow' | 'Execution' + * )} subject + */ + getSubjectRow(subject) { + const k = `${subject.toLowerCase()}Row`; + if (this[k]) { + return this[k]; + } else { + throw 'Unknown row'; + } + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async getRowInputs(row) { + const inputs = { + // settingsButton: row.getByTestId('permission-settings-button') + }; + for (let input of ['create', 'read', 'update', 'delete', 'publish']) { + const testId = `${input}-checkbox`; + if ((await row.getByTestId(testId).count()) > 0) { + inputs[input] = row.getByTestId(testId).locator('input'); + } + } + return inputs; + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickPermissionSettings(row) { + await row.getByTestId('permission-settings-button').click(); + } + + /** + * + * @param {string} subject + * @param {'create'|'read'|'update'|'delete'|'publish'} action + * @param {boolean} val + */ + async updateAction(subject, action, val) { + const row = await this.getSubjectRow(subject); + const inputs = await this.getRowInputs(row); + if (inputs[action]) { + if (await inputs[action].isChecked()) { + if (!val) { + await inputs[action].click(); + } + } else { + if (val) { + await inputs[action].click(); + } + } + } else { + throw new Error(`${subject} does not have action ${action}`); + } + } +} diff --git a/packages/e2e-tests/fixtures/admin/create-user-page.js b/packages/e2e-tests/fixtures/admin/create-user-page.js new file mode 100644 index 0000000000000000000000000000000000000000..222802cf824d87b939cafe934087b4d47ec34daa --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/create-user-page.js @@ -0,0 +1,31 @@ +const { faker } = require('@faker-js/faker'); +const { AuthenticatedPage } = require('../authenticated-page'); + +export class AdminCreateUserPage extends AuthenticatedPage { + screenshot = '/admin/create-user'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.fullNameInput = page.getByTestId('full-name-input'); + this.emailInput = page.getByTestId('email-input'); + this.passwordInput = page.getByTestId('password-input'); + this.roleInput = page.getByTestId('role.id-autocomplete'); + this.createButton = page.getByTestId('create-button'); + this.pageTitle = page.getByTestId('create-user-title'); + } + + seed(seed) { + faker.seed(seed || 0); + } + + generateUser() { + return { + fullName: faker.person.fullName(), + email: faker.internet.email().toLowerCase(), + password: faker.internet.password(), + }; + } +} diff --git a/packages/e2e-tests/fixtures/admin/delete-role-modal.js b/packages/e2e-tests/fixtures/admin/delete-role-modal.js new file mode 100644 index 0000000000000000000000000000000000000000..e456f6bdd976e10e07956557ca15d654eef86eed --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/delete-role-modal.js @@ -0,0 +1,19 @@ +export class DeleteRoleModal { + screenshotPath = '/admin/delete-role-modal'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + this.page = page; + this.modal = page.getByTestId('delete-role-modal'); + this.cancelButton = this.modal.getByTestId('confirmation-cancel-button'); + this.deleteButton = this.modal.getByTestId('confirmation-confirm-button'); + } + + async close () { + await this.page.click('body', { + position: { x: 10, y: 10 } + }); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/delete-user-modal.js b/packages/e2e-tests/fixtures/admin/delete-user-modal.js new file mode 100644 index 0000000000000000000000000000000000000000..6082d0e3f723ed02ecad5aab98f5af0ae34eec93 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/delete-user-modal.js @@ -0,0 +1,19 @@ +export class DeleteUserModal { + screenshotPath = '/admin/delete-modal'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + this.page = page; + this.modal = page.getByTestId('delete-user-modal'); + this.cancelButton = this.modal.getByTestId('confirmation-cancel-button'); + this.deleteButton = this.modal.getByTestId('confirmation-confirm-button'); + } + + async close () { + await this.page.click('body', { + position: { x: 10, y: 10 } + }) + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/edit-role-page.js b/packages/e2e-tests/fixtures/admin/edit-role-page.js new file mode 100644 index 0000000000000000000000000000000000000000..9dd21dfeb8e72e28e6bbbcd2e732262c6c60fed3 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/edit-role-page.js @@ -0,0 +1,10 @@ +const { AdminCreateRolePage } = require('./create-role-page') + +export class AdminEditRolePage extends AdminCreateRolePage { + constructor (page) { + super(page); + delete this.createButton; + this.updateButton = page.getByTestId('update-button'); + this.pageTitle = page.getByTestId('edit-role-title'); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/edit-user-page.js b/packages/e2e-tests/fixtures/admin/edit-user-page.js new file mode 100644 index 0000000000000000000000000000000000000000..9308294babd2180276f4366ccdc85859a6fc393d --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/edit-user-page.js @@ -0,0 +1,37 @@ +const { faker } = require('@faker-js/faker'); +const { AuthenticatedPage } = require('../authenticated-page'); + +faker.seed(9002); + +export class AdminEditUserPage extends AuthenticatedPage { + screenshot = '/admin/edit-user'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.fullNameInput = page.getByTestId('full-name-input'); + this.emailInput = page.getByTestId('email-input'); + this.roleInput = page.getByTestId('role.id-autocomplete'); + this.updateButton = page.getByTestId('update-button'); + this.pageTitle = page.getByTestId('edit-user-title'); + } + + /** + * @param {string} fullName + */ + async waitForLoad(fullName) { + return await this.page.waitForFunction((fullName) => { + const el = document.querySelector("[data-test='full-name-input']"); + return el && el.value === fullName; + }, fullName); + } + + generateUser() { + return { + fullName: faker.person.fullName(), + email: faker.internet.email(), + }; + } +} diff --git a/packages/e2e-tests/fixtures/admin/index.js b/packages/e2e-tests/fixtures/admin/index.js new file mode 100644 index 0000000000000000000000000000000000000000..8c25fd7c6c0a00c403c1eab9a925caa6999b95da --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/index.js @@ -0,0 +1,29 @@ +const { AdminCreateUserPage } = require('./create-user-page'); +const { AdminEditUserPage } = require('./edit-user-page'); +const { AdminUsersPage } = require('./users-page'); + +const { AdminRolesPage } = require('./roles-page'); +const { AdminCreateRolePage } = require('./create-role-page'); +const { AdminEditRolePage } = require('./edit-role-page'); + +export const adminFixtures = { + adminUsersPage: async ({ page }, use) => { + await use(new AdminUsersPage(page)); + }, + adminCreateUserPage: async ({ page }, use) => { + await use(new AdminCreateUserPage(page)); + }, + adminEditUserPage: async ({page}, use) => { + await use(new AdminEditUserPage(page)); + }, + adminRolesPage: async ({ page}, use) => { + await use(new AdminRolesPage(page)); + }, + adminEditRolePage: async ({ page}, use) => { + await use(new AdminEditRolePage(page)); + }, + adminCreateRolePage: async ({ page}, use) => { + await use(new AdminCreateRolePage(page)); + }, +} + diff --git a/packages/e2e-tests/fixtures/admin/role-conditions-modal.js b/packages/e2e-tests/fixtures/admin/role-conditions-modal.js new file mode 100644 index 0000000000000000000000000000000000000000..4e2c64fe0d7ce5ebbb58aed69fbce904b537be41 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/role-conditions-modal.js @@ -0,0 +1,47 @@ +export class RoleConditionsModal { + + /** + * @param {import('@playwright/test').Page} page + * @param {('Connection'|'Execution'|'Flow')} subject + */ + constructor (page, subject) { + this.page = page; + this.modal = page.getByTestId(`${subject}-role-conditions-modal`); + this.modalBody = this.modal.getByTestId('role-conditions-modal-body'); + this.createCheckbox = this.modal.getByTestId( + 'isCreator-create-checkbox' + ).locator('input'); + this.readCheckbox = this.modal.getByTestId( + 'isCreator-read-checkbox' + ).locator('input'); + this.updateCheckbox = this.modal.getByTestId( + 'isCreator-update-checkbox' + ).locator('input'); + this.deleteCheckbox = this.modal.getByTestId( + 'isCreator-delete-checkbox' + ).locator('input'); + this.publishCheckbox = this.modal.getByTestId( + 'isCreator-publish-checkbox' + ).locator('input'); + this.applyButton = this.modal.getByTestId('confirmation-confirm-button'); + this.cancelButton = this.modal.getByTestId('confirmation-cancel-button'); + } + + async getAvailableConditions () { + let conditions = {}; + const actions = ['create', 'read', 'update', 'delete', 'publish']; + for (let action of actions) { + const locator = this[`${action}Checkbox`]; + if (locator && await locator.count() > 0) { + conditions[action] = locator; + } + } + return conditions; + } + + async close () { + await this.page.click('body', { + position: { x: 10, y: 10 } + }); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/roles-page.js b/packages/e2e-tests/fixtures/admin/roles-page.js new file mode 100644 index 0000000000000000000000000000000000000000..e46279f7176addece50fdbce2f884cf65790ec71 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/roles-page.js @@ -0,0 +1,81 @@ +const { AuthenticatedPage } = require('../authenticated-page'); +const { DeleteRoleModal } = require('./delete-role-modal'); + +export class AdminRolesPage extends AuthenticatedPage { + screenshotPath = '/admin-roles'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.roleDrawerLink = page.getByTestId('roles-drawer-link'); + this.createRoleButton = page.getByTestId('create-role'); + this.deleteRoleModal = new DeleteRoleModal(page); + this.roleRow = page.getByTestId('role-row'); + this.rolesLoader = page.getByTestId('roles-list-loader'); + this.pageTitle = page.getByTestId('roles-page-title'); + } + + /** + * + * @param {boolean} isMobile - navigation on smaller devices requires the + * user to open up the drawer menu + */ + async navigateTo(isMobile = false) { + await this.profileMenuButton.click(); + await this.adminMenuItem.click(); + if (isMobile) { + await this.drawerMenuButton.click(); + } + await this.roleDrawerLink.click(); + await this.isMounted(); + await this.rolesLoader.waitFor({ + state: 'detached', + }); + } + + /** + * @param {string} name + */ + async getRoleRowByName(name) { + await this.rolesLoader.waitFor({ + state: 'detached', + }); + return this.roleRow.filter({ + has: this.page.getByTestId('role-name').getByText(name, { exact: true }), + }); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async getRowData(row) { + return { + role: await row.getByTestId('role-name').textContent(), + description: await row.getByTestId('role-description').textContent(), + canEdit: await row.getByTestId('role-edit').isEnabled(), + canDelete: await row.getByTestId('role-delete').isEnabled(), + }; + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickEditRole(row) { + await row.getByTestId('role-edit').click(); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickDeleteRole(row) { + await row.getByTestId('role-delete').click(); + return this.deleteRoleModal; + } + + async editRole(subject) { + const row = await this.getRoleRowByName(subject); + await this.clickEditRole(row); + } +} diff --git a/packages/e2e-tests/fixtures/admin/users-page.js b/packages/e2e-tests/fixtures/admin/users-page.js new file mode 100644 index 0000000000000000000000000000000000000000..4c96fd753364c00bcb0b0c046b0442edbb2c1201 --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/users-page.js @@ -0,0 +1,134 @@ +const { faker } = require('@faker-js/faker'); +const { AuthenticatedPage } = require('../authenticated-page'); +const { DeleteUserModal } = require('./delete-user-modal'); + +faker.seed(9001); + +export class AdminUsersPage extends AuthenticatedPage { + screenshotPath = '/admin'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + this.createUserButton = page.getByTestId('create-user'); + this.userRow = page.getByTestId('user-row'); + this.deleteUserModal = new DeleteUserModal(page); + this.firstPageButton = page.getByTestId('first-page-button'); + this.previousPageButton = page.getByTestId('previous-page-button'); + this.nextPageButton = page.getByTestId('next-page-button'); + this.lastPageButton = page.getByTestId('last-page-button'); + this.usersLoader = page.getByTestId('users-list-loader'); + this.pageTitle = page.getByTestId('users-page-title'); + } + + async navigateTo() { + await this.profileMenuButton.click(); + await this.adminMenuItem.click(); + await this.isMounted(); + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached', + }); + } + } + + /** + * @param {string} email + */ + async getUserRowByEmail(email) { + return this.userRow.filter({ + has: this.page.getByTestId('user-email').filter({ + hasText: email, + }), + }); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async getRowData(row) { + return { + fullName: await row.getByTestId('user-full-name').textContent(), + email: await row.getByTestId('user-email').textContent(), + role: await row.getByTestId('user-role').textContent(), + }; + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickEditUser(row) { + await row.getByTestId('user-edit').click(); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickDeleteUser(row) { + await row.getByTestId('delete-button').click(); + return this.deleteUserModal; + } + + /** + * @param {string} email + * @returns {import('@playwright/test').Locator | null} + */ + async findUserPageWithEmail(email) { + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached', + }); + } + // start at the first page + const firstPageDisabled = await this.firstPageButton.isDisabled(); + if (!firstPageDisabled) { + await this.firstPageButton.click(); + } + + while (true) { + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached', + }); + } + const rowLocator = await this.getUserRowByEmail(email); + console.log('rowLocator.count', email, await rowLocator.count()); + if ((await rowLocator.count()) === 1) { + return rowLocator; + } + if (await this.nextPageButton.isDisabled()) { + return null; + } else { + await this.nextPageButton.click(); + } + } + } + + async getTotalRows() { + return await this.page.evaluate(() => { + const node = document.querySelector('[data-total-count]'); + if (node) { + const count = Number(node.dataset.totalCount); + if (!isNaN(count)) { + return count; + } + } + return 0; + }); + } + + async getRowsPerPage() { + return await this.page.evaluate(() => { + const node = document.querySelector('[data-rows-per-page]'); + if (node) { + const count = Number(node.dataset.rowsPerPage); + if (!isNaN(count)) { + return count; + } + } + return 0; + }); + } +} diff --git a/packages/e2e-tests/fixtures/applications-modal.js b/packages/e2e-tests/fixtures/applications-modal.js new file mode 100644 index 0000000000000000000000000000000000000000..8833087f41d484ba9acac9c79abc3af48f4a3fa5 --- /dev/null +++ b/packages/e2e-tests/fixtures/applications-modal.js @@ -0,0 +1,34 @@ +const { GithubPage } = require('./apps/github/github-page'); +const { BasePage } = require('./base-page'); + +export class ApplicationsModal extends BasePage { + + applications = { + github: GithubPage + }; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.modal = page.getByTestId('add-app-connection-dialog'); + this.searchInput = this.modal.getByTestId('search-for-app-text-field'); + this.appListItem = this.modal.getByTestId('app-list-item'); + this.appLoader = this.modal.getByTestId('search-for-app-loader'); + } + + /** + * @param string link + */ + async selectLink (link) { + if (this.applications[link] === undefined) { + throw { + message: `Unknown link "${link}" passed to ApplicationsModal.selectLink` + } + } + await this.searchInput.fill(link); + await this.appListItem.first().click(); + return new this.applications[link](this.page); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/applications-page.js b/packages/e2e-tests/fixtures/applications-page.js new file mode 100644 index 0000000000000000000000000000000000000000..a84958e64771166e7dab56190e6076c8550b04b2 --- /dev/null +++ b/packages/e2e-tests/fixtures/applications-page.js @@ -0,0 +1,22 @@ +const path = require('node:path'); +const { ApplicationsModal } = require('./applications-modal'); +const { AuthenticatedPage } = require('./authenticated-page'); + +export class ApplicationsPage extends AuthenticatedPage { + screenshotPath = '/applications'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.drawerLink = this.page.getByTestId('apps-page-drawer-link'); + this.addConnectionButton = this.page.getByTestId('add-connection-button'); + } + + async openAddConnectionModal () { + await this.addConnectionButton.click(); + return new ApplicationsModal(this.page); + } +} diff --git a/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js b/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js new file mode 100644 index 0000000000000000000000000000000000000000..f5cd6384863f6cbfe6c1fa95b897b4914b92e52a --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/add-github-connection-modal.js @@ -0,0 +1,49 @@ +import { GithubPopup } from './github-popup'; + +const { BasePage } = require('../../base-page'); + +export class AddGithubConnectionModal extends BasePage { + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.modal = page.getByTestId('add-app-connection-dialog'); + this.oauthRedirectInput = page.getByTestId('oAuthRedirectUrl-text'); + this.clientIdInput = page.getByTestId('consumerKey-text'); + this.clientIdSecretInput = page.getByTestId('consumerSecret-text'); + this.submitButton = page.getByTestId('create-connection-button'); + } + + async visible () { + return await this.modal.isVisible(); + } + + async inputForm () { + await connectionModal.clientIdInput.fill( + process.env.GITHUB_CLIENT_ID + ); + await connectionModal.clientIdSecretInput.fill( + process.env.GITHUB_CLIENT_SECRET + ); + } + + /** + * @returns {import('@playwright/test').Page} + */ + async submit () { + const popupPromise = this.page.waitForEvent('popup'); + await this.submitButton.click(); + const popup = await popupPromise; + await popup.bringToFront(); + return popup; + } + + /** + * @param {import('@playwright/test').Page} page + */ + async handlePopup (page) { + return await GithubPopup.handle(page); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/apps/github/github-page.js b/packages/e2e-tests/fixtures/apps/github/github-page.js new file mode 100644 index 0000000000000000000000000000000000000000..f1a05d5fee5d8c384d2cb98e641734d92da9b461 --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/github-page.js @@ -0,0 +1,65 @@ +const { BasePage } = require('../../base-page'); +const { AddGithubConnectionModal } = require('./add-github-connection-modal'); + +export class GithubPage extends BasePage { + + constructor (page) { + super(page) + this.addConnectionButton = page.getByTestId('add-connection-button'); + this.connectionsTab = page.getByTestId('connections-tab'); + this.flowsTab = page.getByTestId('flows-tab'); + this.connectionRows = page.getByTestId('connection-row'); + this.flowRows = page.getByTestId('flow-row'); + this.firstConnectionButton = page.getByTestId('connections-no-results'); + this.firstFlowButton = page.getByTestId('flows-no-results'); + this.addConnectionModal = new AddGithubConnectionModal(page); + } + + async goto () { + await this.page.goto('/app/github/connections'); + } + + async openConnectionModal () { + await this.addConnectionButton.click(); + await expect(this.addConnectionButton.modal).toBeVisible(); + return this.addConnectionModal; + } + + async flowsVisible () { + return this.page.url() === await this.flowsTab.getAttribute('href'); + } + + async connectionsVisible () { + return this.page.url() === await this.connectionsTab.getAttribute('href'); + } + + async hasFlows () { + if (!(await this.flowsVisible())) { + await this.flowsTab.click(); + await expect(this.flowsTab).toBeVisible(); + } + return await this.flowRows.count() > 0 + } + + async hasConnections () { + if (!(await this.connectionsVisible())) { + await this.connectionsTab.click(); + await expect(this.connectionsTab).toBeVisible(); + } + return await this.connectionRows.count() > 0; + } +} + +/** + * + * @param {import('@playwright/test').Page} page + */ +export async function initGithubConnection (page) { + // assumes already logged in + const githubPage = new GithubPage(page); + await githubPage.goto(); + const modal = await githubPage.openConnectionModal(); + await modal.inputForm(); + const popup = await modal.submit(); + await modal.handlePopup(popup); +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/apps/github/github-popup.js b/packages/e2e-tests/fixtures/apps/github/github-popup.js new file mode 100644 index 0000000000000000000000000000000000000000..0bdb8579dd9e14329c0900e64923cdbe6f129372 --- /dev/null +++ b/packages/e2e-tests/fixtures/apps/github/github-popup.js @@ -0,0 +1,92 @@ +const { BasePage } = require('../../base-page'); + +export class GithubPopup extends BasePage { + + /** + * @param {import('@playwright/test').Page} page + */ + static async handle (page) { + const popup = new GithubPopup(page); + return await popup.handleAuthFlow(); + } + + getPathname () { + const url = this.page.url() + try { + return new URL(url).pathname; + } catch (e) { + return new URL(`https://github.com/${url}`).pathname; + } + } + + async handleAuthFlow () { + if (this.getPathname() === '/login') { + await this.handleLogin(); + } + if (this.page.isClosed()) { return; } + if (this.getPathname() === '/login/oauth/authorize') { + await this.handleAuthorize(); + } + } + + async handleLogin () { + const loginInput = this.page.getByLabel('Username or email address'); + loginInput.click(); + await loginInput.fill(process.env.GITHUB_USERNAME); + const passwordInput = this.page.getByLabel('Password'); + passwordInput.click() + await passwordInput.fill(process.env.GITHUB_PASSWORD); + await this.page.getByRole('button', { name: 'Sign in' }).click(); + // await this.page.waitForTimeout(2000); + if (this.page.isClosed()) { + return + } + // await this.page.waitForLoadState('networkidle', 30000); + this.page.waitForEvent('load'); + if (this.page.isClosed()) { + return + } + await this.page.waitForURL(function (url) { + const u = new URL(url); + return ( + u.pathname === '/login/oauth/authorize' + ) && u.searchParams.get('client_id'); + }); + } + + async handleAuthorize () { + if (this.page.isClosed()) { return } + const authorizeButton = this.page.getByRole( + 'button', + { name: 'Authorize' } + ); + await this.page.waitForEvent('load'); + await authorizeButton.click(); + await this.page.waitForURL(function (url) { + const u = new URL(url); + return ( + u.pathname === '/login/oauth/authorize' + ) && ( + u.searchParams.get('client_id') === null + ); + }) + const passwordInput = this.page.getByLabel('Password'); + if (await passwordInput.isVisible()) { + await passwordInput.fill(process.env.GITHUB_PASSWORD); + const submitButton = this.page + .getByRole('button') + .filter({ hasText: /confirm|submit|enter|go|sign in/gmi }); + if (await submitButton.isVisible()) { + submitButton.waitFor(); + await expect(submitButton).toBeEnabled(); + await submitButton.click(); + } else { + throw { + page: this.page, + error: 'Could not find submit button for confirming user account' + }; + } + } + await this.page.waitForEvent('close') + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/authenticated-page.js b/packages/e2e-tests/fixtures/authenticated-page.js new file mode 100644 index 0000000000000000000000000000000000000000..d0d653be99c0cdf7ddf2dab1a06cc13ccfe3e5ab --- /dev/null +++ b/packages/e2e-tests/fixtures/authenticated-page.js @@ -0,0 +1,24 @@ +const path = require('node:path'); +const { expect } = require('@playwright/test'); +const { BasePage } = require('./base-page'); +const { LoginPage } = require('./login-page'); + +export class AuthenticatedPage extends BasePage { + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.profileMenuButton = this.page.getByTestId('profile-menu-button'); + this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' }); + this.userInterfaceDrawerItem = this.page.getByTestId( + 'user-interface-drawer-link' + ); + this.appBar = this.page.getByTestId('app-bar'); + this.drawerMenuButton = this.page.getByTestId('drawer-menu-button'); + this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link'); + this.typographyLogo = this.page.getByTestId('typography-logo'); + this.customLogo = this.page.getByTestId('custom-logo'); + } +} diff --git a/packages/e2e-tests/fixtures/base-page.js b/packages/e2e-tests/fixtures/base-page.js new file mode 100644 index 0000000000000000000000000000000000000000..1b05789989dbd06e53368a6f1db451c6152f4b90 --- /dev/null +++ b/packages/e2e-tests/fixtures/base-page.js @@ -0,0 +1,86 @@ +const path = require('node:path'); + +/** + * @typedef {( + * 'default' | 'success' | 'warning' | 'error' | 'info' + * )} SnackbarVariant - Snackbar variant types in notistack/v3, see https://notistack.com/api-reference + */ + +export class BasePage { + screenshotPath = '/'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + this.page = page; + this.snackbar = page.locator('*[data-test^="snackbar"]'); + this.pageTitle = this.page.getByTestId('page-title'); + } + + /** + * Finds the latest snackbar message and extracts relevant data + * @param {string | undefined} testId + * @returns {( + * null | { + * variant: SnackbarVariant, + * text: string, + * dataset: { [key: string]: string } + * } + * )} + */ + async getSnackbarData(testId) { + if (!testId) { + testId = 'snackbar'; + } + const snack = this.page.getByTestId(testId); + return { + variant: await snack.getAttribute('data-snackbar-variant'), + text: await snack.evaluate((node) => node.innerText), + dataset: await snack.evaluate((node) => { + function getChildren(n) { + return [n].concat( + ...Array.from(n.children).map((c) => getChildren(c)) + ); + } + const datasets = getChildren(node).map((n) => + Object.assign({}, n.dataset) + ); + return Object.assign({}, ...datasets); + }), + }; + } + + /** + * Closes all snackbars, should be replaced later + */ + async closeSnackbar() { + const snackbars = await this.snackbar.all(); + for (const snackbar of snackbars) { + await snackbar.click(); + } + for (const snackbar of snackbars) { + await snackbar.waitFor({ state: 'detached' }); + } + } + + async clickAway() { + await this.page.locator('body').click({ position: { x: 0, y: 0 } }); + } + + async screenshot(options = {}) { + const { path: plainPath, ...restOptions } = options; + + const computedPath = path.join( + 'output/screenshots', + this.screenshotPath, + plainPath + ); + + return await this.page.screenshot({ path: computedPath, ...restOptions }); + } + + async isMounted() { + await this.pageTitle.waitFor({ state: 'attached' }); + } +} diff --git a/packages/e2e-tests/fixtures/connections-page.js b/packages/e2e-tests/fixtures/connections-page.js new file mode 100644 index 0000000000000000000000000000000000000000..ad2c7fc8befe6e759d1caa9409b7ef954ed72056 --- /dev/null +++ b/packages/e2e-tests/fixtures/connections-page.js @@ -0,0 +1,10 @@ +const path = require('node:path'); +const { AuthenticatedPage } = require('./authenticated-page'); + +export class ConnectionsPage extends AuthenticatedPage { + screenshotPath = '/connections'; + + async clickAddConnectionButton() { + await this.page.getByTestId('add-connection-button').click(); + } +} diff --git a/packages/e2e-tests/fixtures/executions-page.js b/packages/e2e-tests/fixtures/executions-page.js new file mode 100644 index 0000000000000000000000000000000000000000..4a00782ade14363f8b89e0db7053cc083bce5de4 --- /dev/null +++ b/packages/e2e-tests/fixtures/executions-page.js @@ -0,0 +1,6 @@ +const path = require('node:path'); +const { AuthenticatedPage } = require('./authenticated-page'); + +export class ExecutionsPage extends AuthenticatedPage { + screenshotPath = '/executions'; +} diff --git a/packages/e2e-tests/fixtures/flow-editor-page.js b/packages/e2e-tests/fixtures/flow-editor-page.js new file mode 100644 index 0000000000000000000000000000000000000000..d03835259c0e6f29aa6e3b83bdc7277e05406507 --- /dev/null +++ b/packages/e2e-tests/fixtures/flow-editor-page.js @@ -0,0 +1,26 @@ +const path = require('node:path'); +const { AuthenticatedPage } = require('./authenticated-page'); + +export class FlowEditorPage extends AuthenticatedPage { + screenshotPath = '/flow-editor'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.appAutocomplete = this.page.getByTestId('choose-app-autocomplete'); + this.eventAutocomplete = this.page.getByTestId('choose-event-autocomplete'); + this.continueButton = this.page.getByTestId('flow-substep-continue-button'); + this.connectionAutocomplete = this.page.getByTestId( + 'choose-connection-autocomplete' + ); + this.testOuput = this.page.getByTestId('flow-test-substep-output'); + this.unpublishFlowButton = this.page.getByTestId('unpublish-flow-button'); + this.publishFlowButton = this.page.getByTestId('publish-flow-button'); + this.infoSnackbar = this.page.getByTestId('flow-cannot-edit-info-snackbar'); + this.trigger = this.page.getByLabel('Trigger on weekends?'); + this.stepCircularLoader = this.page.getByTestId('step-circular-loader'); + } +} diff --git a/packages/e2e-tests/fixtures/index.js b/packages/e2e-tests/fixtures/index.js new file mode 100644 index 0000000000000000000000000000000000000000..4396f374f82d4895edd9c2ede335507d8ad9369e --- /dev/null +++ b/packages/e2e-tests/fixtures/index.js @@ -0,0 +1,59 @@ +const { test, expect } = require('@playwright/test'); +const { ApplicationsPage } = require('./applications-page'); +const { ConnectionsPage } = require('./connections-page'); +const { ExecutionsPage } = require('./executions-page'); +const { FlowEditorPage } = require('./flow-editor-page'); +const { UserInterfacePage } = require('./user-interface-page'); +const { LoginPage } = require('./login-page'); +const { adminFixtures } = require('./admin'); + +exports.test = test.extend({ + page: async ({ page }, use) => { + const loginPage = new LoginPage(page); + await loginPage.login(); + + await expect(loginPage.loginButton).not.toBeVisible(); + await expect(page).toHaveURL('/flows'); + + await use(page); + }, + applicationsPage: async ({ page }, use) => { + await use(new ApplicationsPage(page)); + }, + connectionsPage: async ({ page }, use) => { + await use(new ConnectionsPage(page)); + }, + executionsPage: async ({ page }, use) => { + await use(new ExecutionsPage(page)); + }, + flowEditorPage: async ({ page }, use) => { + await use(new FlowEditorPage(page)); + }, + userInterfacePage: async ({ page }, use) => { + await use(new UserInterfacePage(page)); + }, + ...adminFixtures +}); + +exports.publicTest = test.extend({ + page: async ({ page }, use) => { + await use(page); + }, + loginPage: async ({ page }, use) => { + const loginPage = new LoginPage(page); + + await loginPage.open(); + + await use(loginPage); + }, +}); + +expect.extend({ + toBeClickableLink: async (locator) => { + await expect(locator).not.toHaveAttribute('aria-disabled', 'true'); + + return { pass: true }; + }, +}); + +exports.expect = expect; diff --git a/packages/e2e-tests/fixtures/login-page.js b/packages/e2e-tests/fixtures/login-page.js new file mode 100644 index 0000000000000000000000000000000000000000..a2e9128ade8e010ca6f14b45738051e28aa2495c --- /dev/null +++ b/packages/e2e-tests/fixtures/login-page.js @@ -0,0 +1,46 @@ +const { BasePage } = require('./base-page'); + +export class LoginPage extends BasePage { + path = '/login'; + static defaultEmail = process.env.LOGIN_EMAIL; + static defaultPassword = process.env.LOGIN_PASSWORD; + + static setDefaultLogin(email, password) { + this.defaultEmail = email; + this.defaultPassword = password; + } + + static resetDefaultLogin() { + this.defaultEmail = process.env.LOGIN_EMAIL; + this.defaultPassword = process.env.LOGIN_PASSWORD; + } + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.page = page; + this.emailTextField = this.page.getByTestId('email-text-field'); + this.passwordTextField = this.page.getByTestId('password-text-field'); + this.loginButton = this.page.getByTestId('login-button'); + this.pageTitle = this.page.getByTestId('login-form-title'); + } + + async open() { + return await this.page.goto(this.path); + } + + async login( + email = LoginPage.defaultEmail, + password = LoginPage.defaultPassword + ) { + await this.page.goto(this.path); + await this.emailTextField.waitFor({ state: 'visible' }); + await this.emailTextField.fill(email); + await this.passwordTextField.fill(password); + + await this.loginButton.click(); + } +} diff --git a/packages/e2e-tests/fixtures/user-interface-page.js b/packages/e2e-tests/fixtures/user-interface-page.js new file mode 100644 index 0000000000000000000000000000000000000000..119b92b146fa3a782d50213b36cca0c87059d542 --- /dev/null +++ b/packages/e2e-tests/fixtures/user-interface-page.js @@ -0,0 +1,53 @@ +const path = require('node:path'); +const { AuthenticatedPage } = require('./authenticated-page'); + +export class UserInterfacePage extends AuthenticatedPage { + screenshotPath = '/user-interface'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor(page) { + super(page); + + this.flowRowCardActionArea = this.page + .getByTestId('flow-row') + .first() + .getByTestId('card-action-area'); + this.updateButton = this.page.getByTestId('update-button'); + this.primaryMainColorInput = this.page + .getByTestId('primary-main-color-input') + .getByTestId('color-text-field'); + this.primaryDarkColorInput = this.page + .getByTestId('primary-dark-color-input') + .getByTestId('color-text-field'); + this.primaryLightColorInput = this.page + .getByTestId('primary-light-color-input') + .getByTestId('color-text-field'); + this.logoSvgCodeInput = this.page.getByTestId('logo-svg-data-text-field'); + this.primaryMainColorButton = this.page + .getByTestId('primary-main-color-input') + .getByTestId('color-button'); + this.primaryDarkColorButton = this.page + .getByTestId('primary-dark-color-input') + .getByTestId('color-button'); + this.primaryLightColorButton = this.page + .getByTestId('primary-light-color-input') + .getByTestId('color-button'); + } + + hexToRgb(hexColor) { + hexColor = hexColor.replace('#', ''); + const r = parseInt(hexColor.substring(0, 2), 16); + const g = parseInt(hexColor.substring(2, 4), 16); + const b = parseInt(hexColor.substring(4, 6), 16); + + return `rgb(${r}, ${g}, ${b})`; + } + + encodeSVG(svgCode) { + const encoded = encodeURIComponent(svgCode); + + return `data:image/svg+xml;utf8,${encoded}`; + } +} diff --git a/packages/e2e-tests/license-server-with-mock.js b/packages/e2e-tests/license-server-with-mock.js new file mode 100644 index 0000000000000000000000000000000000000000..bf16794a2c1813f862b1e991b8de13e08b81399a --- /dev/null +++ b/packages/e2e-tests/license-server-with-mock.js @@ -0,0 +1,28 @@ +const fs = require('node:fs'); +const https = require('node:https'); +const path = require('node:path'); +const { run, send } = require('micro'); + +const options = { + key: fs.readFileSync(path.join(__dirname, './automatisch.io+4-key.pem')), + cert: fs.readFileSync(path.join(__dirname, './automatisch.io+4.pem')), +}; + +const microHttps = (fn) => + https.createServer(options, (req, res) => run(req, res, fn)); + +const server = microHttps(async (req, res) => { + const data = { + id: '7f22d7dd-1fda-4482-83fa-f35bf974a21f', + name: 'Mocked license', + expireAt: '2030-08-09T10:56:54.144Z', + }; + + send(res, 200, data); +}); + +server + .once('listening', () => { + console.log('The mock server is up.'); + }) + .listen(443); diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json new file mode 100644 index 0000000000000000000000000000000000000000..48d395e0b424f0cb390dba1cee954af2227f0bbf --- /dev/null +++ b/packages/e2e-tests/package.json @@ -0,0 +1,38 @@ +{ + "name": "@automatisch/e2e-tests", + "version": "0.10.0", + "license": "See LICENSE file", + "private": true, + "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", + "scripts": { + "start-mock-license-server": "node ./license-server-with-mock.js", + "test": "playwright test", + "test:fast": "yarn test -j 90% --quiet --reporter null --ignore-snapshots -x" + }, + "contributors": [ + { + "name": "automatisch contributors", + "url": "https://github.com/automatisch/automatisch/graphs/contributors" + } + ], + "homepage": "https://github.com/automatisch/automatisch#readme", + "repository": { + "type": "git", + "url": "git+https://github.com/automatisch/automatisch.git" + }, + "bugs": { + "url": "https://github.com/automatisch/automatisch/issues" + }, + "devDependencies": { + "@faker-js/faker": "^8.2.0", + "@playwright/test": "^1.36.2" + }, + "dependencies": { + "dotenv": "^16.3.1", + "eslint": "^8.13.0", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-prettier": "^4.0.0", + "micro": "^10.0.1", + "prettier": "^2.5.1" + } +} diff --git a/packages/e2e-tests/playwright.config.js b/packages/e2e-tests/playwright.config.js new file mode 100644 index 0000000000000000000000000000000000000000..16f63eb63414bd58b03b2f3b9b3ed3270de4d6dd --- /dev/null +++ b/packages/e2e-tests/playwright.config.js @@ -0,0 +1,88 @@ +// @ts-check +const { defineConfig, devices } = require('@playwright/test'); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +require('dotenv').config(); + +/** + * @see https://playwright.dev/docs/test-configuration + */ +module.exports = defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Timeout threshold for each test */ + timeout: 30000, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI + ? [['html', { open: 'never' }], ['github']] + : [['html', { open: 'never' }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.BASE_URL || 'http://localhost:3001', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + testIdAttribute: 'data-test', + viewport: { width: 1280, height: 720 }, + }, + + expect: { + /* Timeout threshold for each assertion */ + timeout: 10000, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/packages/e2e-tests/tests/admin/manage-roles.spec.js b/packages/e2e-tests/tests/admin/manage-roles.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c823645be48176af4281b825962da642c5219baf --- /dev/null +++ b/packages/e2e-tests/tests/admin/manage-roles.spec.js @@ -0,0 +1,469 @@ +const { test, expect } = require('../../fixtures/index'); +const { LoginPage } = require('../../fixtures/login-page'); + +test.describe('Role management page', () => { + test('Admin role is not deletable', async ({ adminRolesPage }) => { + await adminRolesPage.navigateTo(); + const adminRow = await adminRolesPage.getRoleRowByName('Admin'); + await expect(adminRow).toHaveCount(1); + const data = await adminRolesPage.getRowData(adminRow); + await expect(data.role).toBe('Admin'); + await expect(data.canEdit).toBe(true); + await expect(data.canDelete).toBe(false); + }); + + test('Can create, edit, and delete a role', async ({ + adminCreateRolePage, + adminEditRolePage, + adminRolesPage, + page, + }) => { + await test.step('Create a new role', async () => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.isMounted(); + await adminCreateRolePage.nameInput.fill('Create Edit Test'); + await adminCreateRolePage.descriptionInput.fill('Test description'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + + let roleRow = await test.step( + 'Make sure role data is correct', + async () => { + const roleRow = await adminRolesPage.getRoleRowByName( + 'Create Edit Test' + ); + await expect(roleRow).toHaveCount(1); + const roleData = await adminRolesPage.getRowData(roleRow); + await expect(roleData.role).toBe('Create Edit Test'); + await expect(roleData.description).toBe('Test description'); + await expect(roleData.canEdit).toBe(true); + await expect(roleData.canDelete).toBe(true); + return roleRow; + } + ); + + await test.step('Edit the role', async () => { + await adminRolesPage.clickEditRole(roleRow); + await adminEditRolePage.isMounted(); + await adminEditRolePage.nameInput.fill('Create Update Test'); + await adminEditRolePage.descriptionInput.fill('Update test description'); + await adminEditRolePage.updateButton.click(); + await adminEditRolePage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminEditRolePage.getSnackbarData( + 'snackbar-edit-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminEditRolePage.closeSnackbar(); + }); + + roleRow = await test.step( + 'Make sure changes reflected on roles page', + async () => { + await adminRolesPage.isMounted(); + const roleRow = await adminRolesPage.getRoleRowByName( + 'Create Update Test' + ); + await expect(roleRow).toHaveCount(1); + const roleData = await adminRolesPage.getRowData(roleRow); + await expect(roleData.role).toBe('Create Update Test'); + await expect(roleData.description).toBe('Update test description'); + await expect(roleData.canEdit).toBe(true); + await expect(roleData.canDelete).toBe(true); + return roleRow; + } + ); + + await test.step('Delete the role', async () => { + await adminRolesPage.clickDeleteRole(roleRow); + const deleteModal = adminRolesPage.deleteRoleModal; + await deleteModal.modal.waitFor({ + state: 'attached', + }); + await deleteModal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + await deleteModal.modal.waitFor({ + state: 'detached', + }); + await expect(roleRow).toHaveCount(0); + }); + }); + + // This test breaks right now + test.skip('Make sure create/edit role page is scrollable', async ({ + adminRolesPage, + adminEditRolePage, + adminCreateRolePage, + page, + }) => { + const initViewportSize = page.viewportSize; + await page.setViewportSize({ + width: 800, + height: 400, + }); + + await test.step('Ensure create role page is scrollable', async () => { + await adminRolesPage.navigateTo(true); + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.isMounted(); + + const initScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await page.mouse.move(400, 100); + await page.mouse.click(400, 100); + await page.mouse.wheel(200, 0); + const updatedScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await expect(initScrollTop).not.toBe(updatedScrollTop); + }); + + await test.step('Ensure edit role page is scrollable', async () => { + await adminRolesPage.navigateTo(true); + const adminRow = await adminRolesPage.getRoleRowByName('Admin'); + await adminRolesPage.clickEditRole(adminRow); + await adminEditRolePage.isMounted(); + + const initScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await page.mouse.move(400, 100); + await page.mouse.wheel(200, 0); + const updatedScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await expect(initScrollTop).not.toBe(updatedScrollTop); + }); + + await test.step('Reset viewport', async () => { + await page.setViewportSize(initViewportSize); + }); + }); + + test('Cannot delete a role with a user attached to it', async ({ + adminCreateRolePage, + adminRolesPage, + adminUsersPage, + adminCreateUserPage, + adminEditUserPage, + page, + }) => { + await adminRolesPage.navigateTo(); + await test.step('Create a new role', async () => { + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.isMounted(); + await adminCreateRolePage.nameInput.fill('Delete Role'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + await test.step( + 'Create a new user with the "Delete Role" role', + async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill('User Role Test'); + await adminCreateUserPage.emailInput.fill( + 'user-role-test@automatisch.io' + ); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Delete Role', exact: true }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminUsersPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + await test.step( + 'Try to delete "Delete Role" role when new user has it', + async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminRolesPage.getSnackbarData('snackbar-error'); + await expect(snackbar.variant).toBe('error'); + await adminRolesPage.closeSnackbar(); + await modal.close(); + } + ); + await test.step('Change the role the user has', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.usersLoader.waitFor({ + state: 'detached', + }); + const row = await adminUsersPage.findUserPageWithEmail( + 'user-role-test@automatisch.io' + ); + await adminUsersPage.clickEditUser(row); + await adminEditUserPage.roleInput.click(); + await adminEditUserPage.page + .getByRole('option', { name: 'Admin' }) + .click(); + await adminEditUserPage.updateButton.click(); + await adminEditUserPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminEditUserPage.getSnackbarData( + 'snackbar-edit-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminEditUserPage.closeSnackbar(); + }); + await test.step('Delete the original role', async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await expect(modal.modal).toBeVisible(); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + }); + }); + + test('Deleting a role after deleting a user with that role', async ({ + adminCreateRolePage, + adminRolesPage, + adminUsersPage, + adminCreateUserPage, + page, + }) => { + await adminRolesPage.navigateTo(); + await test.step('Create a new role', async () => { + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.isMounted(); + await adminCreateRolePage.nameInput.fill('Cannot Delete Role'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + await test.step('Create a new user with this role', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.isMounted(); + await adminCreateUserPage.fullNameInput.fill('User Delete Role Test'); + await adminCreateUserPage.emailInput.fill( + 'user-delete-role-test@automatisch.io' + ); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Cannot Delete Role' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateUserPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateUserPage.closeSnackbar(); + }); + await test.step('Delete this user', async () => { + await adminUsersPage.navigateTo(); + const row = await adminUsersPage.findUserPageWithEmail( + 'user-delete-role-test@automatisch.io' + ); + // await test.waitForTimeout(10000); + const modal = await adminUsersPage.clickDeleteUser(row); + await modal.deleteButton.click(); + await adminUsersPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + }); + await test.step('Try deleting this role', async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Cannot Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + /* + * TODO: await snackbar - make assertions based on product + * decisions + const snackbar = await adminRolesPage.getSnackbarData(); + await expect(snackbar.variant).toBe('...'); + */ + await adminRolesPage.closeSnackbar(); + }); + }); +}); + +test('Accessibility of role management page', async ({ + page, + adminUsersPage, + adminCreateUserPage, + adminEditUserPage, + adminRolesPage, + adminCreateRolePage, +}) => { + test.slow(); + await test.step('Create the basic test role', async () => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.isMounted(); + await adminCreateRolePage.nameInput.fill('Basic Test'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + + await test.step('Create a new user with the basic role', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.isMounted(); + await adminCreateUserPage.fullNameInput.fill('Role Test'); + await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io'); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page + .getByRole('option', { name: 'Basic Test' }) + .click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminCreateUserPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateUserPage.closeSnackbar(); + }); + + await test.step('Logout and login to the basic role user', async () => { + await page.getByTestId('profile-menu-button').click(); + await page.getByTestId('logout-item').click(); + // await page.reload({ waitUntil: 'networkidle' }); + const loginPage = new LoginPage(page); + // await loginPage.isMounted(); + await loginPage.login('basic-role-test@automatisch.io', 'sample'); + await expect(loginPage.loginButton).not.toBeVisible(); + await expect(page).toHaveURL('/flows'); + }); + + await test.step( + 'Navigate to the admin settings page and make sure it is blank', + async () => { + const pageUrl = new URL(page.url()); + const url = `${pageUrl.origin}/admin-settings/users`; + await page.goto(url); + await page.waitForTimeout(750); + const isUnmounted = await page.evaluate(() => { + const root = document.querySelector('#root'); + if (root) { + return root.children.length === 0; + } + return false; + }); + await expect(isUnmounted).toBe(true); + } + ); + + await test.step('Log back into the admin account', async () => { + await page.goto('/'); + await page.getByTestId('profile-menu-button').click(); + await page.getByTestId('logout-item').click(); + const loginPage = new LoginPage(page); + await loginPage.isMounted(); + await loginPage.login(); + }); + + await test.step('Move the user off the role', async () => { + await adminUsersPage.navigateTo(); + const row = await adminUsersPage.findUserPageWithEmail( + 'basic-role-test@automatisch.io' + ); + await adminUsersPage.clickEditUser(row); + await adminEditUserPage.isMounted(); + await adminEditUserPage.roleInput.click(); + await adminEditUserPage.page.getByRole('option', { name: 'Admin' }).click(); + await adminEditUserPage.updateButton.click(); + await adminEditUserPage.snackbar.waitFor({ + state: 'attached', + }); + await adminEditUserPage.closeSnackbar(); + }); + + await test.step('Delete the role', async () => { + await adminRolesPage.navigateTo(); + const roleRow = await adminRolesPage.getRoleRowByName('Basic Test'); + await adminRolesPage.clickDeleteRole(roleRow); + const deleteModal = adminRolesPage.deleteRoleModal; + await deleteModal.modal.waitFor({ + state: 'attached', + }); + await deleteModal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached', + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + await deleteModal.modal.waitFor({ + state: 'detached', + }); + await expect(roleRow).toHaveCount(0); + }); +}); diff --git a/packages/e2e-tests/tests/admin/manage-users.spec.js b/packages/e2e-tests/tests/admin/manage-users.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..7e6dab7d5abd97bad74e4660dc0a768a09c40774 --- /dev/null +++ b/packages/e2e-tests/tests/admin/manage-users.spec.js @@ -0,0 +1,288 @@ +const { test, expect } = require('../../fixtures/index'); + +/** + * NOTE: Make sure to delete all users generated between test runs, + * otherwise tests will fail since users are only *soft*-deleted + */ +test.describe('User management page', () => { + + test.beforeEach(async ({ adminUsersPage }) => { + await adminUsersPage.navigateTo(); + await adminUsersPage.closeSnackbar(); + }); + + test( + 'User creation and deletion process', + async ({ adminCreateUserPage, adminEditUserPage, adminUsersPage }) => { + adminCreateUserPage.seed(9000); + const user = adminCreateUserPage.generateUser(); + await adminUsersPage.usersLoader.waitFor({ + state: 'detached' /* Note: state: 'visible' introduces flakiness + because visibility: hidden is used as part of the state transition in + notistack, see + https://github.com/iamhosseindhv/notistack/blob/122f47057eb7ce5a1abfe923316cf8475303e99a/src/transitions/Collapse/Collapse.tsx#L110 + */ + }); + await test.step( + 'Create a user', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user.fullName); + await adminCreateUserPage.emailInput.fill(user.email); + await adminCreateUserPage.passwordInput.fill(user.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + await test.step( + 'Check the user exists with the expected properties', + async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + const userRow = await adminUsersPage.getUserRowByEmail(user.email); + const data = await adminUsersPage.getRowData(userRow); + await expect(data.email).toBe(user.email); + await expect(data.fullName).toBe(user.fullName); + await expect(data.role).toBe('Admin'); + } + ); + await test.step( + 'Edit user info and make sure the edit works correctly', + async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + + let userRow = await adminUsersPage.getUserRowByEmail(user.email); + await adminUsersPage.clickEditUser(userRow); + await adminEditUserPage.waitForLoad(user.fullName); + const newUserInfo = adminEditUserPage.generateUser(); + await adminEditUserPage.fullNameInput.fill(newUserInfo.fullName); + await adminEditUserPage.updateButton.click(); + + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-edit-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + + await adminUsersPage.findUserPageWithEmail(user.email); + userRow = await adminUsersPage.getUserRowByEmail(user.email); + const rowData = await adminUsersPage.getRowData(userRow); + await expect(rowData.fullName).toBe(newUserInfo.fullName); + } + ); + await test.step( + 'Delete user and check the page confirms this deletion', + async () => { + await adminUsersPage.findUserPageWithEmail(user.email); + const userRow = await adminUsersPage.getUserRowByEmail(user.email); + await adminUsersPage.clickDeleteUser(userRow); + const modal = adminUsersPage.deleteUserModal; + await modal.deleteButton.click(); + + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + await expect(userRow).not.toBeVisible(false); + } + ); + }); + + test( + 'Creating a user which has been deleted', + async ({ adminCreateUserPage, adminUsersPage }) => { + adminCreateUserPage.seed(9100); + const testUser = adminCreateUserPage.generateUser(); + + await test.step( + 'Create the test user', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.passwordInput.fill(testUser.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + + await test.step( + 'Delete the created user', + async () => { + await adminUsersPage.findUserPageWithEmail(testUser.email); + const userRow = await adminUsersPage.getUserRowByEmail(testUser.email); + await adminUsersPage.clickDeleteUser(userRow); + const modal = adminUsersPage.deleteUserModal; + await modal.deleteButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' + ); + await expect(snackbar).not.toBeNull(); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + await expect(userRow).not.toBeVisible(false); + } + ); + + await test.step( + 'Create the user again', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.passwordInput.fill(testUser.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + await adminUsersPage.snackbar.waitFor({ + state: 'attached' + }); + /* + TODO: assert snackbar behavior after deciding what should + happen here, i.e. if this should create a new user, stay the + same, un-delete the user, or something else + */ + // await adminUsersPage.getSnackbarData('snackbar-error'); + await adminUsersPage.closeSnackbar(); + } + ); + } + ); + + test( + 'Creating a user which already exists', + async ({ adminCreateUserPage, adminUsersPage, page }) => { + adminCreateUserPage.seed(9200); + const testUser = adminCreateUserPage.generateUser(); + + await test.step( + 'Create the test user', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.passwordInput.fill(testUser.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + + await test.step( + 'Create the user again', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(testUser.fullName); + await adminCreateUserPage.emailInput.fill(testUser.email); + await adminCreateUserPage.passwordInput.fill(testUser.password); + const createUserPageUrl = page.url(); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + + await expect(page.url()).toBe(createUserPageUrl); + const snackbar = await adminUsersPage.getSnackbarData('snackbar-error'); + await expect(snackbar.variant).toBe('error'); + await adminUsersPage.closeSnackbar(); + } + ); + } + ); + + test( + 'Editing a user to have the same email as another user should not be allowed', + async ({ + adminCreateUserPage, adminEditUserPage, adminUsersPage, page + }) => { + adminCreateUserPage.seed(9300); + const user1 = adminCreateUserPage.generateUser(); + const user2 = adminCreateUserPage.generateUser(); + await test.step( + 'Create the first user', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user1.fullName); + await adminCreateUserPage.emailInput.fill(user1.email); + await adminCreateUserPage.passwordInput.fill(user1.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + + await test.step( + 'Create the second user', + async () => { + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill(user2.fullName); + await adminCreateUserPage.emailInput.fill(user2.email); + await adminCreateUserPage.passwordInput.fill(user2.password); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminCreateUserPage.createButton.click(); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + + await test.step( + 'Try editing the second user to have the email of the first user', + async () => { + await adminUsersPage.findUserPageWithEmail(user2.email); + let userRow = await adminUsersPage.getUserRowByEmail(user2.email); + await adminUsersPage.clickEditUser(userRow); + await adminEditUserPage.waitForLoad(user2.fullName); + await adminEditUserPage.emailInput.fill(user1.email); + const editPageUrl = page.url(); + await adminEditUserPage.updateButton.click(); + + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-error' + ); + await expect(snackbar.variant).toBe('error'); + await adminUsersPage.closeSnackbar(); + await expect(page.url()).toBe(editPageUrl); + } + ); + } + ); +}); \ No newline at end of file diff --git a/packages/e2e-tests/tests/admin/role-conditions.spec.js b/packages/e2e-tests/tests/admin/role-conditions.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6f69ad585fefbd03afea36028092c19d779f1c7e --- /dev/null +++ b/packages/e2e-tests/tests/admin/role-conditions.spec.js @@ -0,0 +1,69 @@ +const { test, expect } = require('../../fixtures/index'); + +test( + 'Role permissions conform with role conditions ', + async({ adminRolesPage, adminCreateRolePage }) => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + + /* + example config: { + action: 'read', + subject: 'connection', + row: page.getByTestId('connection-permission-row'), + locator: row.getByTestId('read-checkbox') + } + */ + const permissionConfigs = + await adminCreateRolePage.getPermissionConfigs(); + + await test.step( + 'Iterate over each permission config and make sure role conditions conform', + async () => { + for (let config of permissionConfigs) { + await config.locator.click(); + await adminCreateRolePage.clickPermissionSettings(config.row); + const modal = adminCreateRolePage.getRoleConditionsModal( + config.subject + ); + await expect(modal.modal).toBeVisible(); + const conditions = await modal.getAvailableConditions(); + for (let conditionAction of Object.keys(conditions)) { + if (conditionAction === config.action) { + await expect(conditions[conditionAction]).not.toBeDisabled(); + } else { + await expect(conditions[conditionAction]).toBeDisabled(); + } + } + await modal.close(); + await config.locator.click(); + } + } + ); + } +); + +test( + 'Default role permissions conforms with role conditions', + async({ adminRolesPage, adminCreateRolePage }) => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + + const subjects = ['Connection', 'Execution', 'Flow']; + for (let subject of subjects) { + const row = adminCreateRolePage.getSubjectRow(subject) + const modal = adminCreateRolePage.getRoleConditionsModal(subject); + await adminCreateRolePage.clickPermissionSettings(row); + await expect(modal.modal).toBeVisible(); + const availableConditions = await modal.getAvailableConditions(); + const conditions = ['create', 'read', 'update', 'delete', 'publish']; + for (let condition of conditions) { + if (availableConditions[condition]) { + await expect(availableConditions[condition]).toBeDisabled(); + } + } + await modal.close(); + } + + } +); \ No newline at end of file diff --git a/packages/e2e-tests/tests/app-integrations/github.spec.js b/packages/e2e-tests/tests/app-integrations/github.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f2348862da4a7e6acf2e2bf767da53b20bcdd36c --- /dev/null +++ b/packages/e2e-tests/tests/app-integrations/github.spec.js @@ -0,0 +1,63 @@ +const { test, expect } = require('../../fixtures'); + +test('Github OAuth integration', async ({ page, applicationsPage }) => { + const githubConnectionPage = await test.step( + 'Navigate to github connections modal', + async () => { + await applicationsPage.drawerLink.click(); + if (page.url() !== '/apps') { + await page.waitForURL('/apps'); + } + const connectionModal = await applicationsPage.openAddConnectionModal(); + await expect(connectionModal.modal).toBeVisible(); + return await connectionModal.selectLink('github'); + } + ); + + const connectionModal = await test.step( + 'Ensure the github connection modal is visible', + async () => { + const connectionModal = githubConnectionPage.addConnectionModal; + await expect(connectionModal.modal).toBeVisible(); + return connectionModal; + } + ); + + const githubPopup = await test.step( + 'Input data into the add connection form and submit', + async () => { + await connectionModal.clientIdInput.fill(process.env.GITHUB_CLIENT_ID); + await connectionModal.clientIdSecretInput.fill( + process.env.GITHUB_CLIENT_SECRET + ); + return await connectionModal.submit(); + } + ); + + await test.step('Ensure github popup is not a 404', async () => { + // await expect(githubPopup).toBeVisible(); + const title = await githubPopup.title(); + await expect(title).not.toMatch(/^Page not found/); + }); + + /* Skip these in CI + await test.step( + 'Handle github popup authentication flow', + async () => { + await connectionModal.handlePopup(githubPopup); + } + ); + + await test.step( + 'Ensure the new connection is added to the connections list', + async () => { + await page.locator('body').click({ position: { x: 0, y: 0 } }); + // TODO + } + ); + */ +}); + +test.afterAll(async () => { + // TODO - Remove connections from github connections page +}); \ No newline at end of file diff --git a/packages/e2e-tests/tests/apps/list-apps.spec.js b/packages/e2e-tests/tests/apps/list-apps.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..16530008489c27cda153920dd341e17ad8de6a9d --- /dev/null +++ b/packages/e2e-tests/tests/apps/list-apps.spec.js @@ -0,0 +1,92 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +test.describe('Apps page', () => { + test.beforeEach(async ({ applicationsPage }) => { + await applicationsPage.drawerLink.click(); + }); + + // no connected application exists in an empty account + test.skip('displays no applications', async ({ applicationsPage }) => { + await applicationsPage.page.getByTestId('apps-loader').waitFor({ + state: 'detached', + }); + await expect(applicationsPage.page.getByTestId('app-row')).not.toHaveCount( + 0 + ); + + await applicationsPage.screenshot({ + path: 'Applications.png', + }); + }); + + test.describe('can add connection', () => { + test.beforeEach(async ({ applicationsPage }) => { + await expect(applicationsPage.addConnectionButton).toBeClickableLink(); + await applicationsPage.addConnectionButton.click(); + await applicationsPage.page + .getByTestId('search-for-app-loader') + .waitFor({ state: 'detached' }); + }); + + test('lists applications', async ({ applicationsPage }) => { + const appListItemCount = await applicationsPage.page + .getByTestId('app-list-item') + .count(); + expect(appListItemCount).toBeGreaterThan(10); + + await applicationsPage.clickAway(); + }); + + test('searches an application', async ({ applicationsPage }) => { + await applicationsPage.page + .getByTestId('search-for-app-text-field') + .fill('DeepL'); + await applicationsPage.page + .getByTestId('search-for-app-loader') + .waitFor({ state: 'detached' }); + + await expect( + applicationsPage.page.getByTestId('app-list-item') + ).toHaveCount(1); + + await applicationsPage.clickAway(); + }); + + test('goes to app page to create a connection', async ({ + applicationsPage, + }) => { + // loading app, app config, app auth clients take time + test.setTimeout(60000); + + await applicationsPage.page.getByTestId('app-list-item').first().click(); + await expect(applicationsPage.page).toHaveURL( + '/app/airtable/connections/add?shared=false' + ); + await expect( + applicationsPage.page.getByTestId('add-app-connection-dialog') + ).toBeVisible(); + + await applicationsPage.clickAway(); + }); + + test('closes the dialog on backdrop click', async ({ + applicationsPage, + }) => { + await applicationsPage.page.getByTestId('app-list-item').first().click(); + await expect(applicationsPage.page).toHaveURL( + '/app/airtable/connections/add?shared=false' + ); + await expect( + applicationsPage.page.getByTestId('add-app-connection-dialog') + ).toBeVisible(); + await applicationsPage.clickAway(); + await expect(applicationsPage.page).toHaveURL( + '/app/airtable/connections' + ); + await expect( + applicationsPage.page.getByTestId('add-app-connection-dialog') + ).toBeHidden(); + }); + }); +}); diff --git a/packages/e2e-tests/tests/authentication/login.spec.js b/packages/e2e-tests/tests/authentication/login.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f43c0a86038c131f988df3e11be81a3716fe587b --- /dev/null +++ b/packages/e2e-tests/tests/authentication/login.spec.js @@ -0,0 +1,22 @@ +// @ts-check +const { publicTest, test, expect } = require('../../fixtures/index'); + +publicTest.describe('Login page', () => { + publicTest('shows login form', async ({ loginPage }) => { + await loginPage.emailTextField.waitFor({ state: 'attached' }); + await loginPage.passwordTextField.waitFor({ state: 'attached' }); + await loginPage.loginButton.waitFor({ state: 'attached' }); + }); + + publicTest('lets user login', async ({ loginPage }) => { + await loginPage.login(); + + await expect(loginPage.page).toHaveURL('/flows'); + }); + + publicTest(`doesn't let un-existing user login`, async ({ loginPage }) => { + await loginPage.login('nonexisting@automatisch.io', 'sample'); + + await expect(loginPage.page).toHaveURL('/login'); + }); +}); diff --git a/packages/e2e-tests/tests/connections/create-connection.spec.js b/packages/e2e-tests/tests/connections/create-connection.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..f9f4669ce7549174afbfa1981cf5817f93b234fe --- /dev/null +++ b/packages/e2e-tests/tests/connections/create-connection.spec.js @@ -0,0 +1,49 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +test.describe('Connections page', () => { + test.beforeEach(async ({ page, connectionsPage }) => { + await page.getByTestId('apps-page-drawer-link').click(); + await page.goto('/app/ntfy/connections'); + }); + + test('shows connections if any', async ({ page, connectionsPage }) => { + await page.getByTestId('apps-loader').waitFor({ + state: 'detached', + }); + + await connectionsPage.screenshot({ + path: 'Connections.png', + }); + }); + + test.describe('can add connection', () => { + test('has a button to open add connection dialog', async ({ page }) => { + await expect(page.getByTestId('add-connection-button')).toBeClickableLink(); + }); + + test('add connection button takes user to add connection page', async ({ + page, + connectionsPage, + }) => { + await connectionsPage.clickAddConnectionButton(); + await expect(page).toHaveURL('/app/ntfy/connections/add?shared=false'); + }); + + test('shows add connection dialog to create a new connection', async ({ + page, + connectionsPage, + }) => { + await connectionsPage.clickAddConnectionButton(); + await expect(page).toHaveURL('/app/ntfy/connections/add?shared=false'); + await page.getByTestId('create-connection-button').click(); + await expect( + page.getByTestId('create-connection-button') + ).not.toBeVisible(); + + await connectionsPage.screenshot({ + path: 'Ntfy connections after creating a connection.png', + }); + }); + }); +}); diff --git a/packages/e2e-tests/tests/executions/display-execution.spec.js b/packages/e2e-tests/tests/executions/display-execution.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..4b8d42ba5418a6c4659297d6b915b18700f768ec --- /dev/null +++ b/packages/e2e-tests/tests/executions/display-execution.spec.js @@ -0,0 +1,38 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +// no execution data exists in an empty account +test.describe.skip('Executions page', () => { + test.beforeEach(async ({ page, executionsPage }) => { + await page.getByTestId('executions-page-drawer-link').click(); + await page.getByTestId('execution-row').first().click(); + + await expect(page).toHaveURL(/\/executions\//); + }); + + test('displays data in by default', async ({ page, executionsPage }) => { + await expect(page.getByTestId('execution-step').last()).toBeVisible(); + await expect(page.getByTestId('execution-step')).toHaveCount(2); + + await executionsPage.screenshot({ + path: 'Execution - data in.png', + }); + }); + + test('displays data out', async ({ page, executionsPage }) => { + const executionStepCount = await page.getByTestId('execution-step').count(); + for (let i = 0; i < executionStepCount; i++) { + await page.getByTestId('data-out-tab').nth(i).click(); + await expect(page.getByTestId('data-out-panel').nth(i)).toBeVisible(); + + await executionsPage.screenshot({ + path: `Execution - data out - ${i}.png`, + animations: 'disabled', + }); + } + }); + + test('does not display error', async ({ page }) => { + await expect(page.getByTestId('error-tab')).toBeHidden(); + }); +}); diff --git a/packages/e2e-tests/tests/executions/list-executions.spec.js b/packages/e2e-tests/tests/executions/list-executions.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..b198d3c886dc78c9fdd3342b8db0fdda74f72578 --- /dev/null +++ b/packages/e2e-tests/tests/executions/list-executions.spec.js @@ -0,0 +1,18 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +test.describe('Executions page', () => { + test.beforeEach(async ({ page, executionsPage }) => { + await page.getByTestId('executions-page-drawer-link').click(); + }); + + // no executions exist in an empty account + test.skip('displays executions', async ({ page, executionsPage }) => { + await page.getByTestId('executions-loader').waitFor({ + state: 'detached', + }); + await expect(page.getByTestId('execution-row').first()).toBeVisible(); + + await executionsPage.screenshot({ path: 'Executions.png' }); + }); +}); diff --git a/packages/e2e-tests/tests/flow-editor/create-flow.spec.js b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..5486a4cc73bb243409e93662db10e5ef783c1fad --- /dev/null +++ b/packages/e2e-tests/tests/flow-editor/create-flow.spec.js @@ -0,0 +1,206 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +test('Ensure creating a new flow works', async ({ page }) => { + await page.getByTestId('create-flow-button').click(); + await expect(page).toHaveURL(/\/editor\/create/); + await expect(page).toHaveURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); +}) + +test( + 'Create a new flow with a Scheduler step then an Ntfy step', + async ({ flowEditorPage, page }) => { + await test.step('create flow', async () => { + await test.step('navigate to new flow page', async () => { + await page.getByTestId('create-flow-button').click(); + await page.waitForURL( + /\/editor\/[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}/ + ); + }); + + await test.step('has two steps by default', async () => { + await expect(page.getByTestId('flow-step')).toHaveCount(2); + }); + }); + + await test.step('setup Scheduler trigger', async () => { + await test.step('choose app and event substep', async () => { + await test.step('choose application', async () => { + await flowEditorPage.appAutocomplete.click(); + await page + .getByRole('option', { name: 'Scheduler' }) + .click(); + }); + + await test.step('choose and event', async () => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await page + .getByRole('option', { name: 'Every hour' }) + .click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); + await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('set up a trigger', async () => { + await test.step('choose "yes" in "trigger on weekends?"', async () => { + await expect(flowEditorPage.trigger).toBeVisible(); + await flowEditorPage.trigger.click(); + await page.getByRole('option', { name: 'Yes' }).click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.trigger).not.toBeVisible(); + }); + }); + + await test.step('test trigger', async () => { + await test.step('show sample output', async () => { + await expect(flowEditorPage.testOuput).not.toBeVisible(); + await flowEditorPage.continueButton.click(); + await expect(flowEditorPage.testOuput).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Scheduler trigger test output.png', + }); + await flowEditorPage.continueButton.click(); + }); + }); + }); + + await test.step('arrange Ntfy action', async () => { + await test.step('choose app and event substep', async () => { + await test.step('choose application', async () => { + await flowEditorPage.appAutocomplete.click(); + await page.getByRole('option', { name: 'Ntfy' }).click(); + }); + + await test.step('choose an event', async () => { + await expect(flowEditorPage.eventAutocomplete).toBeVisible(); + await flowEditorPage.eventAutocomplete.click(); + await page + .getByRole('option', { name: 'Send message' }) + .click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.appAutocomplete).not.toBeVisible(); + await expect(flowEditorPage.eventAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('choose connection substep', async () => { + await test.step('choose connection list item', async () => { + await flowEditorPage.connectionAutocomplete.click(); + await page.getByRole('option').first().click(); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('set up action substep', async () => { + await test.step('fill topic and message body', async () => { + await page + .getByTestId('parameters.topic-power-input') + .locator('[contenteditable]') + .fill('Topic'); + await page + .getByTestId('parameters.message-power-input') + .locator('[contenteditable]') + .fill('Message body'); + }); + + await test.step('continue to next step', async () => { + await flowEditorPage.continueButton.click(); + }); + + await test.step('collapses the substep', async () => { + await expect(flowEditorPage.connectionAutocomplete).not.toBeVisible(); + }); + }); + + await test.step('test trigger substep', async () => { + await test.step('show sample output', async () => { + await expect(flowEditorPage.testOuput).not.toBeVisible(); + await page + .getByTestId('flow-substep-continue-button') + .first() + .click(); + await expect(flowEditorPage.testOuput).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Ntfy action test output.png', + }); + await flowEditorPage.continueButton.click(); + }); + }); + }); + + await test.step('publish and unpublish', async () => { + await test.step('publish flow', async () => { + await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + await expect(flowEditorPage.publishFlowButton).toBeVisible(); + await flowEditorPage.publishFlowButton.click(); + await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + }); + + await test.step('shows read-only sticky snackbar', async () => { + await expect(flowEditorPage.infoSnackbar).toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Published flow.png', + }); + }); + + await test.step('unpublish from snackbar', async () => { + await page + .getByTestId('unpublish-flow-from-snackbar') + .click(); + await expect(flowEditorPage.infoSnackbar).not.toBeVisible(); + }); + + await test.step('publish once again', async () => { + await expect(flowEditorPage.publishFlowButton).toBeVisible(); + await flowEditorPage.publishFlowButton.click(); + await expect(flowEditorPage.publishFlowButton).not.toBeVisible(); + }); + + await test.step('unpublish from layout top bar', async () => { + await expect(flowEditorPage.unpublishFlowButton).toBeVisible(); + await flowEditorPage.unpublishFlowButton.click(); + await expect(flowEditorPage.unpublishFlowButton).not.toBeVisible(); + await flowEditorPage.screenshot({ + path: 'Unpublished flow.png', + }); + }); + }); + + await test.step('in layout', async () => { + await test.step('can go back to flows page', async () => { + await page.getByTestId('editor-go-back-button').click(); + await expect(page).toHaveURL('/flows'); + }); + }); + } +); \ No newline at end of file diff --git a/packages/e2e-tests/tests/user-interface/user-interface-configuration.spec.js b/packages/e2e-tests/tests/user-interface/user-interface-configuration.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..89c5a9e9693e6c273b232da24ad8c203b71f4e93 --- /dev/null +++ b/packages/e2e-tests/tests/user-interface/user-interface-configuration.spec.js @@ -0,0 +1,170 @@ +// @ts-check +const { test, expect } = require('../../fixtures/index'); + +test.describe('User interface page', () => { + test.beforeEach(async ({ userInterfacePage }) => { + await userInterfacePage.profileMenuButton.click(); + await userInterfacePage.adminMenuItem.click(); + await expect(userInterfacePage.page).toHaveURL(/\/admin-settings\/users/); + await userInterfacePage.userInterfaceDrawerItem.click(); + await expect(userInterfacePage.page).toHaveURL( + /\/admin-settings\/user-interface/ + ); + await userInterfacePage.page.waitForURL(/\/admin-settings\/user-interface/); + }); + + test.describe('checks if the shown values are used', async () => { + test('checks primary main color', async ({ userInterfacePage }) => { + await userInterfacePage.primaryMainColorInput.waitFor({ + state: 'attached', + }); + const initialPrimaryMainColor = + await userInterfacePage.primaryMainColorInput.inputValue(); + const initialRgbColor = userInterfacePage.hexToRgb( + initialPrimaryMainColor + ); + await expect(userInterfacePage.updateButton).toHaveCSS( + 'background-color', + initialRgbColor + ); + }); + + test('checks primary dark color', async ({ userInterfacePage }) => { + await userInterfacePage.primaryDarkColorInput.waitFor({ + state: 'attached', + }); + const initialPrimaryDarkColor = + await userInterfacePage.primaryDarkColorInput.inputValue(); + const initialRgbColor = userInterfacePage.hexToRgb( + initialPrimaryDarkColor + ); + await expect(userInterfacePage.appBar).toHaveCSS( + 'background-color', + initialRgbColor + ); + }); + }); + + test.describe( + 'fill fields and check if the inputs reflect them properly', + async () => { + test('fill primary main color and check the color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryMainColorInput.fill('#FF5733'); + const rgbColor = userInterfacePage.hexToRgb('#FF5733'); + const button = await userInterfacePage.primaryMainColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + + test('fill primary dark color and check the color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryDarkColorInput.fill('#12F63F'); + const rgbColor = userInterfacePage.hexToRgb('#12F63F'); + const button = await userInterfacePage.primaryDarkColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + + test('fill primary light color and check the color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryLightColorInput.fill('#1D0BF5'); + const rgbColor = userInterfacePage.hexToRgb('#1D0BF5'); + const button = await userInterfacePage.primaryLightColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + } + ); + + test.describe('update form based on input values', async () => { + test('fill primary main color', async ({ userInterfacePage }) => { + await userInterfacePage.primaryMainColorInput.fill('#00adef'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + await userInterfacePage.screenshot({ + path: 'updated primary main color.png', + }); + }); + + test('fill primary dark color', async ({ userInterfacePage }) => { + await userInterfacePage.primaryDarkColorInput.fill('#222222'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + await userInterfacePage.screenshot({ + path: 'updated primary dark color.png', + }); + }); + + test.skip('fill primary light color', async ({ userInterfacePage }) => { + await userInterfacePage.primaryLightColorInput.fill('#f90707'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.goToDashboardButton.click(); + await expect(userInterfacePage.page).toHaveURL('/flows'); + await userInterfacePage.flowRowCardActionArea.waitFor({ + state: 'visible', + }); + await userInterfacePage.flowRowCardActionArea.hover(); + await userInterfacePage.screenshot({ + path: 'updated primary light color.png', + }); + }); + + test('fill logo svg code', async ({ userInterfacePage }) => { + await userInterfacePage.logoSvgCodeInput + .fill(` + + A + `); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + await userInterfacePage.screenshot({ + path: 'updated svg code.png', + }); + }); + }); + + test.describe( + 'update form based on input values and check if the inputs still reflect them', + async () => { + test('update primary main color and check color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryMainColorInput.fill('#00adef'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + const rgbColor = userInterfacePage.hexToRgb('#00adef'); + const button = await userInterfacePage.primaryMainColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + + test('update primary dark color and check color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryDarkColorInput.fill('#222222'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + const rgbColor = userInterfacePage.hexToRgb('#222222'); + const button = await userInterfacePage.primaryDarkColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + + test('update primary light color and check color input', async ({ + userInterfacePage, + }) => { + await userInterfacePage.primaryLightColorInput.fill('#f90707'); + await userInterfacePage.updateButton.click(); + await userInterfacePage.snackbar.waitFor({ state: 'visible' }); + const rgbColor = userInterfacePage.hexToRgb('#f90707'); + const button = await userInterfacePage.primaryLightColorButton; + const styleAttribute = await button.getAttribute('style'); + expect(styleAttribute).toEqual(`background-color: ${rgbColor};`); + }); + } + ); +}); diff --git a/packages/web/.env-example b/packages/web/.env-example new file mode 100644 index 0000000000000000000000000000000000000000..d5663a32b2ffc106966955292287584c5f5d1a4b --- /dev/null +++ b/packages/web/.env-example @@ -0,0 +1,4 @@ +PORT=3001 +REACT_APP_BACKEND_URL=http://localhost:3000 +# HTTPS=true +REACT_APP_BASE_URL=http://localhost:3001 diff --git a/packages/web/.eslintignore b/packages/web/.eslintignore new file mode 100644 index 0000000000000000000000000000000000000000..49a238223a39f6f5b47c4b74d8f7cc0e0c096530 --- /dev/null +++ b/packages/web/.eslintignore @@ -0,0 +1,4 @@ +node_modules +build +source +.eslintrc.js diff --git a/packages/web/.eslintrc.js b/packages/web/.eslintrc.js new file mode 100644 index 0000000000000000000000000000000000000000..501340269e657ca04d6d23088ba250a9ffe6cb1f --- /dev/null +++ b/packages/web/.eslintrc.js @@ -0,0 +1,10 @@ +module.exports = { + extends: [ + 'react-app', + 'plugin:@tanstack/eslint-plugin-query/recommended', + 'prettier', + ], + rules: { + 'react/prop-types': 'warn', + }, +}; diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..4d29575de80483b005c29bfcac5061cd2f45313e --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 0000000000000000000000000000000000000000..b58e0af830ec5dbc90534e641396406fd17bf11c --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,46 @@ +# Getting Started with Create React App + +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +## Available Scripts + +In the project directory, you can run: + +### `yarn start` + +Runs the app in the development mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +The page will reload if you make edits.\ +You will also see any lint errors in the console. + +### `yarn test` + +Launches the test runner in the interactive watch mode.\ +See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. + +### `yarn build` + +Builds the app for production to the `build` folder.\ +It correctly bundles React in production mode and optimizes the build for the best performance. + +The build is minified and the filenames include the hashes.\ +Your app is ready to be deployed! + +See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. + +### `yarn eject` + +**Note: this is a one-way operation. Once you `eject`, you can’t go back!** + +If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. + +Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. + +You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. + +## Learn More + +You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). + +To learn React, check out the [React documentation](https://reactjs.org/). diff --git a/packages/web/index.js b/packages/web/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/packages/web/jsconfig.json b/packages/web/jsconfig.json new file mode 100644 index 0000000000000000000000000000000000000000..5875dc5b655a2755efd75eab192ae958d70c3ea0 --- /dev/null +++ b/packages/web/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "baseUrl": "src" + }, + "include": ["src"] +} diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000000000000000000000000000000000000..e80684936420236ed95595501480cfbc1809b821 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,97 @@ +{ + "name": "@automatisch/web", + "version": "0.10.0", + "license": "See LICENSE file", + "description": "The open source Zapier alternative. Build workflow automation without spending time and money.", + "dependencies": { + "@apollo/client": "^3.6.9", + "@casl/ability": "^6.5.0", + "@casl/react": "^3.1.0", + "@dagrejs/dagre": "^1.1.2", + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "@hookform/resolvers": "^2.8.8", + "@mui/icons-material": "^5.11.9", + "@mui/lab": "^5.0.0-alpha.120", + "@mui/material": "^5.11.10", + "@tanstack/react-query": "^5.24.1", + "@testing-library/jest-dom": "^5.11.4", + "@testing-library/react": "^11.1.0", + "@testing-library/user-event": "^12.1.10", + "clipboard-copy": "^4.0.1", + "compare-versions": "^4.1.3", + "graphql": "^15.6.0", + "lodash": "^4.17.21", + "luxon": "^2.3.1", + "mui-color-input": "^2.0.0", + "notistack": "^3.0.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-hook-form": "^7.45.2", + "react-intl": "^5.20.12", + "react-json-tree": "^0.16.2", + "react-router-dom": "^6.0.2", + "react-scripts": "5.0.0", + "react-window": "^1.8.9", + "reactflow": "^11.11.2", + "slate": "^0.94.1", + "slate-history": "^0.93.0", + "slate-react": "^0.94.2", + "uuid": "^9.0.0", + "web-vitals": "^1.0.1", + "yup": "^0.32.11" + }, + "main": "index.js", + "scripts": { + "dev": "react-scripts start", + "build": "react-scripts build", + "build:watch": "yarn nodemon --exec react-scripts build --watch 'src/**/*.ts' --watch 'public/**/*' --ext ts,html", + "test": "react-scripts test", + "eject": "react-scripts eject", + "lint": "eslint src --ext .js,.jsx", + "prepack": "yarn build" + }, + "files": [ + "/build" + ], + "contributors": [ + { + "name": "automatisch contributors", + "url": "https://github.com/automatisch/automatisch/graphs/contributors" + } + ], + "bugs": { + "url": "https://github.com/automatisch/automatisch/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/automatisch/automatisch.git" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@tanstack/eslint-plugin-query": "^5.20.1", + "@tanstack/react-query-devtools": "^5.24.1", + "eslint-config-prettier": "^9.1.0", + "eslint-config-react-app": "^7.0.1", + "prettier": "^3.2.5" + }, + "eslintConfig": { + "extends": [ + "./.eslintrc.js" + ] + } +} diff --git a/packages/web/public/browser-tab.ico b/packages/web/public/browser-tab.ico new file mode 100644 index 0000000000000000000000000000000000000000..59ab8480ef7bffdf8d21b5ef9386dec3f73f51fd Binary files /dev/null and b/packages/web/public/browser-tab.ico differ diff --git a/packages/web/public/fonts/Inter-Bold.ttf b/packages/web/public/fonts/Inter-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..fe23eeb9c93a377d0f4ab003f1f77b555d19b1d1 Binary files /dev/null and b/packages/web/public/fonts/Inter-Bold.ttf differ diff --git a/packages/web/public/fonts/Inter-Medium.ttf b/packages/web/public/fonts/Inter-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a01f3777a6fc284b7a720c0f8248a27066389ef9 Binary files /dev/null and b/packages/web/public/fonts/Inter-Medium.ttf differ diff --git a/packages/web/public/fonts/Inter-Regular.ttf b/packages/web/public/fonts/Inter-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e4851f0ab7e0268da6ce903306e2f871ee19821 Binary files /dev/null and b/packages/web/public/fonts/Inter-Regular.ttf differ diff --git a/packages/web/public/index.html b/packages/web/public/index.html new file mode 100644 index 0000000000000000000000000000000000000000..4222a2b854f1e0c8b4a6945702c333586bccdf39 --- /dev/null +++ b/packages/web/public/index.html @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/packages/web/public/manifest.json b/packages/web/public/manifest.json new file mode 100644 index 0000000000000000000000000000000000000000..692b2f748887915b22df3e4f72f62768ef9023a6 --- /dev/null +++ b/packages/web/public/manifest.json @@ -0,0 +1,9 @@ +{ + "short_name": "automatisch", + "name": "automatisch", + "description": "Build workflow automation without spending time and money. No code is required.", + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/packages/web/public/robots.txt b/packages/web/public/robots.txt new file mode 100644 index 0000000000000000000000000000000000000000..e9e57dc4d41b9b46e05112e9f45b7ea6ac0ba15e --- /dev/null +++ b/packages/web/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/packages/web/src/adminSettingsRoutes.jsx b/packages/web/src/adminSettingsRoutes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3e842d8158ebe85faedfd06f4115cc46f64d90d6 --- /dev/null +++ b/packages/web/src/adminSettingsRoutes.jsx @@ -0,0 +1,117 @@ +import { Route, Navigate } from 'react-router-dom'; + +import Users from 'pages/Users'; +import EditUser from 'pages/EditUser'; +import CreateUser from 'pages/CreateUser'; +import Roles from 'pages/Roles/index.ee'; +import CreateRole from 'pages/CreateRole/index.ee'; +import EditRole from 'pages/EditRole/index.ee'; +import Authentication from 'pages/Authentication'; +import UserInterface from 'pages/UserInterface'; +import * as URLS from 'config/urls'; +import Can from 'components/Can'; +import AdminApplications from 'pages/AdminApplications'; +import AdminApplication from 'pages/AdminApplication'; +// TODO: consider introducing redirections to `/` as fallback +export default ( + <> + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + + + + + } + /> + + + + + } + /> + + + + + } + /> + + } + /> + +); diff --git a/packages/web/src/components/AccountDropdownMenu/index.jsx b/packages/web/src/components/AccountDropdownMenu/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e4889c89ce095a41535d556c15d195d3cafc3398 --- /dev/null +++ b/packages/web/src/components/AccountDropdownMenu/index.jsx @@ -0,0 +1,75 @@ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import PropTypes from 'prop-types'; +import MenuItem from '@mui/material/MenuItem'; +import Menu from '@mui/material/Menu'; +import { Link } from 'react-router-dom'; + +import Can from 'components/Can'; +import apolloClient from 'graphql/client'; +import * as URLS from 'config/urls'; +import useAuthentication from 'hooks/useAuthentication'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRevokeAccessToken from 'hooks/useRevokeAccessToken'; +function AccountDropdownMenu(props) { + const formatMessage = useFormatMessage(); + const authentication = useAuthentication(); + const token = authentication.token; + const navigate = useNavigate(); + const revokeAccessTokenMutation = useRevokeAccessToken(token); + const { open, onClose, anchorEl, id } = props; + + const logout = async () => { + await revokeAccessTokenMutation.mutateAsync(); + + authentication.removeToken(); + await apolloClient.clearStore(); + onClose(); + navigate(URLS.LOGIN); + }; + + return ( + + + {formatMessage('accountDropdownMenu.settings')} + + + + + {formatMessage('accountDropdownMenu.adminSettings')} + + + + + {formatMessage('accountDropdownMenu.logout')} + + + ); +} + +AccountDropdownMenu.propTypes = { + open: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]), + id: PropTypes.string.isRequired, +}; + +export default AccountDropdownMenu; diff --git a/packages/web/src/components/AddAppConnection/index.jsx b/packages/web/src/components/AddAppConnection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7c6f3e23dd9fd2f4dab836a99b77d2ecd5e5d765 --- /dev/null +++ b/packages/web/src/components/AddAppConnection/index.jsx @@ -0,0 +1,171 @@ +import PropTypes from 'prop-types'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Alert from '@mui/material/Alert'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import * as React from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; + +import { AppPropType } from 'propTypes/propTypes'; +import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; +import InputCreator from 'components/InputCreator'; +import * as URLS from 'config/urls'; +import useAuthenticateApp from 'hooks/useAuthenticateApp.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { generateExternalLink } from 'helpers/translationValues'; +import { Form } from './style'; +import useAppAuth from 'hooks/useAppAuth'; +import { useQueryClient } from '@tanstack/react-query'; + +function AddAppConnection(props) { + const { application, connectionId, onClose } = props; + const { name, authDocUrl, key } = application; + const { data: auth } = useAppAuth(key); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const formatMessage = useFormatMessage(); + const [error, setError] = React.useState(null); + const [inProgress, setInProgress] = React.useState(false); + const hasConnection = Boolean(connectionId); + const useShared = searchParams.get('shared') === 'true'; + const appAuthClientId = searchParams.get('appAuthClientId') || undefined; + const { authenticate } = useAuthenticateApp({ + appKey: key, + connectionId, + appAuthClientId, + useShared: !!appAuthClientId, + }); + const queryClient = useQueryClient(); + + React.useEffect(function relayProviderData() { + if (window.opener) { + window.opener.postMessage({ + source: 'automatisch', + payload: { search: window.location.search, hash: window.location.hash }, + }); + + window.close(); + } + }, []); + + React.useEffect( + function initiateSharedAuthenticationForGivenAuthClient() { + if (!appAuthClientId) return; + + if (!authenticate) return; + + const asyncAuthenticate = async () => { + await authenticate(); + navigate(URLS.APP_CONNECTIONS(key)); + }; + + asyncAuthenticate(); + }, + [appAuthClientId, authenticate], + ); + + const handleClientClick = (appAuthClientId) => + navigate(URLS.APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID(key, appAuthClientId)); + + const handleAuthClientsDialogClose = () => + navigate(URLS.APP_CONNECTIONS(key)); + + const submitHandler = React.useCallback( + async (data) => { + if (!authenticate) return; + setInProgress(true); + try { + const response = await authenticate({ + fields: data, + }); + + await queryClient.invalidateQueries({ + queryKey: ['apps', key, 'connections'], + }); + onClose(response); + } catch (err) { + const error = err; + console.log(error); + setError(error.graphQLErrors?.[0]); + } finally { + setInProgress(false); + } + }, + [authenticate], + ); + + if (useShared) + return ( + + ); + + if (appAuthClientId) return ; + + return ( + + + {hasConnection + ? formatMessage('app.reconnectConnection') + : formatMessage('app.addConnection')} + + + {authDocUrl && ( + + {formatMessage('addAppConnection.callToDocs', { + appName: name, + docsLink: generateExternalLink(authDocUrl), + })} + + )} + + {error && ( + + {error.message} + {error.details && ( +
+              {JSON.stringify(error.details, null, 2)}
+            
+ )} +
+ )} + + + +
+ {auth?.data?.fields?.map((field) => ( + + ))} + + + {formatMessage('addAppConnection.submit')} + + +
+
+
+ ); +} + +AddAppConnection.propTypes = { + onClose: PropTypes.func.isRequired, + application: AppPropType.isRequired, + connectionId: PropTypes.string, +}; + +export default AddAppConnection; diff --git a/packages/web/src/components/AddAppConnection/style.js b/packages/web/src/components/AddAppConnection/style.js new file mode 100644 index 0000000000000000000000000000000000000000..2d2c43472504b8136608d7b2d072400b42988e91 --- /dev/null +++ b/packages/web/src/components/AddAppConnection/style.js @@ -0,0 +1,8 @@ +import { styled } from '@mui/material/styles'; +import BaseForm from 'components/Form'; +export const Form = styled(BaseForm)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingTop: theme.spacing(1), +})); diff --git a/packages/web/src/components/AddNewAppConnection/index.jsx b/packages/web/src/components/AddNewAppConnection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4ed2146fd66361f7ec494e00c8d82a989e066120 --- /dev/null +++ b/packages/web/src/components/AddNewAppConnection/index.jsx @@ -0,0 +1,148 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import debounce from 'lodash/debounce'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import Dialog from '@mui/material/Dialog'; +import SearchIcon from '@mui/icons-material/Search'; +import InputAdornment from '@mui/material/InputAdornment'; +import CircularProgress from '@mui/material/CircularProgress'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import FormControl from '@mui/material/FormControl'; +import Box from '@mui/material/Box'; + +import * as URLS from 'config/urls'; +import AppIcon from 'components/AppIcon'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useLazyApps from 'hooks/useLazyApps'; + +function createConnectionOrFlow(appKey, supportsConnections = false) { + if (!supportsConnections) { + return URLS.CREATE_FLOW_WITH_APP(appKey); + } + + return URLS.APP_ADD_CONNECTION(appKey); +} +function AddNewAppConnection(props) { + const { onClose } = props; + const theme = useTheme(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('sm')); + const formatMessage = useFormatMessage(); + const [appName, setAppName] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const { data: apps, mutate } = useLazyApps( + { appName }, + { + onSuccess: () => { + setIsLoading(false); + }, + }, + ); + + const fetchData = React.useMemo(() => debounce(mutate, 300), [mutate]); + + React.useEffect(() => { + setIsLoading(true); + + fetchData(appName); + + return () => { + fetchData.cancel(); + }; + }, [fetchData, appName]); + + return ( + + {formatMessage('apps.addNewAppConnection')} + + + + + {formatMessage('apps.searchApp')} + + + setAppName(event.target.value)} + endAdornment={ + + theme.palette.primary.main }} + /> + + } + label={formatMessage('apps.searchApp')} + inputProps={{ + 'data-test': 'search-for-app-text-field', + }} + /> + + + + + + {isLoading && ( + + )} + + {!isLoading && + apps?.data.map((app) => ( + + + + + + + theme.palette.text.primary }, + }} + /> + + + ))} + + + + ); +} + +AddNewAppConnection.propTypes = { + onClose: PropTypes.func.isRequired, +}; + +export default AddNewAppConnection; diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx b/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2fc93069bd363d5b9b91ff97db0be5ffbf7fc247 --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClientDialog/index.jsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Alert from '@mui/material/Alert'; +import Dialog from '@mui/material/Dialog'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import CircularProgress from '@mui/material/CircularProgress'; +import { ApolloError } from '@apollo/client'; + +import { FieldPropType } from 'propTypes/propTypes'; +import useFormatMessage from 'hooks/useFormatMessage'; +import InputCreator from 'components/InputCreator'; +import Switch from 'components/Switch'; +import TextField from 'components/TextField'; +import { Form } from './style'; + +function AdminApplicationAuthClientDialog(props) { + const { + error, + onClose, + title, + loading, + submitHandler, + authFields, + submitting, + defaultValues, + disabled = false, + } = props; + const formatMessage = useFormatMessage(); + return ( + + {title} + {error && ( + + {error.message} + + )} + + {loading ? ( + + ) : ( + +
( + <> + + + {authFields?.map((field) => ( + + ))} + + {formatMessage('authClient.buttonSubmit')} + + + )} + > +
+ )} +
+
+ ); +} + +AdminApplicationAuthClientDialog.propTypes = { + error: PropTypes.instanceOf(ApolloError), + onClose: PropTypes.func.isRequired, + title: PropTypes.string.isRequired, + loading: PropTypes.bool.isRequired, + submitHandler: PropTypes.func.isRequired, + authFields: PropTypes.arrayOf(FieldPropType), + submitting: PropTypes.bool.isRequired, + defaultValues: PropTypes.object.isRequired, + disabled: PropTypes.bool, +}; + +export default AdminApplicationAuthClientDialog; diff --git a/packages/web/src/components/AdminApplicationAuthClientDialog/style.js b/packages/web/src/components/AdminApplicationAuthClientDialog/style.js new file mode 100644 index 0000000000000000000000000000000000000000..2d2c43472504b8136608d7b2d072400b42988e91 --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClientDialog/style.js @@ -0,0 +1,8 @@ +import { styled } from '@mui/material/styles'; +import BaseForm from 'components/Form'; +export const Form = styled(BaseForm)(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + gap: theme.spacing(2), + paddingTop: theme.spacing(1), +})); diff --git a/packages/web/src/components/AdminApplicationAuthClients/index.jsx b/packages/web/src/components/AdminApplicationAuthClients/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..114150f30866b9941ec7afe20eabc5421bf6acff --- /dev/null +++ b/packages/web/src/components/AdminApplicationAuthClients/index.jsx @@ -0,0 +1,86 @@ +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import Chip from '@mui/material/Chip'; +import Button from '@mui/material/Button'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useAdminAppAuthClients from 'hooks/useAdminAppAuthClients'; +import NoResultFound from 'components/NoResultFound'; + +function AdminApplicationAuthClients(props) { + const { appKey } = props; + const formatMessage = useFormatMessage(); + const { data: appAuthClients, isLoading } = useAdminAppAuthClients(appKey); + + if (isLoading) + return ; + + if (!appAuthClients?.data.length) { + return ( + + ); + } + + const sortedAuthClients = appAuthClients.data.slice().sort((a, b) => { + if (a.id < b.id) { + return -1; + } + if (a.id > b.id) { + return 1; + } + return 0; + }); + + return ( +
+ {sortedAuthClients.map((client) => ( + + + + + + {client.name} + + + + + + + ))} + + + + + +
+ ); +} + +AdminApplicationAuthClients.propTypes = { + appKey: PropTypes.string.isRequired, +}; + +export default AdminApplicationAuthClients; diff --git a/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4271605ad6cc4d97d5dad18bd5463514153c2226 --- /dev/null +++ b/packages/web/src/components/AdminApplicationCreateAuthClient/index.jsx @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useMemo } from 'react'; +import { useMutation } from '@apollo/client'; + +import { AppPropType } from 'propTypes/propTypes'; +import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config'; +import { CREATE_APP_AUTH_CLIENT } from 'graphql/mutations/create-app-auth-client'; +import useAppConfig from 'hooks/useAppConfig.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog'; +import useAppAuth from 'hooks/useAppAuth'; + +function AdminApplicationCreateAuthClient(props) { + const { appKey, onClose } = props; + const { data: auth } = useAppAuth(appKey); + const formatMessage = useFormatMessage(); + + const { data: appConfig, isLoading: isAppConfigLoading } = + useAppConfig(appKey); + + const [ + createAppConfig, + { loading: loadingCreateAppConfig, error: createAppConfigError }, + ] = useMutation(CREATE_APP_CONFIG, { + refetchQueries: ['GetAppConfig'], + context: { autoSnackbar: false }, + }); + + const [ + createAppAuthClient, + { loading: loadingCreateAppAuthClient, error: createAppAuthClientError }, + ] = useMutation(CREATE_APP_AUTH_CLIENT, { + refetchQueries: ['GetAppAuthClients'], + context: { autoSnackbar: false }, + }); + + const submitHandler = async (values) => { + let appConfigId = appConfig?.data?.id; + + if (!appConfigId) { + const { data: appConfigData } = await createAppConfig({ + variables: { + input: { + key: appKey, + allowCustomConnection: false, + shared: false, + disabled: false, + }, + }, + }); + + appConfigId = appConfigData.createAppConfig.id; + } + + const { name, active, ...formattedAuthDefaults } = values; + + await createAppAuthClient({ + variables: { + input: { + appConfigId, + name, + active, + formattedAuthDefaults, + }, + }, + }); + + onClose(); + }; + + const getAuthFieldsDefaultValues = useCallback(() => { + if (!auth?.data?.fields) { + return {}; + } + + const defaultValues = {}; + + auth.data.fields.forEach((field) => { + if (field.value || field.type !== 'string') { + defaultValues[field.key] = field.value; + } else if (field.type === 'string') { + defaultValues[field.key] = ''; + } + }); + + return defaultValues; + }, [auth?.data?.fields]); + + const defaultValues = useMemo( + () => ({ + name: '', + active: false, + ...getAuthFieldsDefaultValues(), + }), + [getAuthFieldsDefaultValues], + ); + + return ( + + ); +} + +AdminApplicationCreateAuthClient.propTypes = { + appKey: PropTypes.string.isRequired, + application: AppPropType.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default AdminApplicationCreateAuthClient; diff --git a/packages/web/src/components/AdminApplicationSettings/index.jsx b/packages/web/src/components/AdminApplicationSettings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..aba0935f22ae8ffa903b191741effa522745ab1d --- /dev/null +++ b/packages/web/src/components/AdminApplicationSettings/index.jsx @@ -0,0 +1,127 @@ +import PropTypes from 'prop-types'; +import { useMemo } from 'react'; +import useAppConfig from 'hooks/useAppConfig.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import Divider from '@mui/material/Divider'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import LoadingButton from '@mui/lab/LoadingButton'; +import { useMutation } from '@apollo/client'; + +import { CREATE_APP_CONFIG } from 'graphql/mutations/create-app-config'; +import { UPDATE_APP_CONFIG } from 'graphql/mutations/update-app-config'; +import Form from 'components/Form'; +import { Switch } from './style'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; + +function AdminApplicationSettings(props) { + const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); + + const { data: appConfig, isLoading: loading } = useAppConfig(props.appKey); + + const [createAppConfig, { loading: loadingCreateAppConfig }] = useMutation( + CREATE_APP_CONFIG, + { + refetchQueries: ['GetAppConfig'], + }, + ); + + const [updateAppConfig, { loading: loadingUpdateAppConfig }] = useMutation( + UPDATE_APP_CONFIG, + { + refetchQueries: ['GetAppConfig'], + }, + ); + + const handleSubmit = async (values) => { + try { + if (!appConfig?.data) { + await createAppConfig({ + variables: { + input: { key: props.appKey, ...values }, + }, + }); + } else { + await updateAppConfig({ + variables: { + input: { id: appConfig.data.id, ...values }, + }, + }); + } + + enqueueSnackbar(formatMessage('adminAppsSettings.successfullySaved'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-save-admin-apps-settings-success', + }, + }); + } catch (error) { + throw new Error('Failed while saving!'); + } + }; + + const defaultValues = useMemo( + () => ({ + allowCustomConnection: appConfig?.data?.allowCustomConnection || false, + shared: appConfig?.data?.shared || false, + disabled: appConfig?.data?.disabled || false, + }), + [appConfig?.data], + ); + + return ( +
( + + + + + + + + + + + + {formatMessage('adminAppsSettings.save')} + + + + )} + >
+ ); +} + +AdminApplicationSettings.propTypes = { + appKey: PropTypes.string.isRequired, +}; + +export default AdminApplicationSettings; diff --git a/packages/web/src/components/AdminApplicationSettings/style.js b/packages/web/src/components/AdminApplicationSettings/style.js new file mode 100644 index 0000000000000000000000000000000000000000..7f42ed863b32faee0aaafb5e91ee64f70e21bea1 --- /dev/null +++ b/packages/web/src/components/AdminApplicationSettings/style.js @@ -0,0 +1,6 @@ +import { styled } from '@mui/material/styles'; +import SwitchBase from 'components/Switch'; +export const Switch = styled(SwitchBase)` + justify-content: space-between; + margin: 0; +`; diff --git a/packages/web/src/components/AdminApplicationUpdateAuthClient/index.jsx b/packages/web/src/components/AdminApplicationUpdateAuthClient/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7e15dec313fa5b1b8353fac7ac05aa2500ab59c8 --- /dev/null +++ b/packages/web/src/components/AdminApplicationUpdateAuthClient/index.jsx @@ -0,0 +1,100 @@ +import PropTypes from 'prop-types'; +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; + +import { AppPropType } from 'propTypes/propTypes'; +import { UPDATE_APP_AUTH_CLIENT } from 'graphql/mutations/update-app-auth-client'; +import useFormatMessage from 'hooks/useFormatMessage'; +import AdminApplicationAuthClientDialog from 'components/AdminApplicationAuthClientDialog'; +import useAdminAppAuthClient from 'hooks/useAdminAppAuthClient.ee'; +import useAppAuth from 'hooks/useAppAuth'; + +function AdminApplicationUpdateAuthClient(props) { + const { application, onClose } = props; + const formatMessage = useFormatMessage(); + const { clientId } = useParams(); + + const { data: adminAppAuthClient, isLoading: isAdminAuthClientLoading } = + useAdminAppAuthClient(clientId); + + const { data: auth } = useAppAuth(application.key); + + const [updateAppAuthClient, { loading: loadingUpdateAppAuthClient, error }] = + useMutation(UPDATE_APP_AUTH_CLIENT, { + refetchQueries: ['GetAppAuthClients'], + context: { autoSnackbar: false }, + }); + + const authFields = auth?.data?.fields?.map((field) => ({ + ...field, + required: false, + })); + + const submitHandler = async (values) => { + if (!adminAppAuthClient) { + return; + } + + const { name, active, ...formattedAuthDefaults } = values; + + await updateAppAuthClient({ + variables: { + input: { + id: adminAppAuthClient.data.id, + name, + active, + formattedAuthDefaults, + }, + }, + }); + + onClose(); + }; + + const getAuthFieldsDefaultValues = useCallback(() => { + if (!authFields) { + return {}; + } + + const defaultValues = {}; + authFields.forEach((field) => { + if (field.value || field.type !== 'string') { + defaultValues[field.key] = field.value; + } else if (field.type === 'string') { + defaultValues[field.key] = ''; + } + }); + return defaultValues; + }, [auth?.fields]); + + const defaultValues = useMemo( + () => ({ + name: adminAppAuthClient?.data?.name || '', + active: adminAppAuthClient?.data?.active || false, + ...getAuthFieldsDefaultValues(), + }), + [adminAppAuthClient, getAuthFieldsDefaultValues], + ); + + return ( + + ); +} + +AdminApplicationUpdateAuthClient.propTypes = { + application: AppPropType.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default AdminApplicationUpdateAuthClient; diff --git a/packages/web/src/components/AdminSettingsLayout/index.jsx b/packages/web/src/components/AdminSettingsLayout/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..efe953b945c03644e8336a553dfea0c8304fbb55 --- /dev/null +++ b/packages/web/src/components/AdminSettingsLayout/index.jsx @@ -0,0 +1,123 @@ +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import GroupIcon from '@mui/icons-material/Group'; +import GroupsIcon from '@mui/icons-material/Groups'; +import LockIcon from '@mui/icons-material/LockPerson'; +import BrushIcon from '@mui/icons-material/Brush'; +import AppsIcon from '@mui/icons-material/Apps'; +import { Outlet } from 'react-router-dom'; + +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import * as React from 'react'; +import AppBar from 'components/AppBar'; +import Drawer from 'components/Drawer'; +import Can from 'components/Can'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; + +function createDrawerLinks({ + canReadRole, + canReadUser, + canUpdateConfig, + canManageSamlAuthProvider, + canUpdateApp, +}) { + const items = [ + canReadUser + ? { + Icon: GroupIcon, + primary: 'adminSettingsDrawer.users', + to: URLS.USERS, + dataTest: 'users-drawer-link', + } + : null, + canReadRole + ? { + Icon: GroupsIcon, + primary: 'adminSettingsDrawer.roles', + to: URLS.ROLES, + dataTest: 'roles-drawer-link', + } + : null, + canUpdateConfig + ? { + Icon: BrushIcon, + primary: 'adminSettingsDrawer.userInterface', + to: URLS.USER_INTERFACE, + dataTest: 'user-interface-drawer-link', + } + : null, + canManageSamlAuthProvider + ? { + Icon: LockIcon, + primary: 'adminSettingsDrawer.authentication', + to: URLS.AUTHENTICATION, + dataTest: 'authentication-drawer-link', + } + : null, + canUpdateApp + ? { + Icon: AppsIcon, + primary: 'adminSettingsDrawer.apps', + to: URLS.ADMIN_APPS, + dataTest: 'apps-drawer-link', + } + : null, + ].filter(Boolean); + return items; +} + +function SettingsLayout() { + const theme = useTheme(); + const formatMessage = useFormatMessage(); + const currentUserAbility = useCurrentUserAbility(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); + const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); + const openDrawer = () => setDrawerOpen(true); + const closeDrawer = () => setDrawerOpen(false); + const drawerLinks = createDrawerLinks({ + canReadUser: currentUserAbility.can('read', 'User'), + canReadRole: currentUserAbility.can('read', 'Role'), + canUpdateConfig: currentUserAbility.can('update', 'Config'), + canManageSamlAuthProvider: + currentUserAbility.can('read', 'SamlAuthProvider') && + currentUserAbility.can('update', 'SamlAuthProvider') && + currentUserAbility.can('create', 'SamlAuthProvider'), + canUpdateApp: currentUserAbility.can('update', 'App'), + }); + const drawerBottomLinks = [ + { + Icon: ArrowBackIosNewIcon, + primary: formatMessage('adminSettingsDrawer.goBack'), + to: '/', + dataTest: 'go-back-drawer-link', + }, + ]; + return ( + + + + + + + + + + + ); +} + +export default SettingsLayout; diff --git a/packages/web/src/components/ApolloProvider/index.jsx b/packages/web/src/components/ApolloProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2f74988a81c895cc5160af4aeb955c6e335944f0 --- /dev/null +++ b/packages/web/src/components/ApolloProvider/index.jsx @@ -0,0 +1,34 @@ +import { ApolloProvider as BaseApolloProvider } from '@apollo/client'; +import * as React from 'react'; + +import { mutateAndGetClient } from 'graphql/client'; +import useAuthentication from 'hooks/useAuthentication'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; + +const ApolloProvider = (props) => { + const enqueueSnackbar = useEnqueueSnackbar(); + const authentication = useAuthentication(); + + const onError = React.useCallback( + (message) => { + enqueueSnackbar(message, { + variant: 'error', + SnackbarProps: { + 'data-test': 'snackbar-error', + }, + }); + }, + [enqueueSnackbar], + ); + + const client = React.useMemo(() => { + return mutateAndGetClient({ + onError, + token: authentication.token, + }); + }, [onError, authentication]); + + return ; +}; + +export default ApolloProvider; diff --git a/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx b/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a9cce58d43a3a45f1b889e0f99c8c086411cd9fb --- /dev/null +++ b/packages/web/src/components/AppAuthClientsDialog/index.ee.jsx @@ -0,0 +1,53 @@ +import PropTypes from 'prop-types'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import * as React from 'react'; +import useAppAuthClients from 'hooks/useAppAuthClients'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function AppAuthClientsDialog(props) { + const { appKey, onClientClick, onClose } = props; + const { data: appAuthClients } = useAppAuthClients(appKey); + + const formatMessage = useFormatMessage(); + + React.useEffect( + function autoAuthenticateSingleClient() { + if (appAuthClients?.data.length === 1) { + onClientClick(appAuthClients.data[0].id); + } + }, + [appAuthClients?.data], + ); + + if (!appAuthClients?.data.length || appAuthClients?.data.length === 1) + return ; + + return ( + + {formatMessage('appAuthClientsDialog.title')} + + + {appAuthClients.data.map((appAuthClient) => ( + + onClientClick(appAuthClient.id)}> + + + + ))} + + + ); +} + +AppAuthClientsDialog.propTypes = { + appKey: PropTypes.string.isRequired, + onClientClick: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, +}; + +export default AppAuthClientsDialog; diff --git a/packages/web/src/components/AppBar/index.jsx b/packages/web/src/components/AppBar/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..57f52ce0df1bc0f18934e75eb957dc10a40740fe --- /dev/null +++ b/packages/web/src/components/AppBar/index.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import MenuIcon from '@mui/icons-material/Menu'; +import MenuOpenIcon from '@mui/icons-material/MenuOpen'; +import MuiAppBar from '@mui/material/AppBar'; +import IconButton from '@mui/material/IconButton'; +import Toolbar from '@mui/material/Toolbar'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import * as React from 'react'; +import AccountDropdownMenu from 'components/AccountDropdownMenu'; +import Container from 'components/Container'; +import Logo from 'components/Logo/index'; +import TrialStatusBadge from 'components/TrialStatusBadge/index.ee'; +import * as URLS from 'config/urls'; +import { Link } from './style'; + +const accountMenuId = 'account-menu'; + +function AppBar(props) { + const { drawerOpen, onDrawerOpen, onDrawerClose, maxWidth = false } = props; + const theme = useTheme(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md')); + const [accountMenuAnchorElement, setAccountMenuAnchorElement] = + React.useState(null); + const isMenuOpen = Boolean(accountMenuAnchorElement); + const handleAccountMenuOpen = (event) => { + setAccountMenuAnchorElement(event.currentTarget); + }; + const handleAccountMenuClose = () => { + setAccountMenuAnchorElement(null); + }; + return ( + + + + + {drawerOpen && matchSmallScreens ? : } + + +
+ + + +
+ + + + + + +
+
+ + +
+ ); +} + +AppBar.propTypes = { + drawerOpen: PropTypes.bool.isRequired, + onDrawerOpen: PropTypes.func.isRequired, + onDrawerClose: PropTypes.func.isRequired, + maxWidth: PropTypes.oneOfType([ + PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl', false]), + PropTypes.string, + ]), +}; + +export default AppBar; diff --git a/packages/web/src/components/AppBar/style.js b/packages/web/src/components/AppBar/style.js new file mode 100644 index 0000000000000000000000000000000000000000..2d26af6e8cf4145078c5ad66e5faeb99c62bb75c --- /dev/null +++ b/packages/web/src/components/AppBar/style.js @@ -0,0 +1,7 @@ +import { styled } from '@mui/material/styles'; +import { Link as RouterLink } from 'react-router-dom'; +export const Link = styled(RouterLink)(() => ({ + textDecoration: 'none', + color: 'inherit', + display: 'inline-flex', +})); diff --git a/packages/web/src/components/AppConnectionContextMenu/index.jsx b/packages/web/src/components/AppConnectionContextMenu/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a544d00535812e1241b4f43ba8fc99b583f8df84 --- /dev/null +++ b/packages/web/src/components/AppConnectionContextMenu/index.jsx @@ -0,0 +1,91 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; + +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { ConnectionPropType } from 'propTypes/propTypes'; +import { useQueryClient } from '@tanstack/react-query'; + +function ContextMenu(props) { + const { + appKey, + connection, + onClose, + onMenuItemClick, + anchorEl, + disableReconnection, + } = props; + const formatMessage = useFormatMessage(); + const queryClient = useQueryClient(); + + const createActionHandler = React.useCallback( + (action) => { + return async function clickHandler(event) { + onMenuItemClick(event, action); + + if (['test', 'reconnect', 'delete'].includes(action.type)) { + await queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'connections'], + }); + } + onClose(); + }; + }, + [onMenuItemClick, onClose, queryClient], + ); + + return ( + + + {formatMessage('connection.viewFlows')} + + + + {formatMessage('connection.testConnection')} + + + + {formatMessage('connection.reconnect')} + + + + {formatMessage('connection.delete')} + + + ); +} + +ContextMenu.propTypes = { + appKey: PropTypes.string.isRequired, + connection: ConnectionPropType.isRequired, + onClose: PropTypes.func.isRequired, + onMenuItemClick: PropTypes.func.isRequired, + anchorEl: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]), + disableReconnection: PropTypes.bool.isRequired, +}; + +export default ContextMenu; diff --git a/packages/web/src/components/AppConnectionRow/index.jsx b/packages/web/src/components/AppConnectionRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5e6dc0ac7446def6e977d8158cc8de94cad98668 --- /dev/null +++ b/packages/web/src/components/AppConnectionRow/index.jsx @@ -0,0 +1,189 @@ +import * as React from 'react'; +import { useMutation } from '@apollo/client'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorIcon from '@mui/icons-material/Error'; +import Skeleton from '@mui/material/Skeleton'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import Box from '@mui/material/Box'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import CircularProgress from '@mui/material/CircularProgress'; +import Stack from '@mui/material/Stack'; +import { DateTime } from 'luxon'; + +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import ConnectionContextMenu from 'components/AppConnectionContextMenu'; +import { DELETE_CONNECTION } from 'graphql/mutations/delete-connection'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { ConnectionPropType } from 'propTypes/propTypes'; +import { CardContent, Typography } from './style'; +import useConnectionFlows from 'hooks/useConnectionFlows'; +import useTestConnection from 'hooks/useTestConnection'; + +const countTranslation = (value) => ( + <> + {value} +
+ +); + +function AppConnectionRow(props) { + const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); + const { id, key, formattedData, verified, createdAt, reconnectable } = + props.connection; + const [verificationVisible, setVerificationVisible] = React.useState(false); + const contextButtonRef = React.useRef(null); + const [anchorEl, setAnchorEl] = React.useState(null); + + const [deleteConnection] = useMutation(DELETE_CONNECTION); + + const { mutate: testConnection, isPending: isTestConnectionPending } = + useTestConnection( + { connectionId: id }, + { + onSettled: () => { + setTimeout(() => setVerificationVisible(false), 3000); + }, + }, + ); + + const handleClose = () => { + setAnchorEl(null); + }; + + const { data, isLoading: isConnectionFlowsLoading } = useConnectionFlows({ + connectionId: id, + }); + const flowCount = data?.meta?.count; + + const onContextMenuClick = () => setAnchorEl(contextButtonRef.current); + + const onContextMenuAction = React.useCallback( + async (event, action) => { + if (action.type === 'delete') { + await deleteConnection({ + variables: { input: { id } }, + update: (cache) => { + const connectionCacheId = cache.identify({ + __typename: 'Connection', + id, + }); + cache.evict({ + id: connectionCacheId, + }); + }, + }); + + enqueueSnackbar(formatMessage('connection.deletedMessage'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-delete-connection-success', + }, + }); + } else if (action.type === 'test') { + setVerificationVisible(true); + testConnection({ variables: { id } }); + } + }, + [deleteConnection, id, testConnection, formatMessage, enqueueSnackbar], + ); + + const relativeCreatedAt = DateTime.fromMillis( + parseInt(createdAt, 10), + ).toRelative(); + + return ( + <> + + + + + + {formattedData?.screenName} + + + + {formatMessage('connection.addedAt', { + datetime: relativeCreatedAt, + })} + + + + + + {verificationVisible && isTestConnectionPending && ( + <> + + + {formatMessage('connection.testing')} + + + )} + {verificationVisible && + !isTestConnectionPending && + verified && ( + <> + + + {formatMessage('connection.testSuccessful')} + + + )} + {verificationVisible && + !isTestConnectionPending && + !verified && ( + <> + + + {formatMessage('connection.testFailed')} + + + )} + + + + + + {formatMessage('connection.flowCount', { + count: countTranslation( + isConnectionFlowsLoading ? ( + + ) : ( + flowCount + ), + ), + })} + + + + + + + + + + + {anchorEl && ( + + )} + + ); +} + +AppConnectionRow.propTypes = { + connection: ConnectionPropType.isRequired, +}; + +export default AppConnectionRow; diff --git a/packages/web/src/components/AppConnectionRow/style.js b/packages/web/src/components/AppConnectionRow/style.js new file mode 100644 index 0000000000000000000000000000000000000000..2bc8d163a18757ad85f6bd16722e1001c8a496bc --- /dev/null +++ b/packages/web/src/components/AppConnectionRow/style.js @@ -0,0 +1,14 @@ +import { styled } from '@mui/material/styles'; +import MuiCardContent from '@mui/material/CardContent'; +import MuiTypography from '@mui/material/Typography'; +export const CardContent = styled(MuiCardContent)(({ theme }) => ({ + display: 'grid', + gridTemplateRows: 'auto', + gridTemplateColumns: '1fr auto auto auto', + gridColumnGap: theme.spacing(2), + alignItems: 'center', +})); +export const Typography = styled(MuiTypography)(() => ({ + textAlign: 'center', + display: 'inline-block', +})); diff --git a/packages/web/src/components/AppConnections/index.jsx b/packages/web/src/components/AppConnections/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..da1e12802b9db91ca8c124387d00cd8470b55966 --- /dev/null +++ b/packages/web/src/components/AppConnections/index.jsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; + +import AppConnectionRow from 'components/AppConnectionRow'; +import NoResultFound from 'components/NoResultFound'; +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import useAppConnections from 'hooks/useAppConnections'; + +function AppConnections(props) { + const { appKey } = props; + const formatMessage = useFormatMessage(); + const { data } = useAppConnections(appKey); + const appConnections = data?.data || []; + const hasConnections = appConnections?.length; + + if (!hasConnections) { + return ( + + ); + } + + return ( + <> + {appConnections.map((appConnection) => ( + + ))} + + ); +} + +AppConnections.propTypes = { + appKey: PropTypes.string.isRequired, +}; + +export default AppConnections; diff --git a/packages/web/src/components/AppFlows/index.jsx b/packages/web/src/components/AppFlows/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ed984d2d68d65b41ebb917bc96685b536a3e1049 --- /dev/null +++ b/packages/web/src/components/AppFlows/index.jsx @@ -0,0 +1,76 @@ +import PropTypes from 'prop-types'; +import { Link, useSearchParams } from 'react-router-dom'; +import Pagination from '@mui/material/Pagination'; +import PaginationItem from '@mui/material/PaginationItem'; + +import * as URLS from 'config/urls'; +import AppFlowRow from 'components/FlowRow'; +import NoResultFound from 'components/NoResultFound'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useConnectionFlows from 'hooks/useConnectionFlows'; +import useAppFlows from 'hooks/useAppFlows'; + +function AppFlows(props) { + const { appKey } = props; + const formatMessage = useFormatMessage(); + const [searchParams, setSearchParams] = useSearchParams(); + const connectionId = searchParams.get('connectionId') || undefined; + const page = parseInt(searchParams.get('page') || '', 10) || 1; + const isConnectionFlowEnabled = !!connectionId; + const isAppFlowEnabled = !!appKey && !connectionId; + + const connectionFlows = useConnectionFlows( + { connectionId, page }, + { enabled: isConnectionFlowEnabled }, + ); + + const appFlows = useAppFlows({ appKey, page }, { enabled: isAppFlowEnabled }); + + const flows = isConnectionFlowEnabled + ? connectionFlows?.data?.data || [] + : appFlows?.data?.data || []; + const pageInfo = isConnectionFlowEnabled + ? connectionFlows?.data?.meta || [] + : appFlows?.data?.meta || []; + const hasFlows = flows?.length; + + if (!hasFlows) { + return ( + + ); + } + + return ( + <> + {flows?.map((appFlow) => ( + + ))} + + {pageInfo && pageInfo.totalPages > 1 && ( + setSearchParams({ page: page.toString() })} + renderItem={(item) => ( + + )} + /> + )} + + ); +} + +AppFlows.propTypes = { + appKey: PropTypes.string.isRequired, +}; + +export default AppFlows; diff --git a/packages/web/src/components/AppIcon/index.jsx b/packages/web/src/components/AppIcon/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..92b102b6b579f8a4d0f1134dc4ee3d49bd959507 --- /dev/null +++ b/packages/web/src/components/AppIcon/index.jsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Avatar from '@mui/material/Avatar'; +const inlineImgStyle = { + objectFit: 'contain', +}; +function AppIcon(props) { + const { name, url, color, sx = {}, variant = 'square', ...restProps } = props; + const initialLetter = name?.[0]; + return ( + + {initialLetter} + + ); +} + +AppIcon.propTypes = { + name: PropTypes.string, + url: PropTypes.string, + color: PropTypes.string, + variant: PropTypes.oneOfType([ + PropTypes.oneOf(['circular', 'rounded', 'square']), + PropTypes.string, + ]), + sx: PropTypes.object, +}; + +export default AppIcon; diff --git a/packages/web/src/components/AppRow/index.jsx b/packages/web/src/components/AppRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6fb30490f25dd54a90d05795295a4bb6bcd80649 --- /dev/null +++ b/packages/web/src/components/AppRow/index.jsx @@ -0,0 +1,78 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import Card from '@mui/material/Card'; +import Box from '@mui/material/Box'; +import CardActionArea from '@mui/material/CardActionArea'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import useFormatMessage from 'hooks/useFormatMessage'; +import AppIcon from 'components/AppIcon'; +import { AppPropType } from 'propTypes/propTypes'; +import { CardContent, Typography } from './style'; + +const countTranslation = (value) => ( + <> + {value} +
+ +); + +function AppRow(props) { + const formatMessage = useFormatMessage(); + const { name, primaryColor, iconUrl, connectionCount, flowCount } = + props.application; + return ( + + + + + + + + + + {name} + + + + + {formatMessage('app.connectionCount', { + count: countTranslation(connectionCount || '-'), + })} + + + + + + {formatMessage('app.flowCount', { + count: countTranslation(flowCount || '-'), + })} + + + + + theme.palette.primary.main }} + /> + + + + + + ); +} + +AppRow.propTypes = { + application: AppPropType.isRequired, + url: PropTypes.string.isRequired, +}; + +export default AppRow; diff --git a/packages/web/src/components/AppRow/style.js b/packages/web/src/components/AppRow/style.js new file mode 100644 index 0000000000000000000000000000000000000000..05709cd985a956a7b1575d13388c943737c22967 --- /dev/null +++ b/packages/web/src/components/AppRow/style.js @@ -0,0 +1,22 @@ +import { styled } from '@mui/material/styles'; +import MuiCardContent from '@mui/material/CardContent'; +import MuiTypography from '@mui/material/Typography'; +export const CardContent = styled(MuiCardContent)(({ theme }) => ({ + display: 'grid', + gridTemplateRows: 'auto', + gridTemplateColumns: 'auto 1fr auto auto auto', + gridColumnGap: theme.spacing(2), + alignItems: 'center', +})); +export const Typography = styled(MuiTypography)(() => ({ + '&.MuiTypography-h6': { + textTransform: 'capitalize', + }, + textAlign: 'center', + display: 'inline-block', +})); +export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + display: 'none', + }, +})); diff --git a/packages/web/src/components/Can/index.jsx b/packages/web/src/components/Can/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..242eeb4b76675fa88e89d26c0037151efddff13b --- /dev/null +++ b/packages/web/src/components/Can/index.jsx @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import { Can as OriginalCan } from '@casl/react'; +import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; + +function Can(props) { + const currentUserAbility = useCurrentUserAbility(); + return ; +} + +Can.propTypes = { + I: PropTypes.string.isRequired, + a: PropTypes.string, + an: PropTypes.string, + passThrough: PropTypes.bool, + children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, +}; + +export default Can; diff --git a/packages/web/src/components/CheckoutCompletedAlert/index.ee.jsx b/packages/web/src/components/CheckoutCompletedAlert/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d236987a20b8df7df2d899619b286793768155dd --- /dev/null +++ b/packages/web/src/components/CheckoutCompletedAlert/index.ee.jsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { useLocation } from 'react-router-dom'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; + +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function CheckoutCompletedAlert() { + const formatMessage = useFormatMessage(); + const location = useLocation(); + const state = location.state; + const checkoutCompleted = state?.checkoutCompleted; + + if (!checkoutCompleted) return ; + + return ( + + + {formatMessage('checkoutCompletedAlert.text')} + + + ); +} diff --git a/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..17712178289dbb772c965451950ed5c7c1e0f465 --- /dev/null +++ b/packages/web/src/components/ChooseAppAndEventSubstep/index.jsx @@ -0,0 +1,274 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import ListItem from '@mui/material/ListItem'; +import TextField from '@mui/material/TextField'; +import Autocomplete from '@mui/material/Autocomplete'; +import Chip from '@mui/material/Chip'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import useApps from 'hooks/useApps'; +import { EditorContext } from 'contexts/Editor'; +import FlowSubstepTitle from 'components/FlowSubstepTitle'; +import { StepPropType, SubstepPropType } from 'propTypes/propTypes'; +import useTriggers from 'hooks/useTriggers'; +import useActions from 'hooks/useActions'; + +const optionGenerator = (app) => ({ + label: app.name, + value: app.key, +}); + +const eventOptionGenerator = (app) => ({ + label: app.name, + value: app.key, + type: app?.type, +}); + +const getOption = (options, selectedOptionValue) => + options.find((option) => option.value === selectedOptionValue); + +function ChooseAppAndEventSubstep(props) { + const { + substep, + expanded = false, + onExpand, + onCollapse, + step, + onSubmit, + onChange, + } = props; + const formatMessage = useFormatMessage(); + const editorContext = React.useContext(EditorContext); + const isTrigger = step.type === 'trigger'; + const isAction = step.type === 'action'; + const useAppsOptions = {}; + + if (isTrigger) { + useAppsOptions.onlyWithTriggers = true; + } + + if (isAction) { + useAppsOptions.onlyWithActions = true; + } + + const { data: apps } = useApps(useAppsOptions); + + const app = apps?.data?.find( + (currentApp) => currentApp?.key === step?.appKey, + ); + + const { data: triggers } = useTriggers(app?.key); + + const { data: actions } = useActions(app?.key); + + const appOptions = React.useMemo( + () => apps?.data?.map((app) => optionGenerator(app)) || [], + [apps?.data], + ); + + const actionsOrTriggers = (isTrigger ? triggers?.data : actions?.data) || []; + + const actionOrTriggerOptions = React.useMemo( + () => actionsOrTriggers.map((trigger) => eventOptionGenerator(trigger)), + [actionsOrTriggers], + ); + + const selectedActionOrTrigger = actionsOrTriggers.find( + (actionOrTrigger) => actionOrTrigger.key === step?.key, + ); + + const isWebhook = isTrigger && selectedActionOrTrigger?.type === 'webhook'; + + const { name } = substep; + + const valid = !!step.key && !!step.appKey; + + // placeholders + const onEventChange = React.useCallback( + (event, selectedOption) => { + if (typeof selectedOption === 'object') { + // TODO: try to simplify type casting below. + const typedSelectedOption = selectedOption; + const option = typedSelectedOption; + const eventKey = option?.value; + if (step.key !== eventKey) { + onChange({ + step: { + ...step, + key: eventKey, + }, + }); + } + } + }, + [step, onChange], + ); + + const onAppChange = React.useCallback( + (event, selectedOption) => { + if (typeof selectedOption === 'object') { + // TODO: try to simplify type casting below. + const typedSelectedOption = selectedOption; + const option = typedSelectedOption; + const appKey = option?.value; + if (step.appKey !== appKey) { + onChange({ + step: { + ...step, + key: '', + appKey, + parameters: {}, + }, + }); + } + } + }, + [step, onChange], + ); + + const onToggle = expanded ? onCollapse : onExpand; + + return ( + + + + + ( + + )} + value={getOption(appOptions, step.appKey) || null} + onChange={onAppChange} + data-test="choose-app-autocomplete" + componentsProps={{ popper: { className: 'nowheel' } }} + /> + + {step.appKey && ( + + + {isTrigger && formatMessage('flowEditor.triggerEvent')} + {!isTrigger && formatMessage('flowEditor.actionEvent')} + + + ( + + {isWebhook && ( + + )} + + {params.InputProps.endAdornment} + + ), + }} + /> + )} + renderOption={(optionProps, option) => ( +
  • + {option.label} + + {option.type === 'webhook' && ( + + )} +
  • + )} + value={getOption(actionOrTriggerOptions, step.key) || null} + onChange={onEventChange} + data-test="choose-event-autocomplete" + componentsProps={{ popper: { className: 'nowheel' } }} + /> + + )} + + {isTrigger && selectedActionOrTrigger?.pollInterval && ( + + )} + + + + +
    + ); +} + +ChooseAppAndEventSubstep.propTypes = { + substep: SubstepPropType.isRequired, + expanded: PropTypes.bool, + onExpand: PropTypes.func.isRequired, + onCollapse: PropTypes.func.isRequired, + step: StepPropType.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, +}; + +export default ChooseAppAndEventSubstep; diff --git a/packages/web/src/components/ChooseConnectionSubstep/index.jsx b/packages/web/src/components/ChooseConnectionSubstep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7b77670541f7425156a57462ec1c94c954a6df41 --- /dev/null +++ b/packages/web/src/components/ChooseConnectionSubstep/index.jsx @@ -0,0 +1,292 @@ +import PropTypes from 'prop-types'; +import Autocomplete from '@mui/material/Autocomplete'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import ListItem from '@mui/material/ListItem'; +import TextField from '@mui/material/TextField'; +import * as React from 'react'; + +import AddAppConnection from 'components/AddAppConnection'; +import AppAuthClientsDialog from 'components/AppAuthClientsDialog/index.ee'; +import FlowSubstepTitle from 'components/FlowSubstepTitle'; +import useAppConfig from 'hooks/useAppConfig.ee'; +import { EditorContext } from 'contexts/Editor'; +import useAuthenticateApp from 'hooks/useAuthenticateApp.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { + AppPropType, + StepPropType, + SubstepPropType, +} from 'propTypes/propTypes'; +import useStepConnection from 'hooks/useStepConnection'; +import { useQueryClient } from '@tanstack/react-query'; +import useAppConnections from 'hooks/useAppConnections'; +import useTestConnection from 'hooks/useTestConnection'; + +const ADD_CONNECTION_VALUE = 'ADD_CONNECTION'; +const ADD_SHARED_CONNECTION_VALUE = 'ADD_SHARED_CONNECTION'; + +const optionGenerator = (connection) => ({ + label: connection?.formattedData?.screenName ?? 'Unnamed', + value: connection?.id, +}); + +const getOption = (options, connectionId) => + options.find((connection) => connection.value === connectionId) || undefined; + +function ChooseConnectionSubstep(props) { + const { + substep, + expanded = false, + onExpand, + onCollapse, + step, + onSubmit, + onChange, + application, + } = props; + const { appKey } = step; + const formatMessage = useFormatMessage(); + const editorContext = React.useContext(EditorContext); + const [showAddConnectionDialog, setShowAddConnectionDialog] = + React.useState(false); + const [showAddSharedConnectionDialog, setShowAddSharedConnectionDialog] = + React.useState(false); + const queryClient = useQueryClient(); + + const { authenticate } = useAuthenticateApp({ + appKey: application.key, + useShared: true, + }); + + const { + data, + isLoading: isAppConnectionsLoading, + refetch, + } = useAppConnections(appKey); + + const { data: appConfig } = useAppConfig(application.key); + + const { data: stepConnectionData } = useStepConnection(step.id); + const stepConnection = stepConnectionData?.data; + + // TODO: show detailed error when connection test/verification fails + const { mutate: testConnection, isPending: isTestConnectionPending } = + useTestConnection({ + connectionId: stepConnection?.id, + }); + + React.useEffect(() => { + if (stepConnection?.id) { + testConnection({ + variables: { + id: stepConnection.id, + }, + }); + } + // intentionally no dependencies for initial test + }, []); + + const connectionOptions = React.useMemo(() => { + const appWithConnections = data?.data; + const options = + appWithConnections?.map((connection) => optionGenerator(connection)) || + []; + + if (!appConfig?.data || appConfig?.data?.canCustomConnect) { + options.push({ + label: formatMessage('chooseConnectionSubstep.addNewConnection'), + value: ADD_CONNECTION_VALUE, + }); + } + + if (appConfig?.data?.canConnect) { + options.push({ + label: formatMessage('chooseConnectionSubstep.addNewSharedConnection'), + value: ADD_SHARED_CONNECTION_VALUE, + }); + } + + return options; + }, [data, formatMessage, appConfig?.data]); + + const handleClientClick = async (appAuthClientId) => { + try { + const response = await authenticate?.({ + appAuthClientId, + }); + const connectionId = response?.createConnection.id; + + if (connectionId) { + await refetch(); + onChange({ + step: { + ...step, + connection: { + id: connectionId, + }, + }, + }); + } + } catch (err) { + // void + } finally { + setShowAddSharedConnectionDialog(false); + } + }; + + const { name } = substep; + + const handleAddConnectionClose = React.useCallback( + async (response) => { + setShowAddConnectionDialog(false); + const connectionId = response?.createConnection.id; + + if (connectionId) { + await refetch(); + onChange({ + step: { + ...step, + connection: { + id: connectionId, + }, + }, + }); + } + }, + [onChange, refetch, step], + ); + + const handleChange = React.useCallback( + async (event, selectedOption) => { + if (typeof selectedOption === 'object') { + // TODO: try to simplify type casting below. + const typedSelectedOption = selectedOption; + const option = typedSelectedOption; + const connectionId = option?.value; + + if (connectionId === ADD_CONNECTION_VALUE) { + setShowAddConnectionDialog(true); + return; + } + + if (connectionId === ADD_SHARED_CONNECTION_VALUE) { + setShowAddSharedConnectionDialog(true); + return; + } + + if (connectionId !== stepConnection?.id) { + onChange({ + step: { + ...step, + connection: { + id: connectionId, + }, + }, + }); + + await queryClient.invalidateQueries({ + queryKey: ['steps', step.id, 'connection'], + }); + } + } + }, + [step, onChange, queryClient], + ); + + React.useEffect(() => { + if (stepConnection?.id) { + testConnection({ + id: stepConnection?.id, + }); + } + }, [stepConnection?.id, testConnection]); + + const onToggle = expanded ? onCollapse : onExpand; + + return ( + + + + + ( + + )} + value={getOption(connectionOptions, stepConnection?.id)} + onChange={handleChange} + loading={isAppConnectionsLoading} + data-test="choose-connection-autocomplete" + componentsProps={{ popper: { className: 'nowheel' } }} + /> + + + + + + {application && showAddConnectionDialog && ( + + )} + + {application && showAddSharedConnectionDialog && ( + setShowAddSharedConnectionDialog(false)} + onClientClick={handleClientClick} + /> + )} + + ); +} + +ChooseConnectionSubstep.propTypes = { + application: AppPropType.isRequired, + substep: SubstepPropType.isRequired, + expanded: PropTypes.bool, + onExpand: PropTypes.func.isRequired, + onCollapse: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + step: StepPropType.isRequired, +}; + +export default ChooseConnectionSubstep; diff --git a/packages/web/src/components/ColorInput/ColorButton/index.jsx b/packages/web/src/components/ColorInput/ColorButton/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1dbf965bc88fbfe6062c17a6b151a42653d0f3ba --- /dev/null +++ b/packages/web/src/components/ColorInput/ColorButton/index.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button } from './style'; + +const BG_IMAGE_FALLBACK = + 'linear-gradient(45deg, #ccc 25%, transparent 25%), linear-gradient(135deg, #ccc 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #ccc 75%), linear-gradient(135deg, transparent 75%, #ccc 75%) /*! @noflip */'; + +const ColorButton = (props) => { + const { + bgColor, + className, + disablePopover, + isBgColorValid, + ...restButtonProps + } = props; + return ( + + )} + + {confirmButtonChildren && onConfirm && ( + + )} + + + ); +} + +ConfirmationDialog.propTypes = { + onClose: PropTypes.func.isRequired, + onConfirm: PropTypes.func.isRequired, + title: PropTypes.node.isRequired, + description: PropTypes.node.isRequired, + cancelButtonChildren: PropTypes.node.isRequired, + confirmButtonChildren: PropTypes.node.isRequired, + open: PropTypes.bool, + 'data-test': PropTypes.string, +}; + +export default ConfirmationDialog; diff --git a/packages/web/src/components/Container/index.jsx b/packages/web/src/components/Container/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ffafaa14826bf544921348dd290774fb779e3f0b --- /dev/null +++ b/packages/web/src/components/Container/index.jsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +import MuiContainer from '@mui/material/Container'; + +export default function Container(props) { + return ; +} + +Container.defaultProps = { + maxWidth: 'lg', +}; diff --git a/packages/web/src/components/ControlledAutocomplete/index.jsx b/packages/web/src/components/ControlledAutocomplete/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0ade223711f2744755065428a00c458d6c64265f --- /dev/null +++ b/packages/web/src/components/ControlledAutocomplete/index.jsx @@ -0,0 +1,137 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import FormHelperText from '@mui/material/FormHelperText'; +import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete'; +import Typography from '@mui/material/Typography'; + +const getOption = (options, value) => + options.find((option) => option.value === value) || null; + +// Enables filtering by value in autocomplete dropdown +const filterOptions = createFilterOptions({ + stringify: ({ label, value }) => ` + ${label} + ${value} + `, +}); + +function ControlledAutocomplete(props) { + const { control, watch, setValue, resetField } = useFormContext(); + const { + required = false, + name, + defaultValue, + shouldUnregister = false, + onBlur, + onChange, + description, + options = [], + dependsOn = [], + showOptionValue, + ...autocompleteProps + } = props; + let dependsOnValues = []; + + if (dependsOn?.length) { + dependsOnValues = watch(dependsOn); + } + + React.useEffect(() => { + const hasDependencies = dependsOnValues.length; + const allDepsSatisfied = dependsOnValues.every(Boolean); + if (hasDependencies && !allDepsSatisfied) { + // Reset the field if any dependency is not satisfied + setValue(name, null); + resetField(name); + } + }, dependsOnValues); + + return ( + ( +
    + {/* encapsulated with an element such as div to vertical spacing delegated from parent */} + { + const typedSelectedOption = selectedOption; + if ( + typedSelectedOption !== null && + Object.prototype.hasOwnProperty.call( + typedSelectedOption, + 'value', + ) + ) { + controllerOnChange(typedSelectedOption.value); + } else { + controllerOnChange(typedSelectedOption); + } + onChange?.(event, selectedOption, reason, details); + }} + onBlur={(...args) => { + controllerOnBlur(); + onBlur?.(...args); + }} + ref={ref} + data-test={`${name}-autocomplete`} + renderOption={(optionProps, option) => ( +
  • + {option.label} + + {showOptionValue && ( + {option.value} + )} +
  • + )} + /> + + + {fieldState.isTouched + ? fieldState.error?.message || description + : description} + +
    + )} + /> + ); +} + +ControlledAutocomplete.propTypes = { + shouldUnregister: PropTypes.bool, + name: PropTypes.string.isRequired, + required: PropTypes.bool, + showOptionValue: PropTypes.bool, + description: PropTypes.string, + dependsOn: PropTypes.arrayOf(PropTypes.string), + defaultValue: PropTypes.any, + onBlur: PropTypes.func, + onChange: PropTypes.func, + options: PropTypes.array, +}; + +export default ControlledAutocomplete; diff --git a/packages/web/src/components/ControlledCheckbox/index.jsx b/packages/web/src/components/ControlledCheckbox/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..95400479ba37e77126ccf3193a34f652dfa46543 --- /dev/null +++ b/packages/web/src/components/ControlledCheckbox/index.jsx @@ -0,0 +1,68 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { Controller, useFormContext } from 'react-hook-form'; +import Checkbox from '@mui/material/Checkbox'; + +function ControlledCheckbox(props) { + const { control } = useFormContext(); + const { + required, + name, + defaultValue = false, + disabled = false, + onBlur, + onChange, + dataTest, + ...checkboxProps + } = props; + return ( + { + return ( + { + controllerOnChange(...args); + onChange?.(...args); + }} + onBlur={(...args) => { + controllerOnBlur(); + onBlur?.(...args); + }} + inputRef={ref} + data-test={dataTest} + /> + ); + }} + /> + ); +} + +ControlledCheckbox.propTypes = { + name: PropTypes.string.isRequired, + defaultValue: PropTypes.bool, + dataTest: PropTypes.string, + required: PropTypes.bool, + disabled: PropTypes.bool, + onBlur: PropTypes.func, + onChange: PropTypes.func, +}; + +export default ControlledCheckbox; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/Controller.jsx b/packages/web/src/components/ControlledCustomAutocomplete/Controller.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3c10427ff588291d9b99501f637d1d52929d46e3 --- /dev/null +++ b/packages/web/src/components/ControlledCustomAutocomplete/Controller.jsx @@ -0,0 +1,34 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { Controller as RHFController, useFormContext } from 'react-hook-form'; + +function Controller(props) { + const { control } = useFormContext(); + const { + defaultValue = '', + name, + required, + shouldUnregister, + children, + } = props; + return ( + React.cloneElement(children, { field })} + /> + ); +} + +Controller.propTypes = { + defaultValue: PropTypes.string, + name: PropTypes.string.isRequired, + required: PropTypes.bool, + shouldUnregister: PropTypes.bool, + children: PropTypes.element.isRequired, +}; + +export default Controller; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bf3c3748add594793edcd2d698275354d5dffece --- /dev/null +++ b/packages/web/src/components/ControlledCustomAutocomplete/CustomOptions.jsx @@ -0,0 +1,95 @@ +import PropTypes from 'prop-types'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import Tab from '@mui/material/Tab'; +import * as React from 'react'; +import Suggestions from 'components/PowerInput/Suggestions'; +import TabPanel from 'components/TabPanel'; +import { FieldDropdownOptionPropType } from 'propTypes/propTypes'; +import Options from './Options'; +import { Tabs } from './style'; + +const CustomOptions = (props) => { + const { + open, + anchorEl, + data, + options = [], + onSuggestionClick, + onOptionClick, + onTabChange, + label, + initialTabIndex, + } = props; + const [activeTabIndex, setActiveTabIndex] = React.useState(undefined); + React.useEffect( + function applyInitialActiveTabIndex() { + setActiveTabIndex((currentActiveTabIndex) => { + if (currentActiveTabIndex === undefined) { + return initialTabIndex; + } + return currentActiveTabIndex; + }); + }, + [initialTabIndex], + ); + return ( + + + { + onTabChange(tabIndex); + setActiveTabIndex(tabIndex); + }} + > + + + + + + + + + + + + + + ); +}; + +CustomOptions.propTypes = { + open: PropTypes.bool.isRequired, + anchorEl: PropTypes.oneOfType([PropTypes.element, PropTypes.func]).isRequired, + data: PropTypes.arrayOf( + PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + output: PropTypes.arrayOf(PropTypes.object).isRequired, + }), + ).isRequired, + options: PropTypes.arrayOf(FieldDropdownOptionPropType).isRequired, + onSuggestionClick: PropTypes.func.isRequired, + onOptionClick: PropTypes.func.isRequired, + onTabChange: PropTypes.func.isRequired, + label: PropTypes.string, + initialTabIndex: PropTypes.oneOf([0, 1]), +}; + +export default CustomOptions; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/Options.jsx b/packages/web/src/components/ControlledCustomAutocomplete/Options.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2d1cb3c83991c4a9e6a3794a46c5ac6ebf4cbfe4 --- /dev/null +++ b/packages/web/src/components/ControlledCustomAutocomplete/Options.jsx @@ -0,0 +1,123 @@ +import PropTypes from 'prop-types'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import throttle from 'lodash/throttle'; +import * as React from 'react'; +import { FixedSizeList } from 'react-window'; +import { Typography } from '@mui/material'; +import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { SearchInputWrapper } from './style'; +import { FieldDropdownOptionPropType } from 'propTypes/propTypes'; + +const SHORT_LIST_LENGTH = 4; +const LIST_ITEM_HEIGHT = 64; + +const computeListHeight = (currentLength) => { + const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength); + return LIST_ITEM_HEIGHT * numberOfRenderedItems; +}; + +const renderItemFactory = + ({ onOptionClick }) => + (props) => { + const { index, style, data } = props; + const suboption = data[index]; + return ( + onOptionClick(event, suboption)} + data-test="power-input-suggestion-item" + key={index} + style={style} + > + + + ); + }; + +const Options = (props) => { + const formatMessage = useFormatMessage(); + const { data, onOptionClick } = props; + const [filteredData, setFilteredData] = React.useState(data); + React.useEffect( + function syncOptions() { + setFilteredData((filteredData) => { + if (filteredData.length === 0 && filteredData.length !== data.length) { + return data; + } + return filteredData; + }); + }, + [data], + ); + const renderItem = React.useMemo( + () => + renderItemFactory({ + onOptionClick, + }), + [onOptionClick], + ); + const onSearchChange = React.useMemo( + () => + throttle((event) => { + const search = event.target.value.toLowerCase(); + if (!search) { + setFilteredData(data); + return; + } + const newFilteredData = data.filter((option) => + `${option.label}\n${option.value}` + .toLowerCase() + .includes(search.toLowerCase()), + ); + setFilteredData(newFilteredData); + }, 400), + [data], + ); + return ( + <> + + + + + + {renderItem} + + + {filteredData.length === 0 && ( + theme.spacing(0, 0, 2, 2) }}> + {formatMessage('customAutocomplete.noOptions')} + + )} + + ); +}; + +Options.propTypes = { + data: PropTypes.arrayOf(FieldDropdownOptionPropType).isRequired, + onOptionClick: PropTypes.func.isRequired, +}; + +export default Options; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/index.jsx b/packages/web/src/components/ControlledCustomAutocomplete/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..874ee6a9affdd7060d8f5c83d2d89f8b20364910 --- /dev/null +++ b/packages/web/src/components/ControlledCustomAutocomplete/index.jsx @@ -0,0 +1,298 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { useController, useFormContext } from 'react-hook-form'; +import { IconButton } from '@mui/material'; +import FormHelperText from '@mui/material/FormHelperText'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import ClearIcon from '@mui/icons-material/Clear'; +import { ActionButtonsWrapper } from './style'; +import ClickAwayListener from '@mui/base/ClickAwayListener'; +import InputLabel from '@mui/material/InputLabel'; +import { createEditor } from 'slate'; +import { Editable, ReactEditor } from 'slate-react'; +import Slate from 'components/Slate'; +import Element from 'components/Slate/Element'; +import { + serialize, + deserialize, + insertVariable, + customizeEditor, + resetEditor, + overrideEditorValue, + focusEditor, +} from 'components/Slate/utils'; +import { + FakeInput, + InputLabelWrapper, + ChildrenWrapper, +} from 'components/PowerInput/style'; +import CustomOptions from './CustomOptions'; +import { processStepWithExecutions } from 'components/PowerInput/data'; +import { StepExecutionsContext } from 'contexts/StepExecutions'; + +function ControlledCustomAutocomplete(props) { + const { + defaultValue = '', + name, + label, + required, + options = [], + dependsOn = [], + description, + loading, + disabled, + shouldUnregister, + } = props; + const { control, watch } = useFormContext(); + const { field, fieldState } = useController({ + control, + name, + defaultValue, + rules: { required }, + shouldUnregister, + }); + const { + value, + onChange: controllerOnChange, + onBlur: controllerOnBlur, + } = field; + const [, forceUpdate] = React.useReducer((x) => x + 1, 0); + const [isInitialValueSet, setInitialValue] = React.useState(false); + const [isSingleChoice, setSingleChoice] = React.useState(undefined); + const priorStepsWithExecutions = React.useContext(StepExecutionsContext); + const editorRef = React.useRef(null); + + const renderElement = React.useCallback( + (props) => , + [disabled], + ); + + const [editor] = React.useState(() => customizeEditor(createEditor())); + + const [showVariableSuggestions, setShowVariableSuggestions] = + React.useState(false); + let dependsOnValues = []; + if (dependsOn?.length) { + dependsOnValues = watch(dependsOn); + } + + React.useEffect(() => { + const ref = ReactEditor.toDOMNode(editor, editor); + resizeObserver.observe(ref); + return () => resizeObserver.unobserve(ref); + }, []); + + const promoteValue = () => { + const serializedValue = serialize(editor.children); + controllerOnChange(serializedValue); + }; + + const resizeObserver = React.useMemo(function syncCustomOptionsPosition() { + return new ResizeObserver(() => { + forceUpdate(); + }); + }, []); + + React.useEffect(() => { + const hasDependencies = dependsOnValues.length; + if (hasDependencies) { + // Reset the field when a dependent has been updated + resetEditor(editor); + } + }, dependsOnValues); + + React.useEffect( + function updateInitialValue() { + const hasOptions = options.length; + const isOptionsLoaded = loading === false; + if (!isInitialValueSet && hasOptions && isOptionsLoaded) { + setInitialValue(true); + const option = options.find((option) => option.value === value); + if (option) { + overrideEditorValue(editor, { option, focus: false }); + setSingleChoice(true); + } else if (value) { + setSingleChoice(false); + } + } + }, + [isInitialValueSet, options, loading], + ); + + React.useEffect(() => { + if (!showVariableSuggestions && value !== serialize(editor.children)) { + promoteValue(); + } + }, [showVariableSuggestions]); + + const hideSuggestionsOnShift = (event) => { + if (event.code === 'Tab') { + setShowVariableSuggestions(false); + } + }; + + const handleKeyDown = (event) => { + hideSuggestionsOnShift(event); + if (event.code === 'Tab') { + promoteValue(); + } + if (isSingleChoice && event.code !== 'Tab') { + event.preventDefault(); + } + }; + + const stepsWithVariables = React.useMemo(() => { + return processStepWithExecutions(priorStepsWithExecutions); + }, [priorStepsWithExecutions]); + + const handleVariableSuggestionClick = React.useCallback( + (variable) => { + insertVariable(editor, variable, stepsWithVariables); + }, + [stepsWithVariables], + ); + + const handleOptionClick = React.useCallback( + (event, option) => { + event.stopPropagation(); + overrideEditorValue(editor, { option, focus: false }); + setShowVariableSuggestions(false); + setSingleChoice(true); + }, + [stepsWithVariables], + ); + + const handleClearButtonClick = (event) => { + event.stopPropagation(); + resetEditor(editor); + promoteValue(); + setSingleChoice(undefined); + }; + + const reset = (tabIndex) => { + const isOptions = tabIndex === 0; + setSingleChoice(isOptions); + resetEditor(editor, { focus: true }); + }; + + return ( + + setShowVariableSuggestions(false)} + > + {/* ref-able single child for ClickAwayListener */} + + { + focusEditor(editor); + }} + > + + + {`${label}${required ? ' *' : ''}`} + + + + { + setShowVariableSuggestions(true); + }} + onBlur={() => { + controllerOnBlur(); + }} + /> + + + {isSingleChoice && serialize(editor.children) !== '' && ( + + + + )} + + + + + + {/* ghost placer for the variables popover */} +
    + + + + + {fieldState.isTouched + ? fieldState.error?.message || description + : description} + + + + + ); +} + +ControlledCustomAutocomplete.propTypes = { + options: PropTypes.array, + loading: PropTypes.bool.isRequired, + showOptionValue: PropTypes.bool, + dependsOn: PropTypes.arrayOf(PropTypes.string), + defaultValue: PropTypes.string, + name: PropTypes.string.isRequired, + label: PropTypes.string, + type: PropTypes.string, + required: PropTypes.bool, + readOnly: PropTypes.bool, + description: PropTypes.string, + docUrl: PropTypes.string, + clickToCopy: PropTypes.bool, + disabled: PropTypes.bool, + shouldUnregister: PropTypes.bool, +}; + +export default ControlledCustomAutocomplete; diff --git a/packages/web/src/components/ControlledCustomAutocomplete/style.js b/packages/web/src/components/ControlledCustomAutocomplete/style.js new file mode 100644 index 0000000000000000000000000000000000000000..42a1e00734a379d7a7c7242d60ed15d7ea35a9df --- /dev/null +++ b/packages/web/src/components/ControlledCustomAutocomplete/style.js @@ -0,0 +1,15 @@ +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; +import MuiTabs from '@mui/material/Tabs'; +export const ActionButtonsWrapper = styled(Stack)` + position: absolute; + right: 0; + top: 50%; + transform: translateY(-50%); +`; +export const Tabs = styled(MuiTabs)` + border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; +`; +export const SearchInputWrapper = styled('div')` + padding: ${({ theme }) => theme.spacing(0, 2, 2, 2)}; +`; diff --git a/packages/web/src/components/CustomLogo/index.ee.jsx b/packages/web/src/components/CustomLogo/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1c0acc0fa44e541bb7bef983d72a77c64baf96d5 --- /dev/null +++ b/packages/web/src/components/CustomLogo/index.ee.jsx @@ -0,0 +1,20 @@ +import useAutomatischConfig from 'hooks/useAutomatischConfig'; +import { LogoImage } from './style.ee'; + +const CustomLogo = () => { + const { data: configData, isLoading } = useAutomatischConfig(); + const config = configData?.data; + + if (isLoading || !config?.['logo.svgData']) return null; + + const logoSvgData = config['logo.svgData']; + + return ( + + ); +}; + +export default CustomLogo; diff --git a/packages/web/src/components/CustomLogo/style.ee.js b/packages/web/src/components/CustomLogo/style.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..1e156e6e13162ad3383e130cbf31eab6fb000c5e --- /dev/null +++ b/packages/web/src/components/CustomLogo/style.ee.js @@ -0,0 +1,7 @@ +import styled from '@emotion/styled'; +export const LogoImage = styled('img')(() => ({ + maxWidth: 200, + maxHeight: 22, + width: '100%', + height: 'auto', +})); diff --git a/packages/web/src/components/DefaultLogo/index.jsx b/packages/web/src/components/DefaultLogo/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..82795db0edea8ef80cbae965b86ab2331b25b8d6 --- /dev/null +++ b/packages/web/src/components/DefaultLogo/index.jsx @@ -0,0 +1,21 @@ +import Typography from '@mui/material/Typography'; +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import MationLogo from 'components/MationLogo'; +import useAutomatischInfo from 'hooks/useAutomatischInfo'; + +const DefaultLogo = () => { + const { data: automatischInfo, isPending } = useAutomatischInfo(); + const isMation = automatischInfo?.data.isMation; + + if (isPending) return ; + if (isMation) return ; + + return ( + + + + ); +}; + +export default DefaultLogo; diff --git a/packages/web/src/components/DeleteAccountDialog/index.ee.jsx b/packages/web/src/components/DeleteAccountDialog/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8d7cead4072db02fdb6dcf9359cf828db0168610 --- /dev/null +++ b/packages/web/src/components/DeleteAccountDialog/index.ee.jsx @@ -0,0 +1,46 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; + +import * as URLS from 'config/urls'; +import ConfirmationDialog from 'components/ConfirmationDialog'; +import apolloClient from 'graphql/client'; +import { DELETE_CURRENT_USER } from 'graphql/mutations/delete-current-user.ee'; +import useAuthentication from 'hooks/useAuthentication'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useCurrentUser from 'hooks/useCurrentUser'; + +function DeleteAccountDialog(props) { + const [deleteCurrentUser] = useMutation(DELETE_CURRENT_USER); + const formatMessage = useFormatMessage(); + const { data } = useCurrentUser(); + const currentUser = data?.data; + + const authentication = useAuthentication(); + const navigate = useNavigate(); + + const handleConfirm = React.useCallback(async () => { + await deleteCurrentUser(); + authentication.removeToken(); + await apolloClient.clearStore(); + navigate(URLS.LOGIN); + }, [deleteCurrentUser, currentUser]); + + return ( + + ); +} + +DeleteAccountDialog.propTypes = { + onClose: PropTypes.func.isRequired, +}; + +export default DeleteAccountDialog; diff --git a/packages/web/src/components/DeleteRoleButton/index.ee.jsx b/packages/web/src/components/DeleteRoleButton/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9ce25473c2116fa1962a7b928942afbb61de862b --- /dev/null +++ b/packages/web/src/components/DeleteRoleButton/index.ee.jsx @@ -0,0 +1,75 @@ +import PropTypes from 'prop-types'; +import { useMutation } from '@apollo/client'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +import Can from 'components/Can'; +import ConfirmationDialog from 'components/ConfirmationDialog'; +import { DELETE_ROLE } from 'graphql/mutations/delete-role.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function DeleteRoleButton(props) { + const { disabled, roleId } = props; + const [showConfirmation, setShowConfirmation] = React.useState(false); + const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); + const queryClient = useQueryClient(); + + const [deleteRole] = useMutation(DELETE_ROLE, { + variables: { input: { id: roleId } }, + }); + + const handleConfirm = React.useCallback(async () => { + try { + await deleteRole(); + queryClient.invalidateQueries({ queryKey: ['admin', 'roles'] }); + setShowConfirmation(false); + enqueueSnackbar(formatMessage('deleteRoleButton.successfullyDeleted'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-delete-role-success', + }, + }); + } catch (error) { + throw new Error('Failed while deleting!'); + } + }, [deleteRole, enqueueSnackbar, formatMessage, queryClient]); + + return ( + <> + + {(allowed) => ( + setShowConfirmation(true)} + size="small" + data-test="role-delete" + > + + + )} + + + setShowConfirmation(false)} + onConfirm={handleConfirm} + cancelButtonChildren={formatMessage('deleteRoleButton.cancel')} + confirmButtonChildren={formatMessage('deleteRoleButton.confirm')} + data-test="delete-role-modal" + /> + + ); +} + +DeleteRoleButton.propTypes = { + disabled: PropTypes.bool, + roleId: PropTypes.string.isRequired, +}; + +export default DeleteRoleButton; diff --git a/packages/web/src/components/DeleteUserButton/index.ee.jsx b/packages/web/src/components/DeleteUserButton/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7a28c7e2b0dcbce06314fd9a8cf85d1b701c79b7 --- /dev/null +++ b/packages/web/src/components/DeleteUserButton/index.ee.jsx @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import { useMutation } from '@apollo/client'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import { useQueryClient } from '@tanstack/react-query'; + +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import ConfirmationDialog from 'components/ConfirmationDialog'; +import { DELETE_USER } from 'graphql/mutations/delete-user.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function DeleteUserButton(props) { + const { userId } = props; + const [showConfirmation, setShowConfirmation] = React.useState(false); + const [deleteUser] = useMutation(DELETE_USER, { + variables: { input: { id: userId } }, + refetchQueries: ['GetUsers'], + }); + const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); + const queryClient = useQueryClient(); + + const handleConfirm = React.useCallback(async () => { + try { + await deleteUser(); + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + setShowConfirmation(false); + enqueueSnackbar(formatMessage('deleteUserButton.successfullyDeleted'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-delete-user-success', + }, + }); + } catch (error) { + throw new Error('Failed while deleting!'); + } + }, [deleteUser]); + + return ( + <> + setShowConfirmation(true)} + size="small" + > + + + + setShowConfirmation(false)} + onConfirm={handleConfirm} + cancelButtonChildren={formatMessage('deleteUserButton.cancel')} + confirmButtonChildren={formatMessage('deleteUserButton.confirm')} + data-test="delete-user-modal" + /> + + ); +} + +DeleteUserButton.propTypes = { + userId: PropTypes.string.isRequired, +}; + +export default DeleteUserButton; diff --git a/packages/web/src/components/Drawer/index.jsx b/packages/web/src/components/Drawer/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e124d56fa0e24cbc66c895981bf2e1ec04fe755c --- /dev/null +++ b/packages/web/src/components/Drawer/index.jsx @@ -0,0 +1,92 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { useTheme } from '@mui/material/styles'; +import Toolbar from '@mui/material/Toolbar'; +import List from '@mui/material/List'; +import Divider from '@mui/material/Divider'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Badge from '@mui/material/Badge'; +import ListItemLink from 'components/ListItemLink'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { Drawer as BaseDrawer } from './style'; + +const iOS = + typeof navigator !== 'undefined' && + /iPad|iPhone|iPod/.test(navigator.userAgent); + +function Drawer(props) { + const { links = [], bottomLinks = [], ...drawerProps } = props; + const theme = useTheme(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md')); + const formatMessage = useFormatMessage(); + const closeOnClick = (event) => { + if (matchSmallScreens) { + props.onClose(event); + } + }; + return ( + + {/* keep the following encapsulating `div` to have `space-between` children */} +
    + + + + {links.map(({ Icon, primary, to, dataTest }, index) => ( + } + primary={formatMessage(primary)} + to={to} + onClick={closeOnClick} + data-test={dataTest} + /> + ))} + + + +
    + + + {bottomLinks.map( + ({ Icon, badgeContent, primary, to, dataTest, target }, index) => ( + + + + } + primary={primary} + to={to} + onClick={closeOnClick} + target={target} + data-test={dataTest} + /> + ), + )} + +
    + ); +} + +const DrawerLinkPropTypes = PropTypes.shape({ + Icon: PropTypes.elementType.isRequired, + primary: PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + target: PropTypes.oneOf(['_blank']), + badgeContent: PropTypes.node, + dataTest: PropTypes.string, +}); + +Drawer.propTypes = { + links: PropTypes.arrayOf(DrawerLinkPropTypes).isRequired, + bottomLinks: PropTypes.arrayOf(DrawerLinkPropTypes), + onClose: PropTypes.func.isRequired, +}; + +export default Drawer; diff --git a/packages/web/src/components/Drawer/style.js b/packages/web/src/components/Drawer/style.js new file mode 100644 index 0000000000000000000000000000000000000000..c64a5ddc21e1f0c307de4d89839f5779838a4ab9 --- /dev/null +++ b/packages/web/src/components/Drawer/style.js @@ -0,0 +1,48 @@ +import { styled } from '@mui/material/styles'; +import { drawerClasses } from '@mui/material/Drawer'; +import MuiSwipeableDrawer from '@mui/material/SwipeableDrawer'; +const drawerWidth = 300; +const openedMixin = (theme) => ({ + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + overflowX: 'hidden', + width: '100vw', + [theme.breakpoints.up('sm')]: { + width: drawerWidth, + }, +}); +const closedMixin = (theme) => ({ + transition: theme.transitions.create('width', { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.leavingScreen, + }), + overflowX: 'hidden', + width: 0, + [theme.breakpoints.up('sm')]: { + width: `calc(${theme.spacing(9)} + 1px)`, + }, +}); +export const Drawer = styled(MuiSwipeableDrawer)(({ theme, open }) => ({ + width: drawerWidth, + flexShrink: 0, + whiteSpace: 'nowrap', + boxSizing: 'border-box', + ...(open && { + ...openedMixin(theme), + [`& .${drawerClasses.paper}`]: { + ...openedMixin(theme), + display: 'flex', + justifyContent: 'space-between', + }, + }), + ...(!open && { + ...closedMixin(theme), + [`& .${drawerClasses.paper}`]: { + ...closedMixin(theme), + display: 'flex', + justifyContent: 'space-between', + }, + }), +})); diff --git a/packages/web/src/components/DynamicField/index.jsx b/packages/web/src/components/DynamicField/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..03eb8d2ed04ef53b879e7947a2bc3c84b4799fa7 --- /dev/null +++ b/packages/web/src/components/DynamicField/index.jsx @@ -0,0 +1,132 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { useFormContext, useWatch } from 'react-hook-form'; +import Typography from '@mui/material/Typography'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; +import InputCreator from 'components/InputCreator'; +import { EditorContext } from 'contexts/Editor'; +import { FieldsPropType } from 'propTypes/propTypes'; + +function DynamicField(props) { + const { label, description, fields, name, defaultValue, stepId } = props; + const { control, setValue, getValues } = useFormContext(); + const fieldsValue = useWatch({ control, name }); + const editorContext = React.useContext(EditorContext); + const createEmptyItem = React.useCallback(() => { + return fields.reduce((previousValue, field) => { + return { + ...previousValue, + [field.key]: '', + __id: uuidv4(), + }; + }, {}); + }, [fields]); + const addItem = React.useCallback(() => { + const values = getValues(name); + if (!values) { + setValue(name, [createEmptyItem()]); + } else { + setValue(name, values.concat(createEmptyItem())); + } + }, [getValues, createEmptyItem]); + const removeItem = React.useCallback( + (index) => { + if (fieldsValue.length === 1) return; + const newFieldsValue = fieldsValue.filter( + (fieldValue, fieldIndex) => fieldIndex !== index, + ); + setValue(name, newFieldsValue); + }, + [fieldsValue], + ); + React.useEffect( + function addInitialGroupWhenEmpty() { + const fieldValues = getValues(name); + if (!fieldValues && defaultValue) { + setValue(name, defaultValue); + } else if (!fieldValues) { + setValue(name, [createEmptyItem()]); + } + }, + [createEmptyItem, defaultValue], + ); + return ( + + {label} + + {fieldsValue?.map((field, index) => ( + + + {fields.map((fieldSchema, fieldSchemaIndex) => ( + + + + ))} + + + removeItem(index)} + sx={{ width: 61, height: 61 }} + > + + + + ))} + + + + + + + + + + {description} + + ); +} + +DynamicField.propTypes = { + onChange: PropTypes.func, + onBlur: PropTypes.func, + defaultValue: PropTypes.arrayOf(PropTypes.object), + name: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + type: PropTypes.string, + required: PropTypes.bool, + readOnly: PropTypes.bool, + description: PropTypes.string, + docUrl: PropTypes.string, + clickToCopy: PropTypes.bool, + disabled: PropTypes.bool, + fields: FieldsPropType.isRequired, + shouldUnregister: PropTypes.bool, + stepId: PropTypes.string, +}; + +export default DynamicField; diff --git a/packages/web/src/components/EditableTypography/index.jsx b/packages/web/src/components/EditableTypography/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b30fcb1e07af735be3fe5729cc2a0cf2c1c8eb50 --- /dev/null +++ b/packages/web/src/components/EditableTypography/index.jsx @@ -0,0 +1,68 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import EditIcon from '@mui/icons-material/Edit'; +import { Box, TextField } from './style'; + +const noop = () => null; + +function EditableTypography(props) { + const { children, onConfirm = noop, sx, ...typographyProps } = props; + const [editing, setEditing] = React.useState(false); + const handleClick = React.useCallback(() => { + setEditing((editing) => !editing); + }, []); + const handleTextFieldClick = React.useCallback((event) => { + event.stopPropagation(); + }, []); + const handleTextFieldKeyDown = React.useCallback( + async (event) => { + const target = event.target; + if (event.key === 'Enter') { + if (target.value !== children) { + await onConfirm(target.value); + } + setEditing(false); + } + }, + [children], + ); + const handleTextFieldBlur = React.useCallback( + async (event) => { + const value = event.target.value; + if (value !== children) { + await onConfirm(value); + } + setEditing(false); + }, + [onConfirm, children], + ); + let component = {children}; + if (editing) { + component = ( + + ); + } + return ( + + + + {component} + + ); +} + +EditableTypography.propTypes = { + children: PropTypes.string.isRequired, + onConfirm: PropTypes.func, + sx: PropTypes.object, +}; + +export default EditableTypography; diff --git a/packages/web/src/components/EditableTypography/style.js b/packages/web/src/components/EditableTypography/style.js new file mode 100644 index 0000000000000000000000000000000000000000..8e11fd83a084198cbe398f6d3c4f9659bb465022 --- /dev/null +++ b/packages/web/src/components/EditableTypography/style.js @@ -0,0 +1,22 @@ +import { styled } from '@mui/material/styles'; +import MuiBox from '@mui/material/Box'; +import MuiTextField from '@mui/material/TextField'; +import { inputClasses } from '@mui/material/Input'; +const boxShouldForwardProp = (prop) => !['editing'].includes(prop); +export const Box = styled(MuiBox, { + shouldForwardProp: boxShouldForwardProp, +})` + display: flex; + flex: 1; + width: 300px; + height: 33px; + align-items: center; + ${({ editing }) => editing && 'border-bottom: 1px dashed #000;'} +`; +export const TextField = styled(MuiTextField)({ + width: '100%', + [`.${inputClasses.root}:before, .${inputClasses.root}:after, .${inputClasses.root}:hover`]: + { + borderBottom: '0 !important', + }, +}); diff --git a/packages/web/src/components/Editor/index.jsx b/packages/web/src/components/Editor/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0995d3004c25f8aec43001c87d1082bdc5cf648b --- /dev/null +++ b/packages/web/src/components/Editor/index.jsx @@ -0,0 +1,118 @@ +import * as React from 'react'; +import { useMutation } from '@apollo/client'; +import Box from '@mui/material/Box'; +import IconButton from '@mui/material/IconButton'; +import AddIcon from '@mui/icons-material/Add'; + +import { CREATE_STEP } from 'graphql/mutations/create-step'; +import { UPDATE_STEP } from 'graphql/mutations/update-step'; +import FlowStep from 'components/FlowStep'; +import { FlowPropType } from 'propTypes/propTypes'; +import { useQueryClient } from '@tanstack/react-query'; + +function Editor(props) { + const [updateStep] = useMutation(UPDATE_STEP); + const [createStep, { loading: creationInProgress }] = + useMutation(CREATE_STEP); + const { flow } = props; + const [triggerStep] = flow.steps; + const [currentStepId, setCurrentStepId] = React.useState(triggerStep.id); + const queryClient = useQueryClient(); + + const onStepChange = React.useCallback( + async (step) => { + const mutationInput = { + id: step.id, + key: step.key, + parameters: step.parameters, + connection: { + id: step.connection?.id, + }, + flow: { + id: flow.id, + }, + }; + + if (step.appKey) { + mutationInput.appKey = step.appKey; + } + + await updateStep({ variables: { input: mutationInput } }); + await queryClient.invalidateQueries({ + queryKey: ['steps', step.id, 'connection'], + }); + await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] }); + }, + [updateStep, flow.id, queryClient], + ); + + const addStep = React.useCallback( + async (previousStepId) => { + const mutationInput = { + previousStep: { + id: previousStepId, + }, + flow: { + id: flow.id, + }, + }; + + const createdStep = await createStep({ + variables: { input: mutationInput }, + }); + + const createdStepId = createdStep.data.createStep.id; + setCurrentStepId(createdStepId); + await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] }); + }, + [createStep, flow.id, queryClient], + ); + + const openNextStep = React.useCallback((nextStep) => { + return () => { + setCurrentStepId(nextStep?.id); + }; + }, []); + + return ( + + {flow?.steps?.map((step, index, steps) => ( + + setCurrentStepId(step.id)} + onClose={() => setCurrentStepId(null)} + onChange={onStepChange} + flowId={flow.id} + onContinue={openNextStep(steps[index + 1])} + /> + + addStep(step.id)} + color="primary" + disabled={creationInProgress || flow.active} + > + + + + ))} + + ); +} + +Editor.propTypes = { + flow: FlowPropType.isRequired, +}; + +export default Editor; diff --git a/packages/web/src/components/EditorLayout/index.jsx b/packages/web/src/components/EditorLayout/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..75cf0cb1cb77e3a51a6ad08bf2126303fa4125ff --- /dev/null +++ b/packages/web/src/components/EditorLayout/index.jsx @@ -0,0 +1,180 @@ +import * as React from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Tooltip from '@mui/material/Tooltip'; +import IconButton from '@mui/material/IconButton'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import Snackbar from '@mui/material/Snackbar'; +import { ReactFlowProvider } from 'reactflow'; + +import { EditorProvider } from 'contexts/Editor'; +import EditableTypography from 'components/EditableTypography'; +import Container from 'components/Container'; +import Editor from 'components/Editor'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { UPDATE_FLOW_STATUS } from 'graphql/mutations/update-flow-status'; +import { UPDATE_FLOW } from 'graphql/mutations/update-flow'; +import * as URLS from 'config/urls'; +import { TopBar } from './style'; +import useFlow from 'hooks/useFlow'; +import { useQueryClient } from '@tanstack/react-query'; +import EditorNew from 'components/EditorNew/EditorNew'; + +const useNewFlowEditor = process.env.REACT_APP_USE_NEW_FLOW_EDITOR === 'true'; + +export default function EditorLayout() { + const { flowId } = useParams(); + const formatMessage = useFormatMessage(); + const [updateFlow] = useMutation(UPDATE_FLOW); + const [updateFlowStatus] = useMutation(UPDATE_FLOW_STATUS); + const { data, isLoading: isFlowLoading } = useFlow(flowId); + const flow = data?.data; + const queryClient = useQueryClient(); + + const onFlowNameUpdate = React.useCallback( + async (name) => { + await updateFlow({ + variables: { + input: { + id: flowId, + name, + }, + }, + optimisticResponse: { + updateFlow: { + __typename: 'Flow', + id: flowId, + name, + }, + }, + }); + + await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); + }, + [flowId, queryClient], + ); + + const onFlowStatusUpdate = React.useCallback( + async (active) => { + await updateFlowStatus({ + variables: { + input: { + id: flowId, + active, + }, + }, + optimisticResponse: { + updateFlowStatus: { + __typename: 'Flow', + id: flowId, + active, + }, + }, + }); + + await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); + }, + [flowId, queryClient], + ); + + return ( + <> + + + + + + + + + {!isFlowLoading && ( + + {flow?.name} + + )} + + + + + + + + {useNewFlowEditor ? ( + + + + + {!flow && !isFlowLoading && 'not found'} + {flow && } + + + + + ) : ( + + + + {!flow && !isFlowLoading && 'not found'} + {flow && } + + + + )} + + onFlowStatusUpdate(!flow.active)} + data-test="unpublish-flow-from-snackbar" + > + {formatMessage('flowEditor.unpublish')} + + } + /> + + ); +} diff --git a/packages/web/src/components/EditorLayout/style.js b/packages/web/src/components/EditorLayout/style.js new file mode 100644 index 0000000000000000000000000000000000000000..0f2cf0eb5412362a67be70e538c8ef424ed9240b --- /dev/null +++ b/packages/web/src/components/EditorLayout/style.js @@ -0,0 +1,10 @@ +import { styled } from '@mui/material/styles'; +import Stack from '@mui/material/Stack'; + +export const TopBar = styled(Stack)(({ theme }) => ({ + zIndex: theme.zIndex.appBar, + position: 'sticky', + top: 0, + left: 0, + right: 0, +})); diff --git a/packages/web/src/components/EditorNew/Edge/Edge.jsx b/packages/web/src/components/EditorNew/Edge/Edge.jsx new file mode 100644 index 0000000000000000000000000000000000000000..980e80dbfb8b9fc9e2fccff186fc981cd094f23d --- /dev/null +++ b/packages/web/src/components/EditorNew/Edge/Edge.jsx @@ -0,0 +1,79 @@ +import { EdgeLabelRenderer, getStraightPath } from 'reactflow'; +import IconButton from '@mui/material/IconButton'; +import AddIcon from '@mui/icons-material/Add'; +import { useMutation } from '@apollo/client'; +import { CREATE_STEP } from 'graphql/mutations/create-step'; +import { useQueryClient } from '@tanstack/react-query'; +import PropTypes from 'prop-types'; + +export default function Edge({ + sourceX, + sourceY, + targetX, + targetY, + source, + data: { flowId, setCurrentStepId, flowActive, layouted }, +}) { + const [createStep, { loading: creationInProgress }] = + useMutation(CREATE_STEP); + const queryClient = useQueryClient(); + const [edgePath, labelX, labelY] = getStraightPath({ + sourceX, + sourceY, + targetX, + targetY, + }); + + const addStep = async (previousStepId) => { + const mutationInput = { + previousStep: { + id: previousStepId, + }, + flow: { + id: flowId, + }, + }; + + const createdStep = await createStep({ + variables: { input: mutationInput }, + }); + + const createdStepId = createdStep.data.createStep.id; + setCurrentStepId(createdStepId); + await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); + }; + + return ( + <> + + addStep(source)} + color="primary" + sx={{ + position: 'absolute', + transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`, + pointerEvents: 'all', + visibility: layouted ? 'visible' : 'hidden', + }} + disabled={creationInProgress || flowActive} + > + + + + + ); +} + +Edge.propTypes = { + sourceX: PropTypes.number.isRequired, + sourceY: PropTypes.number.isRequired, + targetX: PropTypes.number.isRequired, + targetY: PropTypes.number.isRequired, + source: PropTypes.string.isRequired, + data: PropTypes.shape({ + flowId: PropTypes.string.isRequired, + setCurrentStepId: PropTypes.func.isRequired, + flowActive: PropTypes.bool.isRequired, + layouted: PropTypes.bool, + }).isRequired, +}; diff --git a/packages/web/src/components/EditorNew/EditorNew.jsx b/packages/web/src/components/EditorNew/EditorNew.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5b6fea8380573aecc6c1f702a66f4cbeef52b95a --- /dev/null +++ b/packages/web/src/components/EditorNew/EditorNew.jsx @@ -0,0 +1,258 @@ +import { useEffect, useState, useCallback } from 'react'; +import { useMutation } from '@apollo/client'; +import { useQueryClient } from '@tanstack/react-query'; +import { FlowPropType } from 'propTypes/propTypes'; +import ReactFlow, { useNodesState, useEdgesState, addEdge } from 'reactflow'; +import 'reactflow/dist/style.css'; +import { UPDATE_STEP } from 'graphql/mutations/update-step'; + +import { useAutoLayout } from './useAutoLayout'; +import { useScrollBoundries } from './useScrollBoundries'; +import FlowStepNode from './FlowStepNode/FlowStepNode'; +import Edge from './Edge/Edge'; +import InvisibleNode from './InvisibleNode/InvisibleNode'; +import { EditorWrapper } from './style'; + +const nodeTypes = { flowStep: FlowStepNode, invisible: InvisibleNode }; + +const edgeTypes = { + addNodeEdge: Edge, +}; + +const INVISIBLE_NODE_ID = 'invisible-node'; + +const generateEdgeId = (sourceId, targetId) => `${sourceId}-${targetId}`; + +const EditorNew = ({ flow }) => { + const [triggerStep] = flow.steps; + const [currentStepId, setCurrentStepId] = useState(triggerStep.id); + + const [updateStep] = useMutation(UPDATE_STEP); + const queryClient = useQueryClient(); + + const [nodes, setNodes, onNodesChange] = useNodesState([]); + const [edges, setEdges, onEdgesChange] = useEdgesState([]); + useAutoLayout(); + useScrollBoundries(); + + const onConnect = useCallback( + (params) => setEdges((eds) => addEdge(params, eds)), + [setEdges], + ); + + const openNextStep = useCallback( + (nextStep) => () => { + setCurrentStepId(nextStep?.id); + }, + [], + ); + + const onStepChange = useCallback( + async (step) => { + const mutationInput = { + id: step.id, + key: step.key, + parameters: step.parameters, + connection: { + id: step.connection?.id, + }, + flow: { + id: flow.id, + }, + }; + + if (step.appKey) { + mutationInput.appKey = step.appKey; + } + + await updateStep({ + variables: { input: mutationInput }, + }); + await queryClient.invalidateQueries({ + queryKey: ['steps', step.id, 'connection'], + }); + await queryClient.invalidateQueries({ queryKey: ['flows', flow.id] }); + }, + [flow.id, updateStep, queryClient], + ); + + const generateEdges = useCallback((flow, prevEdges) => { + const newEdges = + flow.steps + .map((step, i) => { + const sourceId = step.id; + const targetId = flow.steps[i + 1]?.id; + const edge = prevEdges?.find( + (edge) => edge.id === generateEdgeId(sourceId, targetId), + ); + if (targetId) { + return { + id: generateEdgeId(sourceId, targetId), + source: sourceId, + target: targetId, + type: 'addNodeEdge', + data: { + flowId: flow.id, + flowActive: flow.active, + setCurrentStepId, + layouted: !!edge, + }, + }; + } + }) + .filter((edge) => !!edge) || []; + + const lastStep = flow.steps[flow.steps.length - 1]; + + return lastStep + ? [ + ...newEdges, + { + id: generateEdgeId(lastStep.id, INVISIBLE_NODE_ID), + source: lastStep.id, + target: INVISIBLE_NODE_ID, + type: 'addNodeEdge', + data: { + flowId: flow.id, + flowActive: flow.active, + setCurrentStepId, + layouted: false, + }, + }, + ] + : newEdges; + }, []); + + const generateNodes = useCallback( + (flow, prevNodes) => { + const newNodes = flow.steps.map((step, index) => { + const node = prevNodes?.find(({ id }) => id === step.id); + const collapsed = currentStepId !== step.id; + return { + id: step.id, + type: 'flowStep', + position: { + x: node ? node.position.x : 0, + y: node ? node.position.y : 0, + }, + zIndex: collapsed ? 0 : 1, + data: { + step, + index: index, + flowId: flow.id, + collapsed, + openNextStep: openNextStep(flow.steps[index + 1]), + onOpen: () => setCurrentStepId(step.id), + onClose: () => setCurrentStepId(null), + onChange: onStepChange, + layouted: !!node, + }, + }; + }); + + const prevInvisibleNode = nodes.find((node) => node.type === 'invisible'); + + return [ + ...newNodes, + { + id: INVISIBLE_NODE_ID, + type: 'invisible', + position: { + x: prevInvisibleNode ? prevInvisibleNode.position.x : 0, + y: prevInvisibleNode ? prevInvisibleNode.position.y : 0, + }, + }, + ]; + }, + [currentStepId, nodes, onStepChange, openNextStep], + ); + + const updateNodesData = useCallback( + (steps) => { + setNodes((nodes) => + nodes.map((node) => { + const step = steps.find((step) => step.id === node.id); + if (step) { + return { ...node, data: { ...node.data, step: { ...step } } }; + } + return node; + }), + ); + }, + [setNodes], + ); + + const updateEdgesData = useCallback( + (flow) => { + setEdges((edges) => + edges.map((edge) => { + return { + ...edge, + data: { ...edge.data, flowId: flow.id, flowActive: flow.active }, + }; + }), + ); + }, + [setEdges], + ); + + useEffect(() => { + setNodes( + nodes.map((node) => { + if (node.type === 'flowStep') { + const collapsed = currentStepId !== node.data.step.id; + return { + ...node, + zIndex: collapsed ? 0 : 1, + data: { + ...node.data, + collapsed, + }, + }; + } + return node; + }), + ); + }, [currentStepId]); + + useEffect(() => { + if (flow.steps.length + 1 !== nodes.length) { + const newNodes = generateNodes(flow, nodes); + const newEdges = generateEdges(flow, edges); + + setNodes(newNodes); + setEdges(newEdges); + } else { + updateNodesData(flow.steps); + updateEdgesData(flow); + } + }, [flow]); + + return ( + + + + ); +}; + +EditorNew.propTypes = { + flow: FlowPropType.isRequired, +}; + +export default EditorNew; diff --git a/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx b/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bd2abd5f0317b8963e1e9d01eafc8618dde06302 --- /dev/null +++ b/packages/web/src/components/EditorNew/FlowStepNode/FlowStepNode.jsx @@ -0,0 +1,72 @@ +import { Handle, Position } from 'reactflow'; +import { Box } from '@mui/material'; +import PropTypes from 'prop-types'; + +import FlowStep from 'components/FlowStep'; +import { StepPropType } from 'propTypes/propTypes'; + +import { NodeWrapper, NodeInnerWrapper } from './style.js'; + +function FlowStepNode({ + data: { + step, + index, + flowId, + collapsed, + openNextStep, + onOpen, + onClose, + onChange, + layouted, + }, +}) { + return ( + + + + + + + + ); +} + +FlowStepNode.propTypes = { + data: PropTypes.shape({ + step: StepPropType.isRequired, + index: PropTypes.number.isRequired, + flowId: PropTypes.string.isRequired, + collapsed: PropTypes.bool.isRequired, + openNextStep: PropTypes.func.isRequired, + onOpen: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + layouted: PropTypes.bool.isRequired, + }).isRequired, +}; + +export default FlowStepNode; diff --git a/packages/web/src/components/EditorNew/FlowStepNode/style.js b/packages/web/src/components/EditorNew/FlowStepNode/style.js new file mode 100644 index 0000000000000000000000000000000000000000..f5d113660f8fbe8d4338473f71e664a64b6b04fa --- /dev/null +++ b/packages/web/src/components/EditorNew/FlowStepNode/style.js @@ -0,0 +1,14 @@ +import { styled } from '@mui/material/styles'; +import { Box } from '@mui/material'; + +export const NodeWrapper = styled(Box)(({ theme }) => ({ + width: '100vw', + display: 'flex', + justifyContent: 'center', + padding: theme.spacing(0, 2.5), +})); + +export const NodeInnerWrapper = styled(Box)(({ theme }) => ({ + maxWidth: 900, + flex: 1, +})); diff --git a/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx b/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2d04d452906431e2d350167c21ac18ba7d344c9b --- /dev/null +++ b/packages/web/src/components/EditorNew/InvisibleNode/InvisibleNode.jsx @@ -0,0 +1,19 @@ +import { Handle, Position } from 'reactflow'; +import { Box } from '@mui/material'; + +// This node is used for adding an edge with add node button after the last flow step node +function InvisibleNode() { + return ( + + + Invisible node + + ); +} + +export default InvisibleNode; diff --git a/packages/web/src/components/EditorNew/style.js b/packages/web/src/components/EditorNew/style.js new file mode 100644 index 0000000000000000000000000000000000000000..d55ce0a03e0777f1c40d79a2a61c7e55b4b6ac15 --- /dev/null +++ b/packages/web/src/components/EditorNew/style.js @@ -0,0 +1,13 @@ +import { Stack } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const EditorWrapper = styled(Stack)(({ theme }) => ({ + flexGrow: 1, + '& > div': { + flexGrow: 1, + }, + + '& .react-flow__pane, & .react-flow__node': { + cursor: 'auto !important', + }, +})); diff --git a/packages/web/src/components/EditorNew/useAutoLayout.js b/packages/web/src/components/EditorNew/useAutoLayout.js new file mode 100644 index 0000000000000000000000000000000000000000..7e6a9b233378241ee61420ea0e25a03771075f85 --- /dev/null +++ b/packages/web/src/components/EditorNew/useAutoLayout.js @@ -0,0 +1,69 @@ +import { useCallback, useEffect } from 'react'; +import Dagre from '@dagrejs/dagre'; +import { usePrevious } from 'hooks/usePrevious'; +import { isEqual } from 'lodash'; +import { useNodesInitialized, useNodes, useReactFlow } from 'reactflow'; + +const getLayoutedElements = (nodes, edges) => { + const graph = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({})); + graph.setGraph({ + rankdir: 'TB', + marginy: 60, + ranksep: 64, + }); + edges.forEach((edge) => graph.setEdge(edge.source, edge.target)); + nodes.forEach((node) => graph.setNode(node.id, node)); + + Dagre.layout(graph); + + return { + nodes: nodes.map((node) => { + const { x, y, width, height } = graph.node(node.id); + return { + ...node, + position: { x: x - width / 2, y: y - height / 2 }, + }; + }), + edges, + }; +}; + +export const useAutoLayout = () => { + const nodes = useNodes(); + const prevNodes = usePrevious(nodes); + const nodesInitialized = useNodesInitialized(); + const { getEdges, setNodes, setEdges } = useReactFlow(); + + const onLayout = useCallback( + (nodes, edges) => { + const layoutedElements = getLayoutedElements(nodes, edges); + + setNodes([ + ...layoutedElements.nodes.map((node) => ({ + ...node, + data: { ...node.data, layouted: true }, + })), + ]); + setEdges([ + ...layoutedElements.edges.map((edge) => ({ + ...edge, + data: { ...edge.data, layouted: true }, + })), + ]); + }, + [setEdges, setNodes], + ); + + useEffect(() => { + const shouldAutoLayout = + nodesInitialized && + !isEqual( + nodes.map(({ width, height }) => ({ width, height })), + prevNodes.map(({ width, height }) => ({ width, height })), + ); + + if (shouldAutoLayout) { + onLayout(nodes, getEdges()); + } + }, [nodes]); +}; diff --git a/packages/web/src/components/EditorNew/useScrollBoundries.js b/packages/web/src/components/EditorNew/useScrollBoundries.js new file mode 100644 index 0000000000000000000000000000000000000000..e6c0001441382ad9861c87a6045af8bc9925949b --- /dev/null +++ b/packages/web/src/components/EditorNew/useScrollBoundries.js @@ -0,0 +1,13 @@ +import { useEffect } from 'react'; +import { useViewport, useReactFlow } from 'reactflow'; + +export const useScrollBoundries = () => { + const { setViewport } = useReactFlow(); + const { x, y, zoom } = useViewport(); + + useEffect(() => { + if (y > 0) { + setViewport({ x, y: 0, zoom }); + } + }, [y]); +}; diff --git a/packages/web/src/components/ExecutionHeader/index.jsx b/packages/web/src/components/ExecutionHeader/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8cc42b15c0dbcb96338fbd9961b72cb61ba27fb1 --- /dev/null +++ b/packages/web/src/components/ExecutionHeader/index.jsx @@ -0,0 +1,89 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Tooltip from '@mui/material/Tooltip'; +import Typography from '@mui/material/Typography'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { ExecutionPropType } from 'propTypes/propTypes'; + +function ExecutionName(props) { + return ( + + {props.name} + + ); +} + +ExecutionName.propTypes = { + name: PropTypes.string.isRequired, +}; + +function ExecutionId(props) { + const formatMessage = useFormatMessage(); + const id = ( + + {props.id} + + ); + return ( + + + {formatMessage('execution.id', { id })} + + + ); +} + +ExecutionId.propTypes = { + id: PropTypes.string.isRequired, +}; + +function ExecutionDate(props) { + const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10)); + const relativeCreatedAt = createdAt.toRelative(); + return ( + + + {relativeCreatedAt} + + + ); +} + +ExecutionDate.propTypes = { + createdAt: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date), + ]).isRequired, +}; + +function ExecutionHeader(props) { + const { execution } = props; + if (!execution) return ; + return ( + + + + + + + + + + + ); +} + +ExecutionHeader.propTypes = { + execution: ExecutionPropType, +}; + +export default ExecutionHeader; diff --git a/packages/web/src/components/ExecutionRow/index.jsx b/packages/web/src/components/ExecutionRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..82013a36c27d5cea05cc7857eb9eebb7ae5efa18 --- /dev/null +++ b/packages/web/src/components/ExecutionRow/index.jsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Card from '@mui/material/Card'; +import CardActionArea from '@mui/material/CardActionArea'; +import Chip from '@mui/material/Chip'; +import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos'; +import { DateTime } from 'luxon'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import FlowAppIcons from 'components/FlowAppIcons'; +import { Apps, CardContent, ArrowContainer, Title, Typography } from './style'; +import { ExecutionPropType } from 'propTypes/propTypes'; + +function ExecutionRow(props) { + const formatMessage = useFormatMessage(); + const { execution } = props; + + const { flow } = execution; + const createdAt = DateTime.fromMillis(parseInt(execution.createdAt, 10)); + const relativeCreatedAt = createdAt.toRelative(); + return ( + + + + + + + + + + <Typography variant="h6" noWrap> + {flow.name} + </Typography> + + <Typography variant="caption" noWrap> + {formatMessage('execution.createdAt', { + datetime: relativeCreatedAt, + })} + </Typography> + + + + {execution.testRun && ( + + )} + + + + theme.palette.primary.main }} + /> + + + + + + ); +} + +ExecutionRow.propTypes = { + execution: ExecutionPropType.isRequired, +}; + +export default ExecutionRow; diff --git a/packages/web/src/components/ExecutionRow/style.js b/packages/web/src/components/ExecutionRow/style.js new file mode 100644 index 0000000000000000000000000000000000000000..8b3cff9a3c11c635aaa909e0b93497753a14a6b3 --- /dev/null +++ b/packages/web/src/components/ExecutionRow/style.js @@ -0,0 +1,46 @@ +import { styled } from '@mui/material/styles'; +import MuiCardContent from '@mui/material/CardContent'; +import MuiBox from '@mui/material/Box'; +import MuiStack from '@mui/material/Stack'; +import MuiTypography from '@mui/material/Typography'; +export const CardContent = styled(MuiCardContent)(({ theme }) => ({ + display: 'grid', + gridTemplateRows: 'auto', + gridTemplateColumns: 'calc(30px * 3 + 8px * 2) minmax(0, auto) min-content', + gridGap: theme.spacing(2), + gridTemplateAreas: ` + "apps title arrow-container" + `, + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + gridTemplateAreas: ` + "apps arrow-container" + "title arrow-container" + `, + gridTemplateColumns: 'minmax(0, auto) min-content', + gridTemplateRows: 'auto auto', + }, +})); +export const Apps = styled(MuiStack)(() => ({ + gridArea: 'apps', +})); +export const Title = styled(MuiStack)(() => ({ + gridArea: 'title', +})); +export const ArrowContainer = styled(MuiBox)(() => ({ + flexDirection: 'row', + display: 'flex', + alignItems: 'center', + gap: 10, + gridArea: 'arrow-container', +})); +export const Typography = styled(MuiTypography)(() => ({ + display: 'inline-block', + width: '100%', + maxWidth: '85%', +})); +export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + display: 'none', + }, +})); diff --git a/packages/web/src/components/ExecutionStep/index.jsx b/packages/web/src/components/ExecutionStep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2ce7f6c5b658138e4fcf6f80929a0f51af533bb4 --- /dev/null +++ b/packages/web/src/components/ExecutionStep/index.jsx @@ -0,0 +1,177 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Stack from '@mui/material/Stack'; +import ErrorIcon from '@mui/icons-material/Error'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import Typography from '@mui/material/Typography'; +import Tooltip from '@mui/material/Tooltip'; +import Box from '@mui/material/Box'; + +import TabPanel from 'components/TabPanel'; +import SearchableJSONViewer from 'components/SearchableJSONViewer'; +import AppIcon from 'components/AppIcon'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useApps from 'hooks/useApps'; +import { + AppIconWrapper, + AppIconStatusIconWrapper, + Content, + Header, + Metadata, + Wrapper, +} from './style'; +import { ExecutionStepPropType, StepPropType } from 'propTypes/propTypes'; + +function ExecutionStepId(props) { + const formatMessage = useFormatMessage(); + + const id = ( + + {props.id} + + ); + + return ( + + + {formatMessage('executionStep.id', { id })} + + + ); +} + +ExecutionStepId.propTypes = { + id: PropTypes.string.isRequired, +}; + +function ExecutionStepDate(props) { + const formatMessage = useFormatMessage(); + const createdAt = DateTime.fromMillis(parseInt(props.createdAt, 10)); + const relativeCreatedAt = createdAt.toRelative(); + + return ( + + + {formatMessage('executionStep.executedAt', { + datetime: relativeCreatedAt, + })} + + + ); +} + +ExecutionStepDate.propTypes = { + createdAt: PropTypes.number.isRequired, +}; + +const validIcon = ; +const errorIcon = ; + +function ExecutionStep(props) { + const { executionStep } = props; + const [activeTabIndex, setActiveTabIndex] = React.useState(0); + const step = executionStep.step; + const isTrigger = step.type === 'trigger'; + const isAction = step.type === 'action'; + const formatMessage = useFormatMessage(); + const useAppsOptions = {}; + + if (isTrigger) { + useAppsOptions.onlyWithTriggers = true; + } + + if (isAction) { + useAppsOptions.onlyWithActions = true; + } + + const { data: apps } = useApps(useAppsOptions); + + const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey); + + if (!apps?.data) return null; + + const validationStatusIcon = + executionStep.status === 'success' ? validIcon : errorIcon; + + const hasError = !!executionStep.errorDetails; + + return ( + +
    + + + + + + {validationStatusIcon} + + + + + + + + + {isTrigger && formatMessage('flowStep.triggerType')} + {isAction && formatMessage('flowStep.actionType')} + + + + {step.position}. {app?.name} + + + + + + + + +
    + + + + setActiveTabIndex(tabIndex)} + > + + + {hasError && } + + + + + + + + + + + + {hasError && ( + + + + )} + +
    + ); +} + +ExecutionStep.propTypes = { + collapsed: PropTypes.bool, + step: StepPropType.isRequired, + index: PropTypes.number, + executionStep: ExecutionStepPropType.isRequired, +}; + +export default ExecutionStep; diff --git a/packages/web/src/components/ExecutionStep/style.js b/packages/web/src/components/ExecutionStep/style.js new file mode 100644 index 0000000000000000000000000000000000000000..378d029671db95b2491d48c16c6ecc067ce79f4a --- /dev/null +++ b/packages/web/src/components/ExecutionStep/style.js @@ -0,0 +1,54 @@ +import { styled, alpha } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +import Box from '@mui/material/Box'; +export const AppIconWrapper = styled('div')` + display: flex; + align-items: center; +`; +export const AppIconStatusIconWrapper = styled('span')` + display: inline-flex; + position: relative; + + svg { + position: absolute; + right: 0; + top: 0; + transform: translate(50%, -50%); + // to make it distinguishable over an app icon + background: white; + border-radius: 100%; + overflow: hidden; + } +`; +export const Wrapper = styled(Card)` + width: 100%; + overflow: unset; +`; +export const Header = styled('div', { + shouldForwardProp: (prop) => prop !== 'collapsed', +})` + padding: ${({ theme }) => theme.spacing(2)}; + cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')}; +`; +export const Content = styled('div')` + border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)}; + border-left: none; + border-right: none; + padding: ${({ theme }) => theme.spacing(2, 0)}; +`; +export const Metadata = styled(Box)` + display: grid; + grid-template-columns: 1fr auto; + grid-template-rows: auto auto; + grid-template-areas: + 'step id' + 'step date'; + + ${({ theme }) => theme.breakpoints.down('sm')} { + grid-template-rows: auto auto auto; + grid-template-areas: + 'id' + 'step' + 'date'; + } +`; diff --git a/packages/web/src/components/FlowAppIcons/index.jsx b/packages/web/src/components/FlowAppIcons/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f95874e62795059bc1d0e1cf8b3dbaf576cf9afb --- /dev/null +++ b/packages/web/src/components/FlowAppIcons/index.jsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import AppIcon from 'components/AppIcon'; +import IntermediateStepCount from 'components/IntermediateStepCount'; + +function FlowAppIcons(props) { + const { steps } = props; + const stepsCount = steps.length; + const firstStep = steps[0]; + const lastStep = steps.length > 1 && steps[stepsCount - 1]; + const intermeaditeStepCount = stepsCount - 2; + return ( + <> + + + {intermeaditeStepCount > 0 && ( + + )} + + {lastStep && ( + + )} + + ); +} + +FlowAppIcons.propTypes = { + steps: PropTypes.arrayOf( + PropTypes.shape({ + iconUrl: PropTypes.string, + }), + ).isRequired, +}; + +export default FlowAppIcons; diff --git a/packages/web/src/components/FlowContextMenu/index.jsx b/packages/web/src/components/FlowContextMenu/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bba8f10bec8b37199e56e3b229ada8e1f67d186e --- /dev/null +++ b/packages/web/src/components/FlowContextMenu/index.jsx @@ -0,0 +1,122 @@ +import PropTypes from 'prop-types'; +import { useMutation } from '@apollo/client'; +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; +import { useQueryClient } from '@tanstack/react-query'; + +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import Can from 'components/Can'; +import * as URLS from 'config/urls'; +import { DELETE_FLOW } from 'graphql/mutations/delete-flow'; +import { DUPLICATE_FLOW } from 'graphql/mutations/duplicate-flow'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function ContextMenu(props) { + const { flowId, onClose, anchorEl, onDuplicateFlow, onDeleteFlow, appKey } = + props; + const enqueueSnackbar = useEnqueueSnackbar(); + const formatMessage = useFormatMessage(); + const queryClient = useQueryClient(); + const [duplicateFlow] = useMutation(DUPLICATE_FLOW); + const [deleteFlow] = useMutation(DELETE_FLOW); + + const onFlowDuplicate = React.useCallback(async () => { + await duplicateFlow({ + variables: { input: { id: flowId } }, + }); + + if (appKey) { + await queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'flows'], + }); + } + + enqueueSnackbar(formatMessage('flow.successfullyDuplicated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-duplicate-flow-success', + }, + }); + + onDuplicateFlow?.(); + onClose(); + }, [flowId, onClose, duplicateFlow, queryClient, onDuplicateFlow]); + + const onFlowDelete = React.useCallback(async () => { + await deleteFlow({ + variables: { input: { id: flowId } }, + update: (cache) => { + const flowCacheId = cache.identify({ + __typename: 'Flow', + id: flowId, + }); + cache.evict({ + id: flowCacheId, + }); + }, + }); + + if (appKey) { + await queryClient.invalidateQueries({ + queryKey: ['apps', appKey, 'flows'], + }); + } + + enqueueSnackbar(formatMessage('flow.successfullyDeleted'), { + variant: 'success', + }); + + onDeleteFlow?.(); + onClose(); + }, [flowId, onClose, deleteFlow, queryClient, onDeleteFlow]); + + return ( + + + {(allowed) => ( + + {formatMessage('flow.view')} + + )} + + + + {(allowed) => ( + + {formatMessage('flow.duplicate')} + + )} + + + + {(allowed) => ( + + {formatMessage('flow.delete')} + + )} + + + ); +} + +ContextMenu.propTypes = { + flowId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + anchorEl: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.shape({ current: PropTypes.instanceOf(Element) }), + ]).isRequired, + onDeleteFlow: PropTypes.func, + onDuplicateFlow: PropTypes.func, + appKey: PropTypes.string, +}; + +export default ContextMenu; diff --git a/packages/web/src/components/FlowRow/index.jsx b/packages/web/src/components/FlowRow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3daae6843420b7f42135b0898301c7da3d9f91a3 --- /dev/null +++ b/packages/web/src/components/FlowRow/index.jsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { Link } from 'react-router-dom'; +import Card from '@mui/material/Card'; +import IconButton from '@mui/material/IconButton'; +import CardActionArea from '@mui/material/CardActionArea'; +import Chip from '@mui/material/Chip'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import { DateTime } from 'luxon'; + +import FlowAppIcons from 'components/FlowAppIcons'; +import FlowContextMenu from 'components/FlowContextMenu'; +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import { Apps, CardContent, ContextMenu, Title, Typography } from './style'; +import { FlowPropType } from 'propTypes/propTypes'; + +function getFlowStatusTranslationKey(status) { + if (status === 'published') { + return 'flow.published'; + } else if (status === 'paused') { + return 'flow.paused'; + } + return 'flow.draft'; +} + +function getFlowStatusColor(status) { + if (status === 'published') { + return 'success'; + } else if (status === 'paused') { + return 'error'; + } + return 'info'; +} + +function FlowRow(props) { + const formatMessage = useFormatMessage(); + const contextButtonRef = React.useRef(null); + const [anchorEl, setAnchorEl] = React.useState(null); + const { flow, onDuplicateFlow, onDeleteFlow, appKey } = props; + + const handleClose = () => { + setAnchorEl(null); + }; + + const onContextMenuClick = (event) => { + event.preventDefault(); + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + setAnchorEl(contextButtonRef.current); + }; + + const createdAt = DateTime.fromMillis(parseInt(flow.createdAt, 10)); + const updatedAt = DateTime.fromMillis(parseInt(flow.updatedAt, 10)); + const isUpdated = updatedAt > createdAt; + const relativeCreatedAt = createdAt.toRelative(); + const relativeUpdatedAt = updatedAt.toRelative(); + + return ( + <> + + + + + + + + + <Typography variant="h6" noWrap> + {flow?.name} + </Typography> + + <Typography variant="caption"> + {isUpdated && + formatMessage('flow.updatedAt', { + datetime: relativeUpdatedAt, + })} + {!isUpdated && + formatMessage('flow.createdAt', { + datetime: relativeCreatedAt, + })} + </Typography> + + + + + + + + + + + + + + {anchorEl && ( + + )} + + ); +} + +FlowRow.propTypes = { + flow: FlowPropType.isRequired, + onDeleteFlow: PropTypes.func, + onDuplicateFlow: PropTypes.func, + appKey: PropTypes.string, +}; + +export default FlowRow; diff --git a/packages/web/src/components/FlowRow/style.js b/packages/web/src/components/FlowRow/style.js new file mode 100644 index 0000000000000000000000000000000000000000..f8d7aa283cc5133afe9bcdbfd56b72d4917a1568 --- /dev/null +++ b/packages/web/src/components/FlowRow/style.js @@ -0,0 +1,46 @@ +import { styled } from '@mui/material/styles'; +import MuiStack from '@mui/material/Stack'; +import MuiBox from '@mui/material/Box'; +import MuiCardContent from '@mui/material/CardContent'; +import MuiTypography from '@mui/material/Typography'; +export const CardContent = styled(MuiCardContent)(({ theme }) => ({ + display: 'grid', + gridTemplateRows: 'auto', + gridTemplateColumns: 'calc(30px * 3 + 8px * 2) minmax(0, auto) min-content', + gridGap: theme.spacing(2), + gridTemplateAreas: ` + "apps title menu" + `, + alignItems: 'center', + [theme.breakpoints.down('sm')]: { + gridTemplateAreas: ` + "apps menu" + "title menu" + `, + gridTemplateColumns: 'minmax(0, auto) min-content', + gridTemplateRows: 'auto auto', + }, +})); +export const Apps = styled(MuiStack)(() => ({ + gridArea: 'apps', +})); +export const Title = styled(MuiStack)(() => ({ + gridArea: 'title', +})); +export const ContextMenu = styled(MuiBox)(({ theme }) => ({ + flexDirection: 'row', + display: 'flex', + alignItems: 'center', + gap: theme.spacing(0.625), + gridArea: 'menu', +})); +export const Typography = styled(MuiTypography)(() => ({ + display: 'inline-block', + width: '100%', + maxWidth: '85%', +})); +export const DesktopOnlyBreakline = styled('br')(({ theme }) => ({ + [theme.breakpoints.down('sm')]: { + display: 'none', + }, +})); diff --git a/packages/web/src/components/FlowStep/index.jsx b/packages/web/src/components/FlowStep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ef181bb8b922821055ddcc20cc568c2609be052f --- /dev/null +++ b/packages/web/src/components/FlowStep/index.jsx @@ -0,0 +1,370 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import List from '@mui/material/List'; +import MoreHorizIcon from '@mui/icons-material/MoreHoriz'; +import IconButton from '@mui/material/IconButton'; +import ErrorIcon from '@mui/icons-material/Error'; +import CircularProgress from '@mui/material/CircularProgress'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +import { EditorContext } from 'contexts/Editor'; +import { StepExecutionsProvider } from 'contexts/StepExecutions'; +import TestSubstep from 'components/TestSubstep'; +import FlowSubstep from 'components/FlowSubstep'; +import ChooseAppAndEventSubstep from 'components/ChooseAppAndEventSubstep'; +import ChooseConnectionSubstep from 'components/ChooseConnectionSubstep'; +import Form from 'components/Form'; +import FlowStepContextMenu from 'components/FlowStepContextMenu'; +import AppIcon from 'components/AppIcon'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import useApps from 'hooks/useApps'; +import { + AppIconWrapper, + AppIconStatusIconWrapper, + Content, + Header, + Wrapper, +} from './style'; +import isEmpty from 'helpers/isEmpty'; +import { StepPropType } from 'propTypes/propTypes'; +import useTriggers from 'hooks/useTriggers'; +import useActions from 'hooks/useActions'; +import useTriggerSubsteps from 'hooks/useTriggerSubsteps'; +import useActionSubsteps from 'hooks/useActionSubsteps'; +import useStepWithTestExecutions from 'hooks/useStepWithTestExecutions'; + +const validIcon = ; +const errorIcon = ; + +function generateValidationSchema(substeps) { + const fieldValidations = substeps?.reduce( + (allValidations, { arguments: args }) => { + if (!args || !Array.isArray(args)) return allValidations; + const substepArgumentValidations = {}; + for (const arg of args) { + const { key, required } = arg; + // base validation for the field if not exists + if (!substepArgumentValidations[key]) { + substepArgumentValidations[key] = yup.mixed(); + } + if ( + typeof substepArgumentValidations[key] === 'object' && + (arg.type === 'string' || arg.type === 'dropdown') + ) { + // if the field is required, add the required validation + if (required) { + substepArgumentValidations[key] = substepArgumentValidations[key] + .required(`${key} is required.`) + .test( + 'empty-check', + `${key} must be not empty`, + (value) => !isEmpty(value), + ); + } + // if the field depends on another field, add the dependsOn required validation + if (Array.isArray(arg.dependsOn) && arg.dependsOn.length > 0) { + for (const dependsOnKey of arg.dependsOn) { + const missingDependencyValueMessage = `We're having trouble loading '${key}' data as required field '${dependsOnKey}' is missing.`; + // TODO: make `dependsOnKey` agnostic to the field. However, nested validation schema is not supported. + // So the fields under the `parameters` key are subject to their siblings only and thus, `parameters.` is removed. + substepArgumentValidations[key] = substepArgumentValidations[ + key + ].when(`${dependsOnKey.replace('parameters.', '')}`, { + is: (value) => Boolean(value) === false, + then: (schema) => + schema + .notOneOf([''], missingDependencyValueMessage) + .required(missingDependencyValueMessage), + }); + } + } + } + } + + return { + ...allValidations, + ...substepArgumentValidations, + }; + }, + {}, + ); + + const validationSchema = yup.object({ + parameters: yup.object(fieldValidations), + }); + + return yupResolver(validationSchema); +} + +function FlowStep(props) { + const { collapsed, onChange, onContinue, flowId } = props; + const editorContext = React.useContext(EditorContext); + const contextButtonRef = React.useRef(null); + const step = props.step; + const [anchorEl, setAnchorEl] = React.useState(null); + const isTrigger = step.type === 'trigger'; + const isAction = step.type === 'action'; + const formatMessage = useFormatMessage(); + const [currentSubstep, setCurrentSubstep] = React.useState(0); + const useAppsOptions = {}; + + if (isTrigger) { + useAppsOptions.onlyWithTriggers = true; + } + + if (isAction) { + useAppsOptions.onlyWithActions = true; + } + + const { data: apps } = useApps(useAppsOptions); + + const { data: stepWithTestExecutions, refetch } = useStepWithTestExecutions( + step.id, + ); + const stepWithTestExecutionsData = stepWithTestExecutions?.data; + + React.useEffect(() => { + if (!collapsed && !isTrigger) { + refetch(step.id); + } + }, [collapsed, refetch, step.id, isTrigger]); + + const app = apps?.data?.find((currentApp) => currentApp.key === step.appKey); + + const { data: triggers } = useTriggers(app?.key); + + const { data: actions } = useActions(app?.key); + + const actionsOrTriggers = (isTrigger ? triggers?.data : actions?.data) || []; + + const actionOrTrigger = actionsOrTriggers?.find( + ({ key }) => key === step.key, + ); + + const { data: triggerSubsteps } = useTriggerSubsteps({ + appKey: app?.key, + triggerKey: actionOrTrigger?.key, + }); + + const triggerSubstepsData = triggerSubsteps?.data || []; + + const { data: actionSubsteps } = useActionSubsteps({ + appKey: app?.key, + actionKey: actionOrTrigger?.key, + }); + + const actionSubstepsData = actionSubsteps?.data || []; + + const substeps = + triggerSubstepsData.length > 0 + ? triggerSubstepsData + : actionSubstepsData || []; + + const handleChange = React.useCallback(({ step }) => { + onChange(step); + }, []); + + const expandNextStep = React.useCallback(() => { + setCurrentSubstep((currentSubstep) => (currentSubstep ?? 0) + 1); + }, []); + + const handleSubmit = (val) => { + handleChange({ step: val }); + }; + + const stepValidationSchema = React.useMemo( + () => generateValidationSchema(substeps), + [substeps], + ); + + if (!apps?.data) { + return ( + + ); + } + + const onContextMenuClose = (event) => { + event.stopPropagation(); + setAnchorEl(null); + }; + + const onContextMenuClick = (event) => { + event.stopPropagation(); + setAnchorEl(contextButtonRef.current); + }; + + const onOpen = () => collapsed && props.onOpen?.(); + + const onClose = () => props.onClose?.(); + + const toggleSubstep = (substepIndex) => + setCurrentSubstep((value) => + value !== substepIndex ? substepIndex : null, + ); + + const validationStatusIcon = + step.status === 'completed' ? validIcon : errorIcon; + + return ( + +
    + + + + + + {validationStatusIcon} + + + +
    + + {isTrigger + ? formatMessage('flowStep.triggerType') + : formatMessage('flowStep.actionType')} + + + + {step.position}. {app?.name} + +
    + + + {/* as there are no other actions besides "delete step", we hide the context menu. */} + {!isTrigger && !editorContext.readOnly && ( + + + + )} + +
    +
    + + + + + +
    + toggleSubstep(0)} + onCollapse={() => toggleSubstep(0)} + onSubmit={expandNextStep} + onChange={handleChange} + step={step} + /> + + {actionOrTrigger && + substeps?.length > 0 && + substeps.map((substep, index) => ( + + {substep.key === 'chooseConnection' && app && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + application={app} + step={step} + /> + )} + + {substep.key === 'testStep' && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + onContinue={onContinue} + showWebhookUrl={ + 'showWebhookUrl' in actionOrTrigger + ? actionOrTrigger.showWebhookUrl + : false + } + step={step} + flowId={flowId} + /> + )} + + {substep.key && + ['chooseConnection', 'testStep'].includes( + substep.key, + ) === false && ( + toggleSubstep(index + 1)} + onCollapse={() => toggleSubstep(index + 1)} + onSubmit={expandNextStep} + onChange={handleChange} + step={step} + /> + )} + + ))} + +
    +
    +
    + + +
    + + {anchorEl && ( + + )} +
    + ); +} + +FlowStep.propTypes = { + collapsed: PropTypes.bool, + step: StepPropType.isRequired, + index: PropTypes.number, + onOpen: PropTypes.func, + onClose: PropTypes.func, + onChange: PropTypes.func.isRequired, + onContinue: PropTypes.func, +}; + +export default FlowStep; diff --git a/packages/web/src/components/FlowStep/style.js b/packages/web/src/components/FlowStep/style.js new file mode 100644 index 0000000000000000000000000000000000000000..b76843966fbff30cf6e532008d9e25aaa07fc167 --- /dev/null +++ b/packages/web/src/components/FlowStep/style.js @@ -0,0 +1,35 @@ +import { styled, alpha } from '@mui/material/styles'; +import Card from '@mui/material/Card'; +export const AppIconWrapper = styled('div')` + position: relative; +`; +export const AppIconStatusIconWrapper = styled('span')` + position: absolute; + right: 0; + top: 0; + transform: translate(50%, -50%); + display: inline-flex; + + svg { + // to make it distinguishable over an app icon + background: white; + border-radius: 100%; + overflow: hidden; + } +`; +export const Wrapper = styled(Card)` + width: 100%; + overflow: unset; +`; +export const Header = styled('div', { + shouldForwardProp: (prop) => prop !== 'collapsed', +})` + padding: ${({ theme }) => theme.spacing(2)}; + cursor: ${({ collapsed }) => (collapsed ? 'pointer' : 'unset')}; +`; +export const Content = styled('div')` + border: 1px solid ${({ theme }) => alpha(theme.palette.divider, 0.8)}; + border-left: none; + border-right: none; + padding: ${({ theme }) => theme.spacing(2, 0)}; +`; diff --git a/packages/web/src/components/FlowStepContextMenu/index.jsx b/packages/web/src/components/FlowStepContextMenu/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9fedb4697da4c76851936b41c37adfcacdef8b1a --- /dev/null +++ b/packages/web/src/components/FlowStepContextMenu/index.jsx @@ -0,0 +1,50 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { useMutation } from '@apollo/client'; + +import Menu from '@mui/material/Menu'; +import MenuItem from '@mui/material/MenuItem'; + +import { DELETE_STEP } from 'graphql/mutations/delete-step'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { useQueryClient } from '@tanstack/react-query'; + +function FlowStepContextMenu(props) { + const { stepId, onClose, anchorEl, deletable, flowId } = props; + const formatMessage = useFormatMessage(); + const queryClient = useQueryClient(); + const [deleteStep] = useMutation(DELETE_STEP); + + const deleteActionHandler = React.useCallback( + async (event) => { + event.stopPropagation(); + await deleteStep({ variables: { input: { id: stepId } } }); + await queryClient.invalidateQueries({ queryKey: ['flows', flowId] }); + }, + [stepId, queryClient], + ); + + return ( + + {deletable && ( + + {formatMessage('connection.delete')} + + )} + + ); +} + +FlowStepContextMenu.propTypes = { + stepId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + anchorEl: PropTypes.element.isRequired, + deletable: PropTypes.bool.isRequired, +}; + +export default FlowStepContextMenu; diff --git a/packages/web/src/components/FlowSubstep/FilterConditions/index.jsx b/packages/web/src/components/FlowSubstep/FilterConditions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9104e7509a781e511f890fd4e247cd2a67862b61 --- /dev/null +++ b/packages/web/src/components/FlowSubstep/FilterConditions/index.jsx @@ -0,0 +1,241 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { v4 as uuidv4 } from 'uuid'; +import { useFormContext, useWatch } from 'react-hook-form'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import RemoveIcon from '@mui/icons-material/Remove'; +import AddIcon from '@mui/icons-material/Add'; +import useFormatMessage from 'hooks/useFormatMessage'; +import InputCreator from 'components/InputCreator'; +import { EditorContext } from 'contexts/Editor'; + +const createGroupItem = () => ({ + key: '', + operator: operators[0].value, + value: '', + id: uuidv4(), +}); + +const createGroup = () => ({ + and: [createGroupItem()], +}); + +const operators = [ + { + label: 'Equal', + value: 'equal', + }, + { + label: 'Not Equal', + value: 'not_equal', + }, + { + label: 'Greater Than', + value: 'greater_than', + }, + { + label: 'Less Than', + value: 'less_than', + }, + { + label: 'Greater Than Or Equal', + value: 'greater_than_or_equal', + }, + { + label: 'Less Than Or Equal', + value: 'less_than_or_equal', + }, + { + label: 'Contains', + value: 'contains', + }, + { + label: 'Not Contains', + value: 'not_contains', + }, +]; + +const createStringArgument = (argumentOptions) => { + return { + ...argumentOptions, + type: 'string', + required: true, + variables: true, + }; +}; + +const createDropdownArgument = (argumentOptions) => { + return { + ...argumentOptions, + required: true, + type: 'dropdown', + }; +}; + +function FilterConditions(props) { + const { stepId } = props; + const formatMessage = useFormatMessage(); + const { control, setValue, getValues } = useFormContext(); + const groups = useWatch({ control, name: 'parameters.or' }); + const editorContext = React.useContext(EditorContext); + + React.useEffect(function addInitialGroupWhenEmpty() { + const groups = getValues('parameters.or'); + + if (!groups) { + setValue('parameters.or', [createGroup()]); + } + }, []); + + const appendGroup = React.useCallback(() => { + const values = getValues('parameters.or'); + setValue('parameters.or', values.concat(createGroup())); + }, []); + + const appendGroupItem = React.useCallback((index) => { + const group = getValues(`parameters.or.${index}.and`); + setValue(`parameters.or.${index}.and`, group.concat(createGroupItem())); + }, []); + + const removeGroupItem = React.useCallback((groupIndex, groupItemIndex) => { + const group = getValues(`parameters.or.${groupIndex}.and`); + + if (group.length === 1) { + const groups = getValues('parameters.or'); + setValue( + 'parameters.or', + groups.filter((group, index) => index !== groupIndex), + ); + } else { + setValue( + `parameters.or.${groupIndex}.and`, + group.filter((groupItem, index) => index !== groupItemIndex), + ); + } + }, []); + + return ( + + + {groups?.map((group, groupIndex) => ( + <> + {groupIndex !== 0 && } + + + {groupIndex === 0 && + formatMessage('filterConditions.onlyContinueIf')} + {groupIndex !== 0 && + formatMessage('filterConditions.orContinueIf')} + + + {group?.and?.map((groupItem, groupItemIndex) => ( + + + + + + + + + + + + + + + + removeGroupItem(groupIndex, groupItemIndex)} + sx={{ width: 61, height: 61 }} + > + + + + ))} + + + appendGroupItem(groupIndex)} + > + And + + + {groups.length - 1 === groupIndex && ( + + Or + + )} + + + ))} + + + ); +} + +FilterConditions.propTypes = { + stepId: PropTypes.string.isRequired, +}; + +export default FilterConditions; diff --git a/packages/web/src/components/FlowSubstep/index.jsx b/packages/web/src/components/FlowSubstep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..55bb5cd661853e4d1364f609302dfd280d839f03 --- /dev/null +++ b/packages/web/src/components/FlowSubstep/index.jsx @@ -0,0 +1,90 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import { useFormContext } from 'react-hook-form'; +import Collapse from '@mui/material/Collapse'; +import ListItem from '@mui/material/ListItem'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import { EditorContext } from 'contexts/Editor'; +import FlowSubstepTitle from 'components/FlowSubstepTitle'; +import InputCreator from 'components/InputCreator'; +import FilterConditions from './FilterConditions'; +import { StepPropType, SubstepPropType } from 'propTypes/propTypes'; + +function FlowSubstep(props) { + const { + substep, + expanded = false, + onExpand, + onCollapse, + onSubmit, + step, + } = props; + const { name, arguments: args } = substep; + const editorContext = React.useContext(EditorContext); + const formContext = useFormContext(); + const validationStatus = formContext.formState.isValid; + const onToggle = expanded ? onCollapse : onExpand; + + return ( + + + + + {!!args?.length && ( + + {args.map((argument) => ( + + ))} + + )} + + {step.appKey === 'filter' && } + + + + + + ); +} + +FlowSubstep.propTypes = { + substep: SubstepPropType.isRequired, + expanded: PropTypes.bool, + onExpand: PropTypes.func.isRequired, + onCollapse: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + step: StepPropType.isRequired, +}; + +export default FlowSubstep; diff --git a/packages/web/src/components/FlowSubstepTitle/index.jsx b/packages/web/src/components/FlowSubstepTitle/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..406c17bf1b204177d32497ded729425334e0c348 --- /dev/null +++ b/packages/web/src/components/FlowSubstepTitle/index.jsx @@ -0,0 +1,37 @@ +import PropTypes from 'prop-types'; +import * as React from 'react'; +import ExpandLessIcon from '@mui/icons-material/ExpandLess'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import ErrorIcon from '@mui/icons-material/Error'; +import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import { ListItemButton, Typography } from './style'; + +const validIcon = ; + +const errorIcon = ; + +function FlowSubstepTitle(props) { + const { expanded = false, onClick = () => null, valid = null, title } = props; + const hasValidation = valid !== null; + const validationStatusIcon = valid ? validIcon : errorIcon; + + return ( + + + {expanded ? : } + {title} + + + {hasValidation && validationStatusIcon} + + ); +} + +FlowSubstepTitle.propTypes = { + expanded: PropTypes.bool, + onClick: PropTypes.func.isRequired, + valid: PropTypes.bool, + title: PropTypes.string.isRequired, +}; + +export default FlowSubstepTitle; diff --git a/packages/web/src/components/FlowSubstepTitle/style.jsx b/packages/web/src/components/FlowSubstepTitle/style.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c8c0c09055fd69c2a3edaa2202c278a904218f9b --- /dev/null +++ b/packages/web/src/components/FlowSubstepTitle/style.jsx @@ -0,0 +1,11 @@ +import { styled } from '@mui/material/styles'; +import MuiListItemButton from '@mui/material/ListItemButton'; +import MuiTypography from '@mui/material/Typography'; +export const ListItemButton = styled(MuiListItemButton)` + justify-content: space-between; +`; +export const Typography = styled(MuiTypography)` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; +`; diff --git a/packages/web/src/components/ForgotPasswordForm/index.ee.jsx b/packages/web/src/components/ForgotPasswordForm/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..92a7205d4eb0709163b1e464d0cb8454ee71beb1 --- /dev/null +++ b/packages/web/src/components/ForgotPasswordForm/index.ee.jsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { useMutation } from '@apollo/client'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import { FORGOT_PASSWORD } from 'graphql/mutations/forgot-password.ee'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function ForgotPasswordForm() { + const formatMessage = useFormatMessage(); + const [forgotPassword, { data, loading }] = useMutation(FORGOT_PASSWORD); + + const handleSubmit = async (values) => { + await forgotPassword({ + variables: { + input: values, + }, + }); + }; + + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('forgotPasswordForm.title')} + + +
    + + + + {formatMessage('forgotPasswordForm.submit')} + + + {data && ( + theme.palette.success.main }} + > + {formatMessage('forgotPasswordForm.instructionsSent')} + + )} + +
    + ); +} diff --git a/packages/web/src/components/Form/index.jsx b/packages/web/src/components/Form/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..061e10d88a954d386b186a335606513f13e939cf --- /dev/null +++ b/packages/web/src/components/Form/index.jsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { FormProvider, useForm, useWatch } from 'react-hook-form'; +const noop = () => null; +export default function Form(props) { + const { + children, + onSubmit = noop, + defaultValues, + resolver, + render, + mode = 'all', + ...formProps + } = props; + const methods = useForm({ + defaultValues, + reValidateMode: 'onBlur', + resolver, + mode, + }); + const form = useWatch({ control: methods.control }); + /** + * For fields having `dependsOn` fields, we need to re-validate the form. + */ + React.useEffect(() => { + methods.trigger(); + }, [methods.trigger, form]); + React.useEffect(() => { + methods.reset(defaultValues); + }, [defaultValues]); + return ( + +
    + {render ? render(methods) : children} +
    +
    + ); +} diff --git a/packages/web/src/components/HideOnScroll/index.jsx b/packages/web/src/components/HideOnScroll/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b93f91da4094c5bf140c5e35dda9cfe5368833d0 --- /dev/null +++ b/packages/web/src/components/HideOnScroll/index.jsx @@ -0,0 +1,7 @@ +import * as React from 'react'; +import Slide from '@mui/material/Slide'; +import useScrollTrigger from '@mui/material/useScrollTrigger'; +export default function HideOnScroll(props) { + const trigger = useScrollTrigger(); + return ; +} diff --git a/packages/web/src/components/InputCreator/index.jsx b/packages/web/src/components/InputCreator/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..613244cf768cbbb3f11114654bc488d7e4790837 --- /dev/null +++ b/packages/web/src/components/InputCreator/index.jsx @@ -0,0 +1,206 @@ +import * as React from 'react'; +import MuiTextField from '@mui/material/TextField'; +import CircularProgress from '@mui/material/CircularProgress'; + +import useDynamicFields from 'hooks/useDynamicFields'; +import useDynamicData from 'hooks/useDynamicData'; +import PowerInput from 'components/PowerInput'; +import TextField from 'components/TextField'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import ControlledCustomAutocomplete from 'components/ControlledCustomAutocomplete'; +import DynamicField from 'components/DynamicField'; + +const optionGenerator = (options) => + options?.map(({ name, value }) => ({ label: name, value: value })); + +export default function InputCreator(props) { + const { + onChange, + onBlur, + schema, + namePrefix, + stepId, + disabled, + showOptionValue, + shouldUnregister, + } = props; + const { + key: name, + label, + required, + readOnly = false, + value, + description, + type, + } = schema; + const { data, loading } = useDynamicData(stepId, schema); + const { data: additionalFieldsData, isLoading: isDynamicFieldsLoading } = + useDynamicFields(stepId, schema); + const additionalFields = additionalFieldsData?.data; + + const computedName = namePrefix ? `${namePrefix}.${name}` : name; + + if (type === 'dynamic') { + return ( + + ); + } + + if (type === 'dropdown') { + const preparedOptions = schema.options || optionGenerator(data); + + return ( + + {!schema.variables && ( + ( + + )} + defaultValue={value} + description={description} + loading={loading} + disabled={disabled} + showOptionValue={showOptionValue} + shouldUnregister={shouldUnregister} + componentsProps={{ popper: { className: 'nowheel' } }} + /> + )} + + {schema.variables && ( + + )} + + {isDynamicFieldsLoading && !additionalFields?.length && ( +
    + +
    + )} + + {additionalFields?.map((field) => ( + + ))} +
    + ); + } + + if (type === 'string') { + if (schema.variables) { + return ( + + + + {isDynamicFieldsLoading && !additionalFields?.length && ( +
    + +
    + )} + + {additionalFields?.map((field) => ( + + ))} +
    + ); + } + + return ( + + + + {isDynamicFieldsLoading && !additionalFields?.length && ( +
    + +
    + )} + + {additionalFields?.map((field) => ( + + ))} +
    + ); + } + return ; +} diff --git a/packages/web/src/components/IntermediateStepCount/index.jsx b/packages/web/src/components/IntermediateStepCount/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eae814cbc3bedf635556bb8be8fe6e800f005291 --- /dev/null +++ b/packages/web/src/components/IntermediateStepCount/index.jsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +import { Container } from './style'; +export default function IntermediateStepCount(props) { + const { count } = props; + return ( + + + +{count} + + + ); +} diff --git a/packages/web/src/components/IntermediateStepCount/style.js b/packages/web/src/components/IntermediateStepCount/style.js new file mode 100644 index 0000000000000000000000000000000000000000..f88863bc1e813af178e6182d81718b07351b3561 --- /dev/null +++ b/packages/web/src/components/IntermediateStepCount/style.js @@ -0,0 +1,11 @@ +import { styled } from '@mui/material/styles'; +export const Container = styled('div')(({ theme }) => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + minWidth: 30, + height: 30, + border: `1px solid ${theme.palette.text.disabled}`, + borderRadius: theme.shape.borderRadius, +})); diff --git a/packages/web/src/components/IntlProvider/index.jsx b/packages/web/src/components/IntlProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e3a1a35f13b827b25cd5ad8283b4f82e229dc256 --- /dev/null +++ b/packages/web/src/components/IntlProvider/index.jsx @@ -0,0 +1,14 @@ +import { IntlProvider as BaseIntlProvider } from 'react-intl'; +import englishMessages from 'locales/en.json'; +const IntlProvider = ({ children }) => { + return ( + + {children} + + ); +}; +export default IntlProvider; diff --git a/packages/web/src/components/Invoices/index.ee.jsx b/packages/web/src/components/Invoices/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a3fa6bfa140e2545a18ec6c55b4327d5ce72ef64 --- /dev/null +++ b/packages/web/src/components/Invoices/index.ee.jsx @@ -0,0 +1,97 @@ +import * as React from 'react'; +import { DateTime } from 'luxon'; +import Box from '@mui/material/Box'; +import Link from '@mui/material/Link'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; + +import useInvoices from 'hooks/useInvoices.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function Invoices() { + const formatMessage = useFormatMessage(); + const { data, isLoading: isInvoicesLoading } = useInvoices(); + const invoices = data?.data; + + if (isInvoicesLoading || invoices?.length === 0) return ; + + return ( + + + + + + {formatMessage('invoices.invoices')} + + + + + + + + + {formatMessage('invoices.date')} + + + + + + {formatMessage('invoices.amount')} + + + + + + {formatMessage('invoices.invoice')} + + + + + + + {invoices?.map((invoice, invoiceIndex) => ( + + {invoiceIndex !== 0 && } + + + + + {DateTime.fromISO(invoice.payout_date).toFormat( + 'LLL dd, yyyy', + )} + + + + + €{invoice.amount} + + + + + + {formatMessage('invoices.link')} + + + + + + ))} + + + + ); +} diff --git a/packages/web/src/components/JSONViewer/index.jsx b/packages/web/src/components/JSONViewer/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bde7a28c908b349d110ba2a079802e0f244aa8be --- /dev/null +++ b/packages/web/src/components/JSONViewer/index.jsx @@ -0,0 +1,51 @@ +import * as React from 'react'; +import { JSONTree } from 'react-json-tree'; +const theme = { + scheme: 'inspector', + author: 'Alexander Kuznetsov (alexkuz@gmail.com)', + // base00 - Default Background + base00: 'transparent', + // base01 - Lighter Background (Used for status bars, line number and folding marks) + base01: '#282828', + // base02 - Selection Background + base02: '#383838', + // base03 - Comments, Invisibles, Line Highlighting + base03: '#585858', + // base04 - Dark Foreground (Used for status bars) + base04: '#b8b8b8', + // base05 - Default Foreground, Caret, Delimiters, Operators + base05: '#d8d8d8', + // base06 - Light Foreground (Not often used) + base06: '#e8e8e8', + // base07 - Light Background (Not often used) + base07: '#FFFFFF', + // base08 - Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted + base08: '#E92F28', + // base09 - Integers, Boolean, Constants, XML Attributes, Markup Link Url + base09: '#005cc5', + // base0A - Classes, Markup Bold, Search Text Background + base0A: '#f7ca88', + // base0B - Strings, Inherited Class, Markup Code, Diff Inserted + base0B: '#22863a', + // base0C - Support, Regular Expressions, Escape Characters, Markup Quotes + base0C: '#86c1b9', + // base0D - Functions, Methods, Attribute IDs, Headings + base0D: '#d73a49', + // base0E - Keywords, Storage, Selector, Markup Italic, Diff Changed + base0E: '#EC31C0', + // base0F - Deprecated, Opening/Closing Embedded Language Tags, e.g. + base0F: '#a16946', +}; +function JSONViewer(props) { + const { data } = props; + return ( + true} + invertTheme={false} + theme={theme} + /> + ); +} +export default JSONViewer; diff --git a/packages/web/src/components/JSONViewer/style.jsx b/packages/web/src/components/JSONViewer/style.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4a61b81c5693fb46095fedbb5fff42708bd93c35 --- /dev/null +++ b/packages/web/src/components/JSONViewer/style.jsx @@ -0,0 +1,31 @@ +import GlobalStyles from '@mui/material/GlobalStyles'; +export const jsonViewerStyles = ( + ({ + 'json-viewer': { + '--background-color': 'transparent', + '--font-family': 'monaco, Consolas, Lucida Console, monospace', + '--font-size': '1rem', + '--indent-size': '1.5em', + '--indentguide-size': '1px', + '--indentguide-style': 'solid', + '--indentguide-color': theme.palette.text.primary, + '--indentguide-color-active': '#666', + '--indentguide': + 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color)', + '--indentguide-active': + 'var(--indentguide-size) var(--indentguide-style) var(--indentguide-color-active)', + /* Types colors */ + '--string-color': theme.palette.text.secondary, + '--number-color': theme.palette.text.primary, + '--boolean-color': theme.palette.text.primary, + '--null-color': theme.palette.text.primary, + '--property-color': theme.palette.text.primary, + /* Collapsed node preview */ + '--preview-color': theme.palette.text.primary, + /* Search highlight color */ + '--highlight-color': '#6fb3d2', + }, + })} + /> +); diff --git a/packages/web/src/components/Layout/index.jsx b/packages/web/src/components/Layout/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8f0532de2e9beff340a0a36d4323b72067178487 --- /dev/null +++ b/packages/web/src/components/Layout/index.jsx @@ -0,0 +1,137 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Stack from '@mui/material/Stack'; +import AppsIcon from '@mui/icons-material/Apps'; +import SwapCallsIcon from '@mui/icons-material/SwapCalls'; +import HistoryIcon from '@mui/icons-material/History'; +import NotificationsIcon from '@mui/icons-material/Notifications'; +import ArrowBackIosNew from '@mui/icons-material/ArrowBackIosNew'; + +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useVersion from 'hooks/useVersion'; +import AppBar from 'components/AppBar'; +import Drawer from 'components/Drawer'; +import useAutomatischConfig from 'hooks/useAutomatischConfig'; + +const drawerLinks = [ + { + Icon: SwapCallsIcon, + primary: 'drawer.flows', + to: URLS.FLOWS, + dataTest: 'flows-page-drawer-link', + }, + { + Icon: AppsIcon, + primary: 'drawer.apps', + to: URLS.APPS, + dataTest: 'apps-page-drawer-link', + }, + { + Icon: HistoryIcon, + primary: 'drawer.executions', + to: URLS.EXECUTIONS, + dataTest: 'executions-page-drawer-link', + }, +]; + +const generateDrawerBottomLinks = async ({ + disableNotificationsPage, + notificationBadgeContent = 0, + additionalDrawerLink, + additionalDrawerLinkText, + formatMessage, +}) => { + const notificationsPageLinkObject = { + Icon: NotificationsIcon, + primary: formatMessage('settingsDrawer.notifications'), + to: URLS.UPDATES, + badgeContent: notificationBadgeContent, + }; + + const hasAdditionalDrawerLink = + additionalDrawerLink && additionalDrawerLinkText; + + const additionalDrawerLinkObject = { + Icon: ArrowBackIosNew, + primary: additionalDrawerLinkText || '', + to: additionalDrawerLink || '', + target: '_blank', + }; + + const links = []; + + if (!disableNotificationsPage) { + links.push(notificationsPageLinkObject); + } + + if (hasAdditionalDrawerLink) { + links.push(additionalDrawerLinkObject); + } + + return links; +}; + +export default function PublicLayout({ children }) { + const version = useVersion(); + const { data: configData, isLoading } = useAutomatischConfig(); + const config = configData?.data; + + const theme = useTheme(); + const formatMessage = useFormatMessage(); + const [bottomLinks, setBottomLinks] = React.useState([]); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); + const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); + const openDrawer = () => setDrawerOpen(true); + const closeDrawer = () => setDrawerOpen(false); + + React.useEffect(() => { + async function perform() { + const newBottomLinks = await generateDrawerBottomLinks({ + notificationBadgeContent: version.newVersionCount, + disableNotificationsPage: config?.disableNotificationsPage, + additionalDrawerLink: config?.additionalDrawerLink, + additionalDrawerLinkText: config?.additionalDrawerLinkText, + formatMessage, + }); + setBottomLinks(newBottomLinks); + } + + if (isLoading) return; + + perform(); + }, [config, isLoading, version.newVersionCount]); + + return ( + <> + + + + + + + + {children} + + + + ); +} diff --git a/packages/web/src/components/ListItemLink/index.jsx b/packages/web/src/components/ListItemLink/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0e4e6b6eb536b0a8079940416c4d5bcd1467c058 --- /dev/null +++ b/packages/web/src/components/ListItemLink/index.jsx @@ -0,0 +1,49 @@ +import * as React from 'react'; +import { useMatch } from 'react-router-dom'; +import ListItem from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import { Link } from 'react-router-dom'; +export default function ListItemLink(props) { + const { icon, primary, to, onClick, 'data-test': dataTest, target } = props; + const selected = useMatch({ path: to, end: true }); + const CustomLink = React.useMemo( + () => + React.forwardRef(function InLineLink(linkProps, ref) { + try { + // challenge the link to check if it's absolute URL + new URL(to); // should throw an error if it's not an absolute URL + return ( + + ); + } catch { + return ; + } + }), + [to], + ); + return ( +
  • + + {icon} + + +
  • + ); +} diff --git a/packages/web/src/components/ListLoader/index.jsx b/packages/web/src/components/ListLoader/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ddc64728dc06e94b303ea8b985e5251f138c0d97 --- /dev/null +++ b/packages/web/src/components/ListLoader/index.jsx @@ -0,0 +1,41 @@ +import { + IconButton, + Skeleton, + Stack, + TableCell, + TableRow, +} from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/Delete'; +const ListLoader = ({ rowsNumber, columnsNumber, 'data-test': dataTest }) => { + return ( + <> + {[...Array(rowsNumber)].map((row, index) => ( + + {[...Array(columnsNumber)].map((cell, index) => ( + + + + ))} + + + + + + + + + + + + + + ))} + + ); +}; +export default ListLoader; diff --git a/packages/web/src/components/LoginForm/index.jsx b/packages/web/src/components/LoginForm/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6cf95fbc631a23facad278c6d36d2199fe4d0cb2 --- /dev/null +++ b/packages/web/src/components/LoginForm/index.jsx @@ -0,0 +1,115 @@ +import * as React from 'react'; +import { useNavigate, Link as RouterLink } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import Paper from '@mui/material/Paper'; +import Link from '@mui/material/Link'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import useAuthentication from 'hooks/useAuthentication'; +import useCloud from 'hooks/useCloud'; +import * as URLS from 'config/urls'; +import { LOGIN } from 'graphql/mutations/login'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function LoginForm() { + const isCloud = useCloud(); + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const authentication = useAuthentication(); + const [login, { loading }] = useMutation(LOGIN); + + React.useEffect(() => { + if (authentication.isAuthenticated) { + navigate(URLS.DASHBOARD); + } + }, [authentication.isAuthenticated]); + + const handleSubmit = async (values) => { + const { data } = await login({ + variables: { + input: values, + }, + }); + const { token } = data.login; + authentication.updateToken(token); + }; + + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('loginForm.title')} + + +
    + + + + + {isCloud && ( + + {formatMessage('loginForm.forgotPasswordText')} + + )} + + + {formatMessage('loginForm.submit')} + + + {isCloud && ( + + {formatMessage('loginForm.noAccount')} +   + + {formatMessage('loginForm.signUp')} + + + )} + +
    + ); +} + +export default LoginForm; diff --git a/packages/web/src/components/Logo/index.jsx b/packages/web/src/components/Logo/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5f638e4eec0fd3d56921057ecc2b6f62d851db4f --- /dev/null +++ b/packages/web/src/components/Logo/index.jsx @@ -0,0 +1,19 @@ +import * as React from 'react'; + +import CustomLogo from 'components/CustomLogo/index.ee'; +import DefaultLogo from 'components/DefaultLogo'; +import useAutomatischConfig from 'hooks/useAutomatischConfig'; + +const Logo = () => { + const { data: configData, isLoading } = useAutomatischConfig(); + const config = configData?.data; + const logoSvgData = config?.['logo.svgData']; + + if (isLoading && !logoSvgData) return ; + + if (logoSvgData) return ; + + return ; +}; + +export default Logo; diff --git a/packages/web/src/components/Logo/style.js b/packages/web/src/components/Logo/style.js new file mode 100644 index 0000000000000000000000000000000000000000..2d26af6e8cf4145078c5ad66e5faeb99c62bb75c --- /dev/null +++ b/packages/web/src/components/Logo/style.js @@ -0,0 +1,7 @@ +import { styled } from '@mui/material/styles'; +import { Link as RouterLink } from 'react-router-dom'; +export const Link = styled(RouterLink)(() => ({ + textDecoration: 'none', + color: 'inherit', + display: 'inline-flex', +})); diff --git a/packages/web/src/components/MationLogo/assets/mation-logo.svg b/packages/web/src/components/MationLogo/assets/mation-logo.svg new file mode 100644 index 0000000000000000000000000000000000000000..37b70a5174cb635162a45d2ce3f34e66b066bbfc --- /dev/null +++ b/packages/web/src/components/MationLogo/assets/mation-logo.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/web/src/components/MationLogo/index.jsx b/packages/web/src/components/MationLogo/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..34240749fbb168689474aea9ec333fb3842c1ba4 --- /dev/null +++ b/packages/web/src/components/MationLogo/index.jsx @@ -0,0 +1,6 @@ +import * as React from 'react'; +import { ReactComponent as MationLogoSvg } from './assets/mation-logo.svg'; +const MationLogo = () => { + return ; +}; +export default MationLogo; diff --git a/packages/web/src/components/MetadataProvider/index.jsx b/packages/web/src/components/MetadataProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b645349246606a046011f794c9a5118326a2b331 --- /dev/null +++ b/packages/web/src/components/MetadataProvider/index.jsx @@ -0,0 +1,36 @@ +import * as React from 'react'; + +import useAutomatischConfig from 'hooks/useAutomatischConfig'; + +const MetadataProvider = ({ children }) => { + const { data: configData } = useAutomatischConfig(); + const config = configData?.data; + + React.useEffect(() => { + document.title = config?.title || 'Automatisch'; + }, [config?.title]); + + React.useEffect(() => { + const existingFaviconElement = document.querySelector("link[rel~='icon']"); + + if (config?.disableFavicon === true) { + existingFaviconElement?.remove(); + } + + if (config?.disableFavicon === false) { + if (existingFaviconElement) { + existingFaviconElement.href = '/browser-tab.ico'; + } else { + const newFaviconElement = document.createElement('link'); + newFaviconElement.rel = 'icon'; + document.head.appendChild(newFaviconElement); + newFaviconElement.href = '/browser-tab.ico'; + } + } + + }, [config?.disableFavicon]); + + return <>{children}; +}; + +export default MetadataProvider; diff --git a/packages/web/src/components/NoResultFound/index.jsx b/packages/web/src/components/NoResultFound/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ecee7a50cc6c0a71eb71bd819590e760a2b597e2 --- /dev/null +++ b/packages/web/src/components/NoResultFound/index.jsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Card from '@mui/material/Card'; +import AddCircleIcon from '@mui/icons-material/AddCircle'; +import CardActionArea from '@mui/material/CardActionArea'; +import Typography from '@mui/material/Typography'; +import { CardContent } from './style'; +export default function NoResultFound(props) { + const { text, to } = props; + const ActionAreaLink = React.useMemo( + () => + React.forwardRef(function InlineLink(linkProps, ref) { + if (!to) return
    {linkProps.children}
    ; + return ; + }), + [to], + ); + return ( + + + + {!!to && } + + {text} + + + + ); +} diff --git a/packages/web/src/components/NoResultFound/style.js b/packages/web/src/components/NoResultFound/style.js new file mode 100644 index 0000000000000000000000000000000000000000..828f18782d504b3b1eeb4bcc146d3c7987dba55a --- /dev/null +++ b/packages/web/src/components/NoResultFound/style.js @@ -0,0 +1,10 @@ +import { styled } from '@mui/material/styles'; +import MuiCardContent from '@mui/material/CardContent'; +export const CardContent = styled(MuiCardContent)` + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(2)}; + min-height: 200px; +`; diff --git a/packages/web/src/components/NotFound/index.jsx b/packages/web/src/components/NotFound/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5ca70e5b1ff0868c80d88a09d58690aa3533b35a --- /dev/null +++ b/packages/web/src/components/NotFound/index.jsx @@ -0,0 +1,42 @@ +import { Link } from 'react-router-dom'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useAuthentication from 'hooks/useAuthentication'; +import Layout from 'components/Layout'; +import PublicLayout from 'components/PublicLayout'; + +export default function NoResultFound() { + const formatMessage = useFormatMessage(); + const { isAuthenticated } = useAuthentication(); + const pageContent = ( + + + 404 + + + {formatMessage('notFoundPage.title')} + + + + + + ); + + return isAuthenticated ? ( + {pageContent} + ) : ( + {pageContent} + ); +} diff --git a/packages/web/src/components/NotificationCard/index.jsx b/packages/web/src/components/NotificationCard/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..727d80bdc2fbf32ba225fce9e2bd9f043ca507ad --- /dev/null +++ b/packages/web/src/components/NotificationCard/index.jsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import Card from '@mui/material/Card'; +import CardHeader from '@mui/material/CardHeader'; +import CardActionArea from '@mui/material/CardActionArea'; +import CardContent from '@mui/material/CardContent'; +import Typography from '@mui/material/Typography'; +import { DateTime } from 'luxon'; +import useFormatMessage from 'hooks/useFormatMessage'; +const getHumanlyDate = (timestamp) => + DateTime.fromMillis(timestamp).toRelative(); +export default function NotificationCard(props) { + const { name, createdAt, documentationUrl, description } = props; + const formatMessage = useFormatMessage(); + const relativeCreatedAt = getHumanlyDate(new Date(createdAt).getTime()); + const subheader = formatMessage('notification.releasedAt', { + relativeDate: relativeCreatedAt, + }); + return ( + + + + + + + + + + ); +} diff --git a/packages/web/src/components/PageTitle/index.jsx b/packages/web/src/components/PageTitle/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ab34a0be102d6fe48082b3eaa16fcb3cf763e5aa --- /dev/null +++ b/packages/web/src/components/PageTitle/index.jsx @@ -0,0 +1,5 @@ +import * as React from 'react'; +import Typography from '@mui/material/Typography'; +export default function PageTitle(props) { + return ; +} diff --git a/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1eca4b159dbcb9a41a05cb3a75b5b5b524742107 --- /dev/null +++ b/packages/web/src/components/PermissionCatalogField/PermissionCatalogFieldLoader/index.jsx @@ -0,0 +1,59 @@ +import { + IconButton, + Skeleton, + Stack, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@mui/material'; +import SettingsIcon from '@mui/icons-material/Settings'; +import ControlledCheckbox from 'components/ControlledCheckbox'; +const PermissionCatalogFieldLoader = () => { + return ( + + + + + + {[...Array(5)].map((row, index) => ( + + + + ))} + + + + + {[...Array(3)].map((row, index) => ( + + + + + + {[...Array(5)].map((action, index) => ( + + + + + + ))} + + + + + + + + + + ))} + +
    +
    + ); +}; +export default PermissionCatalogFieldLoader; diff --git a/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.jsx b/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4ff1f43b5a32e61c3671e35769344c504709d93a --- /dev/null +++ b/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.jsx @@ -0,0 +1,135 @@ +import Button from '@mui/material/Button'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogTitle from '@mui/material/DialogTitle'; +import Paper from '@mui/material/Paper'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import * as React from 'react'; +import { useFormContext } from 'react-hook-form'; +import ControlledCheckbox from 'components/ControlledCheckbox'; +import useFormatMessage from 'hooks/useFormatMessage'; +export default function PermissionSettings(props) { + const { + onClose, + open = false, + fieldPrefix, + subject, + actions, + conditions, + defaultChecked, + } = props; + const formatMessage = useFormatMessage(); + const { getValues, resetField } = useFormContext(); + const cancel = () => { + for (const action of actions) { + for (const condition of conditions) { + const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`; + resetField(fieldName); + } + } + onClose(); + }; + const apply = () => { + for (const action of actions) { + for (const condition of conditions) { + const fieldName = `${fieldPrefix}.${action.key}.conditions.${condition.key}`; + const value = getValues(fieldName); + resetField(fieldName, { defaultValue: value }); + } + } + onClose(); + }; + return ( + + {formatMessage('permissionSettings.title')} + + + + + + + + + {actions.map((action) => ( + + + {action.label} + + + ))} + + + + {conditions.map((condition) => ( + + + + {condition.label} + + + + {actions.map((action) => ( + + + {action.subjects.includes(subject) && ( + + )} + + {!action.subjects.includes(subject) && '-'} + + + ))} + + ))} + +
    +
    +
    + + + + + + +
    + ); +} diff --git a/packages/web/src/components/PermissionCatalogField/index.ee.jsx b/packages/web/src/components/PermissionCatalogField/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d298cb7525f9e08c17485fff0aae24afe91767a8 --- /dev/null +++ b/packages/web/src/components/PermissionCatalogField/index.ee.jsx @@ -0,0 +1,113 @@ +import SettingsIcon from '@mui/icons-material/Settings'; +import IconButton from '@mui/material/IconButton'; +import Paper from '@mui/material/Paper'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Typography from '@mui/material/Typography'; +import * as React from 'react'; + +import ControlledCheckbox from 'components/ControlledCheckbox'; +import usePermissionCatalog from 'hooks/usePermissionCatalog.ee'; +import PermissionSettings from './PermissionSettings.ee'; +import PermissionCatalogFieldLoader from './PermissionCatalogFieldLoader'; + +const PermissionCatalogField = ({ + name = 'permissions', + disabled = false, + defaultChecked = false, +}) => { + const { data, isLoading: isPermissionCatalogLoading } = + usePermissionCatalog(); + const permissionCatalog = data?.data; + const [dialogName, setDialogName] = React.useState(); + + if (isPermissionCatalogLoading) return ; + + return ( + + + + + + + {permissionCatalog?.actions.map((action) => ( + + + {action.label} + + + ))} + + + + + + {permissionCatalog?.subjects.map((subject) => ( + + + {subject.label} + + + {permissionCatalog?.actions.map((action) => ( + + + {action.subjects.includes(subject.key) && ( + + )} + + {!action.subjects.includes(subject.key) && '-'} + + + ))} + + + + setDialogName(subject.key)} + disabled={disabled} + data-test="permission-settings-button" + > + + + + setDialogName('')} + fieldPrefix={`${name}.${subject.key}`} + subject={subject.key} + actions={permissionCatalog?.actions} + conditions={permissionCatalog?.conditions} + defaultChecked={defaultChecked} + /> + + + + ))} + +
    +
    + ); +}; +export default PermissionCatalogField; diff --git a/packages/web/src/components/Portal/index.jsx b/packages/web/src/components/Portal/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f4339748b703e51460c53ac8f04b4804ada93a28 --- /dev/null +++ b/packages/web/src/components/Portal/index.jsx @@ -0,0 +1,7 @@ +import ReactDOM from 'react-dom'; +const Portal = ({ children }) => { + return typeof document === 'object' + ? ReactDOM.createPortal(children, document.body) + : null; +}; +export default Portal; diff --git a/packages/web/src/components/PowerInput/Popper.jsx b/packages/web/src/components/PowerInput/Popper.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3509fe08801c4d8913df5e0ae6ac209928f68ca9 --- /dev/null +++ b/packages/web/src/components/PowerInput/Popper.jsx @@ -0,0 +1,37 @@ +import Paper from '@mui/material/Paper'; +import MuiPopper from '@mui/material/Popper'; +import Tab from '@mui/material/Tab'; +import * as React from 'react'; +import Suggestions from 'components/PowerInput/Suggestions'; +import TabPanel from 'components/TabPanel'; +import { Tabs } from './style'; +const Popper = (props) => { + const { open, anchorEl, data, onSuggestionClick } = props; + return ( + + + + + + + + + + + + ); +}; +export default Popper; diff --git a/packages/web/src/components/PowerInput/Suggestions.jsx b/packages/web/src/components/PowerInput/Suggestions.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0ea5f2804ebdd0b240d65d1d16ceebffbdef3fe2 --- /dev/null +++ b/packages/web/src/components/PowerInput/Suggestions.jsx @@ -0,0 +1,185 @@ +import ExpandLess from '@mui/icons-material/ExpandLess'; +import ExpandMore from '@mui/icons-material/ExpandMore'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Collapse from '@mui/material/Collapse'; +import List from '@mui/material/List'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemText from '@mui/material/ListItemText'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import throttle from 'lodash/throttle'; +import * as React from 'react'; +import { FixedSizeList } from 'react-window'; +import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; +const SHORT_LIST_LENGTH = 4; +const LIST_ITEM_HEIGHT = 64; +const computeListHeight = (currentLength) => { + const numberOfRenderedItems = Math.min(SHORT_LIST_LENGTH, currentLength); + return LIST_ITEM_HEIGHT * numberOfRenderedItems; +}; +const getPartialArray = (array, length = array.length) => { + return array.slice(0, length); +}; +const renderItemFactory = + ({ onSuggestionClick }) => + (props) => { + const { index, style, data } = props; + const suboption = data[index]; + return ( + onSuggestionClick(suboption)} + data-test="power-input-suggestion-item" + key={index} + style={style} + > + + + ); + }; +const Suggestions = (props) => { + const formatMessage = useFormatMessage(); + const { data, onSuggestionClick = () => null } = props; + const [current, setCurrent] = React.useState(0); + const [listLength, setListLength] = React.useState(SHORT_LIST_LENGTH); + const [filteredData, setFilteredData] = React.useState(data); + React.useEffect( + function syncOptions() { + setFilteredData((filteredData) => { + if (filteredData.length === 0 && filteredData.length !== data.length) { + return data; + } + return filteredData; + }); + }, + [data], + ); + const renderItem = React.useMemo( + () => + renderItemFactory({ + onSuggestionClick, + }), + [onSuggestionClick], + ); + const expandList = () => { + setListLength(Infinity); + }; + const collapseList = () => { + setListLength(SHORT_LIST_LENGTH); + }; + React.useEffect(() => { + setListLength(SHORT_LIST_LENGTH); + }, [current]); + const onSearchChange = React.useMemo( + () => + throttle((event) => { + const search = event.target.value.toLowerCase(); + if (!search) { + setFilteredData(data); + return; + } + const newFilteredData = data + .map((stepWithOutput) => { + return { + id: stepWithOutput.id, + name: stepWithOutput.name, + output: stepWithOutput.output.filter((option) => + `${option.label}\n${option.sampleValue}` + .toLowerCase() + .includes(search.toLowerCase()), + ), + }; + }) + .filter((stepWithOutput) => stepWithOutput.output.length); + setFilteredData(newFilteredData); + }, 400), + [data], + ); + return ( + + + + + + {filteredData.length > 0 && ( + + {filteredData.map((option, index) => ( + + + setCurrent((currentIndex) => + currentIndex === index ? null : index, + ) + } + sx={{ py: 0.5 }} + > + + + {!!option.output?.length && + (current === index ? : )} + + + + + {renderItem} + + + {(option.output?.length || 0) > listLength && ( + + )} + + {listLength === Infinity && ( + + )} + + + ))} + + )} + + {filteredData.length === 0 && ( + theme.spacing(0, 0, 2, 2) }}> + {formatMessage('powerInputSuggestions.noOptions')} + + )} + + ); +}; +export default Suggestions; diff --git a/packages/web/src/components/PowerInput/data.js b/packages/web/src/components/PowerInput/data.js new file mode 100644 index 0000000000000000000000000000000000000000..8657cecbceb4a522a34947a37d6fdf094b449b12 --- /dev/null +++ b/packages/web/src/components/PowerInput/data.js @@ -0,0 +1,61 @@ +const joinBy = (delimiter = '.', ...args) => + args.filter(Boolean).join(delimiter); +const process = ({ data, parentKey, index, parentLabel = '' }) => { + if (typeof data !== 'object') { + return [ + { + label: `${parentLabel}.${index}`, + value: `${parentKey}.${index}`, + sampleValue: data, + }, + ]; + } + const entries = Object.entries(data); + return entries.flatMap(([name, sampleValue]) => { + const label = joinBy('.', parentLabel, index?.toString(), name); + const value = joinBy('.', parentKey, index?.toString(), name); + if (Array.isArray(sampleValue)) { + return sampleValue.flatMap((item, index) => + process({ + data: item, + parentKey: value, + index, + parentLabel: label, + }) + ); + } + if (typeof sampleValue === 'object' && sampleValue !== null) { + return process({ + data: sampleValue, + parentKey: value, + parentLabel: label, + }); + } + return [ + { + label, + value, + sampleValue, + }, + ]; + }); +}; +export const processStepWithExecutions = (steps) => { + if (!steps) return []; + return steps + .filter((step) => { + const hasExecutionSteps = !!step.executionSteps?.length; + return hasExecutionSteps; + }) + .map((step, index) => ({ + id: step.id, + // TODO: replace with step.name once introduced + name: `${index + 1}. ${ + (step.appKey || '').charAt(0)?.toUpperCase() + step.appKey?.slice(1) + }`, + output: process({ + data: step.executionSteps?.[0]?.dataOut || {}, + parentKey: `step.${step.id}`, + }), + })); +}; diff --git a/packages/web/src/components/PowerInput/index.jsx b/packages/web/src/components/PowerInput/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..15b69298e4f160a55f8ae94a53dc6d79bac38aac --- /dev/null +++ b/packages/web/src/components/PowerInput/index.jsx @@ -0,0 +1,150 @@ +import ClickAwayListener from '@mui/base/ClickAwayListener'; +import FormHelperText from '@mui/material/FormHelperText'; +import InputLabel from '@mui/material/InputLabel'; +import * as React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { createEditor } from 'slate'; +import { Editable } from 'slate-react'; +import Slate from 'components/Slate'; +import Element from 'components/Slate/Element'; +import { + customizeEditor, + deserialize, + insertVariable, + serialize, +} from 'components/Slate/utils'; +import { StepExecutionsContext } from 'contexts/StepExecutions'; +import Popper from './Popper'; +import { processStepWithExecutions } from './data'; +import { ChildrenWrapper, FakeInput, InputLabelWrapper } from './style'; + +const PowerInput = (props) => { + const { control } = useFormContext(); + const { + defaultValue = '', + onBlur, + name, + label, + required, + description, + disabled, + shouldUnregister, + } = props; + const priorStepsWithExecutions = React.useContext(StepExecutionsContext); + const editorRef = React.useRef(null); + + const renderElement = React.useCallback( + (props) => , + [], + ); + + const [editor] = React.useState(() => customizeEditor(createEditor())); + + const [showVariableSuggestions, setShowVariableSuggestions] = + React.useState(false); + + const disappearSuggestionsOnShift = (event) => { + if (event.code === 'Tab') { + setShowVariableSuggestions(false); + } + }; + + const stepsWithVariables = React.useMemo(() => { + return processStepWithExecutions(priorStepsWithExecutions); + }, [priorStepsWithExecutions]); + + const handleBlur = React.useCallback( + (value) => { + onBlur?.(value); + }, + [onBlur], + ); + + const handleVariableSuggestionClick = React.useCallback( + (variable) => { + insertVariable(editor, variable, stepsWithVariables); + }, + [stepsWithVariables], + ); + + return ( + ( + { + controllerOnChange(serialize(value)); + }} + > + { + setShowVariableSuggestions(false); + }} + > + {/* ref-able single child for ClickAwayListener */} + + + + + {`${label}${required ? ' *' : ''}`} + + + + { + setShowVariableSuggestions(true); + }} + onBlur={() => { + controllerOnBlur(); + handleBlur(value); + }} + /> + + {/* ghost placer for the variables popover */} +
    + + + + {description} + + + + )} + /> + ); +}; +export default PowerInput; diff --git a/packages/web/src/components/PowerInput/style.js b/packages/web/src/components/PowerInput/style.js new file mode 100644 index 0000000000000000000000000000000000000000..914a7466e031878a408f8b55c654910302396782 --- /dev/null +++ b/packages/web/src/components/PowerInput/style.js @@ -0,0 +1,64 @@ +import MuiTabs from '@mui/material/Tabs'; +import { styled } from '@mui/material/styles'; + +export const ChildrenWrapper = styled('div')` + overflow-wrap: break-word; + word-wrap: break-word; + word-break: break-word; + hyphens: auto; +`; + +export const InputLabelWrapper = styled('div')` + position: absolute; + left: ${({ theme }) => theme.spacing(1.75)}; + inset: 0; + left: -6px; +`; + +export const FakeInput = styled('div', { + shouldForwardProp: (prop) => prop !== 'disabled', +})` + border: 1px solid #eee; + min-height: 56px; + width: 100%; + display: block; + padding: ${({ theme }) => theme.spacing(0, 10, 0, 1.75)}; + border-radius: ${({ theme }) => theme.spacing(0.5)}; + border-color: rgba(0, 0, 0, 0.23); + position: relative; + + ${({ disabled, theme }) => + !!disabled && + ` + color: ${theme.palette.action.disabled}; + border-color: ${theme.palette.action.disabled}; + `} + + ${({ disabled, theme }) => + !disabled && + ` + &:hover { + border-color: ${theme.palette.text.primary}; + } + &:focus-within, + &:focus { + &:before { + border-color: ${theme.palette.primary.main}; + border-radius: ${theme.spacing(0.5)}; + border-style: solid; + border-width: 2px; + bottom: -2px; + content: ''; + display: block; + left: -2px; + position: absolute; + right: -2px; + top: -2px; + } + } + `} +`; + +export const Tabs = styled(MuiTabs)` + border-bottom: 1px solid ${({ theme }) => theme.palette.divider}; +`; diff --git a/packages/web/src/components/PublicLayout/index.jsx b/packages/web/src/components/PublicLayout/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5d072ba6ceec18ce9e9c1ffcc5f8e351f0d3ffd4 --- /dev/null +++ b/packages/web/src/components/PublicLayout/index.jsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import Toolbar from '@mui/material/Toolbar'; +import AppBar from '@mui/material/AppBar'; +import Box from '@mui/material/Box'; +import Logo from 'components/Logo'; +import Container from 'components/Container'; + +function Layout({ children }) { + return ( + <> + + + + + + + + + + + {children} + + + ); +} + +Layout.propTypes = { + children: PropTypes.node.isRequired, +}; + +export default Layout; diff --git a/packages/web/src/components/QueryClientProvider/index.jsx b/packages/web/src/components/QueryClientProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7ad4e95263e2a45edf591f5b39ff5d26dc7758f6 --- /dev/null +++ b/packages/web/src/components/QueryClientProvider/index.jsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import PropTypes from 'prop-types'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import api from 'helpers/api.js'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 1000, + retryOnMount: false, + refetchOnWindowFocus: false, + // provides a convenient default while it should be overridden for other HTTP methods + queryFn: async ({ queryKey, signal }) => { + const { data } = await api.get(queryKey[0], { + signal, + }); + + return data; + }, + retry: false, + }, + }, +}); + +export default function AutomatischQueryClientProvider({ children }) { + return ( + + {children} + + + ); +} + +AutomatischQueryClientProvider.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/packages/web/src/components/ResetPasswordForm/index.ee.jsx b/packages/web/src/components/ResetPasswordForm/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f7c874c55bcd6a19021c84b05029d96bceba1358 --- /dev/null +++ b/packages/web/src/components/ResetPasswordForm/index.ee.jsx @@ -0,0 +1,123 @@ +import { useMutation } from '@apollo/client'; +import { yupResolver } from '@hookform/resolvers/yup'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import * as yup from 'yup'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import * as URLS from 'config/urls'; +import { RESET_PASSWORD } from 'graphql/mutations/reset-password.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +const validationSchema = yup.object().shape({ + password: yup.string().required('resetPasswordForm.mandatoryInput'), + confirmPassword: yup + .string() + .required('resetPasswordForm.mandatoryInput') + .oneOf([yup.ref('password')], 'resetPasswordForm.passwordsMustMatch'), +}); +export default function ResetPasswordForm() { + const enqueueSnackbar = useEnqueueSnackbar(); + const formatMessage = useFormatMessage(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const [resetPassword, { data, loading }] = useMutation(RESET_PASSWORD); + const token = searchParams.get('token'); + const handleSubmit = async (values) => { + await resetPassword({ + variables: { + input: { + password: values.password, + token, + }, + }, + }); + enqueueSnackbar(formatMessage('resetPasswordForm.passwordUpdated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-reset-password-success', + }, + }); + navigate(URLS.LOGIN); + }; + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('resetPasswordForm.title')} + + +
    ( + <> + + + + + + {formatMessage('resetPasswordForm.submit')} + + + )} + /> + + ); +} diff --git a/packages/web/src/components/RoleList/index.ee.jsx b/packages/web/src/components/RoleList/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..096b65e659420886e8429398645e6f17ad7e3302 --- /dev/null +++ b/packages/web/src/components/RoleList/index.ee.jsx @@ -0,0 +1,103 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import EditIcon from '@mui/icons-material/Edit'; + +import DeleteRoleButton from 'components/DeleteRoleButton/index.ee'; +import ListLoader from 'components/ListLoader'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRoles from 'hooks/useRoles.ee'; +import * as URLS from 'config/urls'; + +export default function RoleList() { + const formatMessage = useFormatMessage(); + const { data, isLoading: isRolesLoading } = useRoles(); + const roles = data?.data; + + return ( + + + + + + + {formatMessage('roleList.name')} + + + + + + {formatMessage('roleList.description')} + + + + + + + + {isRolesLoading && ( + + )} + {!isRolesLoading && + roles?.map((role) => ( + + + + {role.name} + + + + + + {role.description} + + + + + + + + + + + + + + ))} + +
    +
    + ); +} diff --git a/packages/web/src/components/Router/index.jsx b/packages/web/src/components/Router/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4955e5f71262fc7277e680f22c12cd3bb7021d4f --- /dev/null +++ b/packages/web/src/components/Router/index.jsx @@ -0,0 +1,2 @@ +import { BrowserRouter as Router } from 'react-router-dom'; +export default Router; diff --git a/packages/web/src/components/SearchInput/index.jsx b/packages/web/src/components/SearchInput/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2fa1691779cba67a656d4a95ba45221bd5bd63ee --- /dev/null +++ b/packages/web/src/components/SearchInput/index.jsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import InputLabel from '@mui/material/InputLabel'; +import OutlinedInput from '@mui/material/OutlinedInput'; +import InputAdornment from '@mui/material/InputAdornment'; +import FormControl from '@mui/material/FormControl'; +import SearchIcon from '@mui/icons-material/Search'; +import useFormatMessage from 'hooks/useFormatMessage'; +export default function SearchInput({ onChange }) { + const formatMessage = useFormatMessage(); + return ( + + + {formatMessage('searchPlaceholder')} + + + + theme.palette.primary.main }} /> + + } + label={formatMessage('searchPlaceholder')} + /> + + ); +} diff --git a/packages/web/src/components/SearchableJSONViewer/index.jsx b/packages/web/src/components/SearchableJSONViewer/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5d977f51b8679dae27b15997e1958741efe4674a --- /dev/null +++ b/packages/web/src/components/SearchableJSONViewer/index.jsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import throttle from 'lodash/throttle'; +import isEmpty from 'lodash/isEmpty'; +import { Box, Typography } from '@mui/material'; +import JSONViewer from 'components/JSONViewer'; +import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; +import filterObject from 'helpers/filterObject'; +const SearchableJSONViewer = ({ data }) => { + const [filteredData, setFilteredData] = React.useState(data); + const formatMessage = useFormatMessage(); + const onSearchChange = React.useMemo( + () => + throttle((event) => { + const search = event.target.value.toLowerCase(); + if (!search) { + setFilteredData(data); + return; + } + const newFilteredData = filterObject(data, search); + if (isEmpty(newFilteredData)) { + setFilteredData(null); + } else { + setFilteredData(newFilteredData); + } + }, 400), + [data], + ); + return ( + <> + + + + {filteredData && } + {!filteredData && ( + {formatMessage('jsonViewer.noDataFound')} + )} + + ); +}; +export default SearchableJSONViewer; diff --git a/packages/web/src/components/SettingsLayout/index.jsx b/packages/web/src/components/SettingsLayout/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8cb31a91417590873b9b5446e8ac18d138f609c3 --- /dev/null +++ b/packages/web/src/components/SettingsLayout/index.jsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Toolbar from '@mui/material/Toolbar'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import AccountCircleIcon from '@mui/icons-material/AccountCircle'; +import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; +import PaymentIcon from '@mui/icons-material/Payment'; +import * as URLS from 'config/urls'; +import useAutomatischInfo from 'hooks/useAutomatischInfo'; +import useFormatMessage from 'hooks/useFormatMessage'; +import AppBar from 'components/AppBar'; +import Drawer from 'components/Drawer'; +function createDrawerLinks({ isCloud }) { + const items = [ + { + Icon: AccountCircleIcon, + primary: 'settingsDrawer.myProfile', + to: URLS.SETTINGS_PROFILE, + }, + ]; + if (isCloud) { + items.push({ + Icon: PaymentIcon, + primary: 'settingsDrawer.billingAndUsage', + to: URLS.SETTINGS_BILLING_AND_USAGE, + }); + } + return items; +} +export default function SettingsLayout({ children }) { + const { data: automatischInfo } = useAutomatischInfo(); + const isCloud = automatischInfo?.data.isCloud; + const theme = useTheme(); + const formatMessage = useFormatMessage(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('lg')); + const [isDrawerOpen, setDrawerOpen] = React.useState(!matchSmallScreens); + const openDrawer = () => setDrawerOpen(true); + const closeDrawer = () => setDrawerOpen(false); + const drawerLinks = createDrawerLinks({ isCloud }); + const drawerBottomLinks = [ + { + Icon: ArrowBackIosNewIcon, + primary: formatMessage('settingsDrawer.goBack'), + to: '/', + }, + ]; + return ( + <> + + + + + + + + + {children} + + + + ); +} diff --git a/packages/web/src/components/SignUpForm/index.ee.jsx b/packages/web/src/components/SignUpForm/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d155729aea96d75bb5d68e40ca10851b96e4ece3 --- /dev/null +++ b/packages/web/src/components/SignUpForm/index.ee.jsx @@ -0,0 +1,171 @@ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import Paper from '@mui/material/Paper'; +import Typography from '@mui/material/Typography'; +import LoadingButton from '@mui/lab/LoadingButton'; +import * as yup from 'yup'; +import { yupResolver } from '@hookform/resolvers/yup'; +import useAuthentication from 'hooks/useAuthentication'; +import * as URLS from 'config/urls'; +import { REGISTER_USER } from 'graphql/mutations/register-user.ee'; +import Form from 'components/Form'; +import TextField from 'components/TextField'; +import { LOGIN } from 'graphql/mutations/login'; +import useFormatMessage from 'hooks/useFormatMessage'; +const validationSchema = yup.object().shape({ + fullName: yup.string().trim().required('signupForm.mandatoryInput'), + email: yup + .string() + .trim() + .email('signupForm.validateEmail') + .required('signupForm.mandatoryInput'), + password: yup.string().required('signupForm.mandatoryInput'), + confirmPassword: yup + .string() + .required('signupForm.mandatoryInput') + .oneOf([yup.ref('password')], 'signupForm.passwordsMustMatch'), +}); +const initialValues = { + fullName: '', + email: '', + password: '', + confirmPassword: '', +}; +function SignUpForm() { + const navigate = useNavigate(); + const authentication = useAuthentication(); + const formatMessage = useFormatMessage(); + const [registerUser, { loading: registerUserLoading }] = + useMutation(REGISTER_USER); + const [login, { loading: loginLoading }] = useMutation(LOGIN); + React.useEffect(() => { + if (authentication.isAuthenticated) { + navigate(URLS.DASHBOARD); + } + }, [authentication.isAuthenticated]); + const handleSubmit = async (values) => { + const { fullName, email, password } = values; + await registerUser({ + variables: { + input: { fullName, email, password }, + }, + }); + const { data } = await login({ + variables: { + input: { email, password }, + }, + }); + const { token } = data.login; + authentication.updateToken(token); + }; + return ( + + theme.palette.text.disabled, + pb: 2, + mb: 2, + }} + gutterBottom + > + {formatMessage('signupForm.title')} + + + ( + <> + + + + + + + + + + {formatMessage('signupForm.submit')} + + + )} + /> + + ); +} +export default SignUpForm; diff --git a/packages/web/src/components/Slate/Element.jsx b/packages/web/src/components/Slate/Element.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f28d9e9993dc7853ed156173b411fab240b14f6a --- /dev/null +++ b/packages/web/src/components/Slate/Element.jsx @@ -0,0 +1,10 @@ +import Variable from './Variable'; +export default function Element(props) { + const { attributes, children, element, disabled } = props; + switch (element.type) { + case 'variable': + return ; + default: + return

    {children}

    ; + } +} diff --git a/packages/web/src/components/Slate/Variable.jsx b/packages/web/src/components/Slate/Variable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4aff76282fae4a9672cf2013bc93ba47d5a8d9ee --- /dev/null +++ b/packages/web/src/components/Slate/Variable.jsx @@ -0,0 +1,26 @@ +import Chip from '@mui/material/Chip'; +import { useSelected, useFocused } from 'slate-react'; +export default function Variable({ attributes, children, element, disabled }) { + const selected = useSelected(); + const focused = useFocused(); + const label = ( + <> + {element.name}:{' '} + {element.sampleValue} + {children} + + ); + return ( + + ); +} diff --git a/packages/web/src/components/Slate/index.jsx b/packages/web/src/components/Slate/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..93d3fdfd241716412b798d506dd571d86dc4a61b --- /dev/null +++ b/packages/web/src/components/Slate/index.jsx @@ -0,0 +1,2 @@ +import { Slate } from 'slate-react'; +export default Slate; diff --git a/packages/web/src/components/Slate/types.js b/packages/web/src/components/Slate/types.js new file mode 100644 index 0000000000000000000000000000000000000000..cb0ff5c3b541f646105198ee23ac0fc3d805023e --- /dev/null +++ b/packages/web/src/components/Slate/types.js @@ -0,0 +1 @@ +export {}; diff --git a/packages/web/src/components/Slate/utils.js b/packages/web/src/components/Slate/utils.js new file mode 100644 index 0000000000000000000000000000000000000000..bf83c1c5890bd0a8c9b7978eb932e49d74107a77 --- /dev/null +++ b/packages/web/src/components/Slate/utils.js @@ -0,0 +1,214 @@ +import { Text } from 'slate'; +import { withHistory } from 'slate-history'; +import { ReactEditor, withReact } from 'slate-react'; +function isCustomText(value) { + const isText = Text.isText(value); + const hasValueProperty = 'value' in value; + if (isText && hasValueProperty) return true; + return false; +} +function getStepPosition(id, stepsWithVariables) { + const stepIndex = stepsWithVariables.findIndex((stepWithVariables) => { + return stepWithVariables.id === id; + }); + return stepIndex + 1; +} +function getVariableName(variable) { + return variable.replace(/{{|}}/g, ''); +} +function getVariableStepId(variable) { + const nameWithoutCurlies = getVariableName(variable); + const stepId = nameWithoutCurlies.match(stepIdRegExp)?.[1] || ''; + return stepId; +} +function getVariableSampleValue(variable, stepsWithVariables) { + const variableStepId = getVariableStepId(variable); + const stepWithVariables = stepsWithVariables.find( + ({ id }) => id === variableStepId + ); + if (!stepWithVariables) return null; + const variableName = getVariableName(variable); + const variableData = stepWithVariables.output.find( + ({ value }) => variableName === value + ); + if (!variableData) return null; + return variableData.sampleValue; +} +function getVariableDetails(variable, stepsWithVariables) { + const variableName = getVariableName(variable); + const stepId = getVariableStepId(variableName); + const stepPosition = getStepPosition(stepId, stepsWithVariables); + const sampleValue = getVariableSampleValue(variable, stepsWithVariables); + const label = variableName.replace(`step.${stepId}.`, `step${stepPosition}.`); + return { + sampleValue, + label, + }; +} +const variableRegExp = /({{.*?}})/; +const stepIdRegExp = /^step.([\da-zA-Z-]*)/; +export const deserialize = (value, options, stepsWithVariables) => { + const selectedNativeOption = options?.find( + (option) => value === option.value + ); + if (selectedNativeOption) { + return [ + { + type: 'paragraph', + value: selectedNativeOption.value, + children: [{ text: selectedNativeOption.label }], + }, + ]; + } + if (value === null || value === undefined || value === '') + return [ + { + type: 'paragraph', + children: [{ text: '' }], + }, + ]; + return value + .toString() + .split('\n') + .map((line) => { + const nodes = line.split(variableRegExp); + if (nodes.length > 1) { + return { + type: 'paragraph', + children: nodes.map((node) => { + if (node.match(variableRegExp)) { + const variableDetails = getVariableDetails( + node, + stepsWithVariables + ); + return { + type: 'variable', + name: variableDetails.label, + sampleValue: variableDetails.sampleValue, + value: node, + children: [{ text: '' }], + }; + } + return { + text: node, + }; + }), + }; + } + return { + type: 'paragraph', + children: [{ text: line }], + }; + }); +}; +export const serialize = (value) => { + const serializedNodes = value.map((node) => serializeNode(node)); + const hasSingleNode = value.length === 1; + /** + * return single serialize node alone so that we don't stringify. + * booleans stay booleans, numbers stay number + */ + if (hasSingleNode) { + return serializedNodes[0]; + } + const serializedValue = serializedNodes.join('\n'); + return serializedValue; +}; +const serializeNode = (node) => { + if (isCustomText(node)) { + return node.value; + } + if (Text.isText(node)) { + return node.text; + } + if (node.type === 'variable') { + return node.value; + } + const hasSingleChild = node.children.length === 1; + /** + * serialize alone so that we don't stringify. + * booleans stay booleans, numbers stay number + */ + if (hasSingleChild) { + return serializeNode(node.children[0]); + } + return node.children.map((n) => serializeNode(n)).join(''); +}; +export const withVariables = (editor) => { + const { isInline, isVoid } = editor; + editor.isInline = (element) => { + return element.type === 'variable' ? true : isInline(element); + }; + editor.isVoid = (element) => { + return element.type === 'variable' ? true : isVoid(element); + }; + return editor; +}; +export const insertVariable = (editor, variableData, stepsWithVariables) => { + const variableDetails = getVariableDetails( + `{{${variableData.value}}}`, + stepsWithVariables + ); + const variable = { + type: 'variable', + name: variableDetails.label, + sampleValue: variableDetails.sampleValue, + value: `{{${variableData.value}}}`, + children: [{ text: '' }], + }; + editor.insertNodes(variable, { select: false }); + focusEditor(editor); +}; +export const focusEditor = (editor) => { + ReactEditor.focus(editor); + editor.move(); +}; +export const resetEditor = (editor, options) => { + const focus = options?.focus || false; + editor.removeNodes({ + at: { + anchor: editor.start([]), + focus: editor.end([]), + }, + }); + // `editor.normalize({ force: true })` doesn't add an empty node in the editor + editor.insertNode(createTextNode('')); + if (focus) { + focusEditor(editor); + } +}; +export const overrideEditorValue = (editor, options) => { + const { option, focus } = options; + const variable = { + type: 'paragraph', + children: [ + { + value: option.value, + text: option.label, + }, + ], + }; + editor.withoutNormalizing(() => { + editor.removeNodes({ + at: { + anchor: editor.start([]), + focus: editor.end([]), + }, + }); + editor.insertNode(variable); + if (focus) { + focusEditor(editor); + } + }); +}; +export const createTextNode = (text) => ({ + type: 'paragraph', + children: [ + { + text, + }, + ], +}); +export const customizeEditor = (editor) => { + return withVariables(withReact(withHistory(editor))); +}; diff --git a/packages/web/src/components/SnackbarProvider/index.jsx b/packages/web/src/components/SnackbarProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ee2a100770d072b2eace6bce370a6e06fd9b5030 --- /dev/null +++ b/packages/web/src/components/SnackbarProvider/index.jsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import { SnackbarProvider as BaseSnackbarProvider } from 'notistack'; +const SnackbarProvider = (props) => { + return ( + + ); +}; +export default SnackbarProvider; diff --git a/packages/web/src/components/SplitButton/index.jsx b/packages/web/src/components/SplitButton/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ff26323c53fc666b439fefcbd6f6a3bd9edf236f --- /dev/null +++ b/packages/web/src/components/SplitButton/index.jsx @@ -0,0 +1,118 @@ +import PropTypes from 'prop-types'; +import ArrowDropDownIcon from '@mui/icons-material/ArrowDropDown'; +import Button from '@mui/material/Button'; +import ButtonGroup from '@mui/material/ButtonGroup'; +import ClickAwayListener from '@mui/material/ClickAwayListener'; +import Grow from '@mui/material/Grow'; +import MenuItem from '@mui/material/MenuItem'; +import MenuList from '@mui/material/MenuList'; +import Paper from '@mui/material/Paper'; +import Popper from '@mui/material/Popper'; +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +export default function SplitButton(props) { + const { options, disabled, defaultActionIndex = 0 } = props; + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + + const multiOptions = options.length > 1; + const selectedOption = options[defaultActionIndex]; + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + return ( + + + + + {multiOptions && ( + + )} + + + {multiOptions && ( + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + + {option.label} + + ))} + + + + + )} + + )} + + ); +} + +SplitButton.propTypes = { + options: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string.isRequired, + key: PropTypes.string.isRequired, + 'data-test': PropTypes.string.isRequired, + to: PropTypes.string.isRequired, + disabled: PropTypes.bool.isRequired, + }).isRequired, + ).isRequired, + disabled: PropTypes.bool, + defaultActionIndex: PropTypes.number, +}; diff --git a/packages/web/src/components/SsoProviders/index.ee.jsx b/packages/web/src/components/SsoProviders/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f2797b21ece58a48297fb6867579df78b7d38ee2 --- /dev/null +++ b/packages/web/src/components/SsoProviders/index.ee.jsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import Paper from '@mui/material/Paper'; +import Button from '@mui/material/Button'; +import Stack from '@mui/material/Stack'; +import Divider from '@mui/material/Divider'; + +import useSamlAuthProviders from 'hooks/useSamlAuthProviders.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +function SsoProviders() { + const formatMessage = useFormatMessage(); + const { data, isLoading: isSamlAuthProvidersLoading } = + useSamlAuthProviders(); + const providers = data?.data; + + if (!isSamlAuthProvidersLoading && !providers?.length) return null; + + return ( + <> + {formatMessage('loginPage.divider')} + + + + {providers?.map((provider) => ( + + ))} + + + + ); +} +export default SsoProviders; diff --git a/packages/web/src/components/SubscriptionCancelledAlert/index.ee.jsx b/packages/web/src/components/SubscriptionCancelledAlert/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b36b1ffccac46109868148690ec29875d5086e48 --- /dev/null +++ b/packages/web/src/components/SubscriptionCancelledAlert/index.ee.jsx @@ -0,0 +1,37 @@ +import * as React from 'react'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; + +import useSubscription from 'hooks/useSubscription.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { DateTime } from 'luxon'; +import useUserTrial from 'hooks/useUserTrial.ee'; + +export default function SubscriptionCancelledAlert() { + const formatMessage = useFormatMessage(); + const subscription = useSubscription(); + const trial = useUserTrial(); + + if (subscription?.data?.status === 'active' || trial.hasTrial) + return ; + + const cancellationEffectiveDateObject = DateTime.fromISO( + subscription?.data?.cancellationEffectiveDate, + ); + + return ( + + + {formatMessage('subscriptionCancelledAlert.text', { + date: cancellationEffectiveDateObject.toFormat('DDD'), + })} + + + ); +} diff --git a/packages/web/src/components/Switch/index.jsx b/packages/web/src/components/Switch/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..42645b89497447f73b72f7d6751491eec3b5f141 --- /dev/null +++ b/packages/web/src/components/Switch/index.jsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import MuiSwitch from '@mui/material/Switch'; +export default function Switch(props) { + const { control } = useFormContext(); + const inputRef = React.useRef(null); + const { + required, + name, + defaultChecked = false, + shouldUnregister = false, + disabled = false, + onBlur, + onChange, + label, + FormControlLabelProps, + className, + ...switchProps + } = props; + return ( + ( + { + controllerOnChange(...args); + onChange?.(...args); + }} + onBlur={(...args) => { + controllerOnBlur(); + onBlur?.(...args); + }} + inputRef={(element) => { + inputRef.current = element; + ref(element); + }} + /> + } + label={label} + /> + )} + /> + ); +} diff --git a/packages/web/src/components/TabPanel/index.jsx b/packages/web/src/components/TabPanel/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5ad8db7201ecb53d117f3864c0447b0b3eb94fb0 --- /dev/null +++ b/packages/web/src/components/TabPanel/index.jsx @@ -0,0 +1,9 @@ +import * as React from 'react'; +export default function TabPanel(props) { + const { children, value, index, ...other } = props; + return ( + + ); +} diff --git a/packages/web/src/components/TestSubstep/index.jsx b/packages/web/src/components/TestSubstep/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..92ee99ad355a760eb6cc199435f6cc11886c6dca --- /dev/null +++ b/packages/web/src/components/TestSubstep/index.jsx @@ -0,0 +1,157 @@ +import * as React from 'react'; +import { useMutation } from '@apollo/client'; +import Box from '@mui/material/Box'; +import Collapse from '@mui/material/Collapse'; +import ListItem from '@mui/material/ListItem'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import LoadingButton from '@mui/lab/LoadingButton'; + +import { EditorContext } from 'contexts/Editor'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { EXECUTE_FLOW } from 'graphql/mutations/execute-flow'; +import JSONViewer from 'components/JSONViewer'; +import WebhookUrlInfo from 'components/WebhookUrlInfo'; +import FlowSubstepTitle from 'components/FlowSubstepTitle'; +import { useQueryClient } from '@tanstack/react-query'; + +function serializeErrors(graphQLErrors) { + return graphQLErrors?.map((error) => { + try { + return { + ...error, + message: ( +
    +            {JSON.stringify(JSON.parse(error.message), null, 2)}
    +          
    + ), + }; + } catch { + return error; + } + }); +} + +function TestSubstep(props) { + const { + substep, + expanded = false, + onExpand, + onCollapse, + onSubmit, + onContinue, + step, + showWebhookUrl = false, + flowId, + } = props; + const formatMessage = useFormatMessage(); + const editorContext = React.useContext(EditorContext); + const [executeFlow, { data, error, loading, called, reset }] = useMutation( + EXECUTE_FLOW, + { + context: { autoSnackbar: false }, + }, + ); + const response = data?.executeFlow?.data; + const isCompleted = !error && called && !loading; + const hasNoOutput = !response && isCompleted; + const { name } = substep; + const queryClient = useQueryClient(); + + React.useEffect( + function resetTestDataOnSubstepToggle() { + if (!expanded) { + reset(); + } + }, + [expanded, reset], + ); + + const handleSubmit = React.useCallback(async () => { + if (isCompleted) { + onContinue?.(); + return; + } + + await executeFlow({ + variables: { + input: { + stepId: step.id, + }, + }, + }); + + await queryClient.invalidateQueries({ + queryKey: ['flows', flowId], + }); + }, [onSubmit, onContinue, isCompleted, queryClient, flowId]); + + const onToggle = expanded ? onCollapse : onExpand; + + return ( + + + + + {!!error?.graphQLErrors?.length && ( + + {serializeErrors(error.graphQLErrors).map((error, i) => ( +
    {error.message}
    + ))} +
    + )} + + {step.webhookUrl && showWebhookUrl && ( + + )} + + {hasNoOutput && ( + + + {formatMessage('flowEditor.noTestDataTitle')} + + + + {formatMessage('flowEditor.noTestDataMessage')} + + + )} + + {response && ( + + + + )} + + + {isCompleted && formatMessage('flowEditor.continue')} + {!isCompleted && formatMessage('flowEditor.testAndContinue')} + +
    +
    +
    + ); +} +export default TestSubstep; diff --git a/packages/web/src/components/TextField/index.jsx b/packages/web/src/components/TextField/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cd62889ad55fbf16e0d90c6788554082a22ad38f --- /dev/null +++ b/packages/web/src/components/TextField/index.jsx @@ -0,0 +1,76 @@ +import * as React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import MuiTextField from '@mui/material/TextField'; +import IconButton from '@mui/material/IconButton'; +import InputAdornment from '@mui/material/InputAdornment'; +import ContentCopyIcon from '@mui/icons-material/ContentCopy'; +import copyInputValue from 'helpers/copyInputValue'; +const createCopyAdornment = (ref) => { + return ( + + copyInputValue(ref.current)} edge="end"> + + + + ); +}; +export default function TextField(props) { + const { control } = useFormContext(); + const inputRef = React.useRef(null); + const { + required, + name, + defaultValue, + shouldUnregister = false, + clickToCopy = false, + readOnly = false, + disabled = false, + onBlur, + onChange, + 'data-test': dataTest, + ...textFieldProps + } = props; + return ( + ( + { + controllerOnChange(...args); + onChange?.(...args); + }} + onBlur={(...args) => { + controllerOnBlur(); + onBlur?.(...args); + }} + inputRef={(element) => { + inputRef.current = element; + ref(element); + }} + InputProps={{ + readOnly, + endAdornment: clickToCopy ? createCopyAdornment(inputRef) : null, + }} + inputProps={{ + 'data-test': dataTest, + }} + /> + )} + /> + ); +} diff --git a/packages/web/src/components/ThemeProvider/index.jsx b/packages/web/src/components/ThemeProvider/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ebdb882a0529fd3bc57ddda1cc6f89fb4e0a0b88 --- /dev/null +++ b/packages/web/src/components/ThemeProvider/index.jsx @@ -0,0 +1,56 @@ +import CssBaseline from '@mui/material/CssBaseline'; +import { ThemeProvider as BaseThemeProvider } from '@mui/material/styles'; +import clone from 'lodash/clone'; +import get from 'lodash/get'; +import set from 'lodash/set'; +import * as React from 'react'; + +import useAutomatischInfo from 'hooks/useAutomatischInfo'; +import useAutomatischConfig from 'hooks/useAutomatischConfig'; +import { defaultTheme, mationTheme } from 'styles/theme'; + +const customizeTheme = (theme, config) => { + // `clone` is needed so that the new theme reference triggers re-render + const shallowDefaultTheme = clone(theme); + + for (const key in config) { + const value = config[key]; + const exists = get(theme, key); + + if (exists) { + set(shallowDefaultTheme, key, value); + } + } + + return shallowDefaultTheme; +}; +const ThemeProvider = ({ children, ...props }) => { + const { data: automatischInfo, isPending: isAutomatischInfoPending } = + useAutomatischInfo(); + const isMation = automatischInfo?.data.isMation; + const { data: configData, isLoading: configLoading } = useAutomatischConfig(); + const config = configData?.data; + + const customTheme = React.useMemo(() => { + const installationTheme = isMation ? mationTheme : defaultTheme; + + if (configLoading || isAutomatischInfoPending) return installationTheme; + + const customTheme = customizeTheme(installationTheme, config || {}); + + return customTheme; + }, [configLoading, config, isMation, isAutomatischInfoPending]); + + // TODO: maybe a global loading state for the custom theme? + if (isAutomatischInfoPending || configLoading) return <>; + + return ( + + + + {children} + + ); +}; + +export default ThemeProvider; diff --git a/packages/web/src/components/TrialOverAlert/index.ee.jsx b/packages/web/src/components/TrialOverAlert/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cbb53f1cae4bf0190d9d14006d211a2c021bd45a --- /dev/null +++ b/packages/web/src/components/TrialOverAlert/index.ee.jsx @@ -0,0 +1,31 @@ +import * as React from 'react'; +import Alert from '@mui/material/Alert'; +import Typography from '@mui/material/Typography'; + +import * as URLS from 'config/urls'; +import { generateInternalLink } from 'helpers/translationValues'; +import useUserTrial from 'hooks/useUserTrial.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; + +export default function TrialOverAlert() { + const formatMessage = useFormatMessage(); + const trialStatus = useUserTrial(); + + if (!trialStatus || !trialStatus.over) return ; + + return ( + + + {formatMessage('trialOverAlert.text', { + link: generateInternalLink(URLS.SETTINGS_PLAN_UPGRADE), + })} + + + ); +} diff --git a/packages/web/src/components/TrialStatusBadge/index.ee.jsx b/packages/web/src/components/TrialStatusBadge/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d31d9e66c158c09f7603ed70af931d03a5439746 --- /dev/null +++ b/packages/web/src/components/TrialStatusBadge/index.ee.jsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; + +import { Chip } from './style.ee'; +import * as URLS from 'config/urls'; +import useUserTrial from 'hooks/useUserTrial.ee'; + +export default function TrialStatusBadge() { + const data = useUserTrial(); + + if (!data.hasTrial) return ; + + const { message, status } = data; + + return ( + + ); +} diff --git a/packages/web/src/components/TrialStatusBadge/style.ee.jsx b/packages/web/src/components/TrialStatusBadge/style.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..98f82b2bd3d227672dd2092ca2bb2f8013862afe --- /dev/null +++ b/packages/web/src/components/TrialStatusBadge/style.ee.jsx @@ -0,0 +1,12 @@ +import { styled } from '@mui/material/styles'; +import MuiChip, { chipClasses } from '@mui/material/Chip'; +export const Chip = styled(MuiChip)` + &.${chipClasses.root} { + font-weight: 500; + } + + &.${chipClasses.colorWarning} { + background: #fef3c7; + color: #78350f; + } +`; diff --git a/packages/web/src/components/UpgradeFreeTrial/index.ee.jsx b/packages/web/src/components/UpgradeFreeTrial/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..98978cee494a2acf44a7b04ca8311252395cce98 --- /dev/null +++ b/packages/web/src/components/UpgradeFreeTrial/index.ee.jsx @@ -0,0 +1,179 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Card from '@mui/material/Card'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import LockIcon from '@mui/icons-material/Lock'; + +import usePaymentPlans from 'hooks/usePaymentPlans.ee'; +import useCurrentUser from 'hooks/useCurrentUser'; +import usePaddle from 'hooks/usePaddle.ee'; + +export default function UpgradeFreeTrial() { + const { data: plans, isLoading: isPaymentPlansLoading } = usePaymentPlans(); + const { data } = useCurrentUser(); + const currentUser = data?.data; + const { loaded: paddleLoaded } = usePaddle(); + const [selectedIndex, setSelectedIndex] = React.useState(0); + const selectedPlan = plans?.data?.[selectedIndex]; + const updateSelection = (index) => setSelectedIndex(index); + + const handleCheckout = React.useCallback(() => { + window.Paddle.Checkout?.open({ + product: selectedPlan.productId, + email: currentUser?.email, + passthrough: JSON.stringify({ + id: currentUser?.id, + email: currentUser?.email, + }), + }); + }, [selectedPlan, currentUser]); + + if (isPaymentPlansLoading || !plans?.data?.length) return null; + + return ( + + + + + + Upgrade your free trial + + {/* */} + + + + + + + theme.palette.background.default, + }} + > + + + + Monthly Tasks + + + + + Price + + + + + + {plans?.data?.map((plan, index) => ( + updateSelection(index)} + sx={{ + '&:hover': { cursor: 'pointer' }, + backgroundColor: + selectedIndex === index ? '#f1f3fa' : 'white', + border: + selectedIndex === index + ? '2px solid #0059f7' + : 'none', + }} + > + + + {plan.limit} + + + + + {plan.price} / month + + + + ))} + +
    +
    +
    + + + + + Due today:  + + + {selectedPlan.price} + + + + + + VAT if applicable + + + +
    +
    +
    + ); +} diff --git a/packages/web/src/components/UsageDataInformation/index.ee.jsx b/packages/web/src/components/UsageDataInformation/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5d0cf6615a45c80152b208af32a1a8257a343f11 --- /dev/null +++ b/packages/web/src/components/UsageDataInformation/index.ee.jsx @@ -0,0 +1,255 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Stack from '@mui/material/Stack'; +import Box from '@mui/material/Box'; +import Button from '@mui/material/Button'; +import Chip from '@mui/material/Chip'; +import Card from '@mui/material/Card'; +import CardActions from '@mui/material/CardActions'; +import CardContent from '@mui/material/CardContent'; +import Divider from '@mui/material/Divider'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; + +import TrialOverAlert from 'components/TrialOverAlert/index.ee'; +import SubscriptionCancelledAlert from 'components/SubscriptionCancelledAlert/index.ee'; +import CheckoutCompletedAlert from 'components/CheckoutCompletedAlert/index.ee'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import usePlanAndUsage from 'hooks/usePlanAndUsage'; +import useSubscription from 'hooks/useSubscription.ee'; +import useUserTrial from 'hooks/useUserTrial.ee'; +import { useQueryClient } from '@tanstack/react-query'; +import useCurrentUser from 'hooks/useCurrentUser'; + +const capitalize = (str) => str[0].toUpperCase() + str.slice(1, str.length); + +function BillingCard(props) { + const { name, title = '', action, text } = props; + + return ( + theme.palette.background.default, + }} + > + + + {name} + + + + {title} + + + + + + + + ); +} + +function Action(props) { + const { action, text } = props; + + if (!action) return ; + + if (action.startsWith('http')) { + return ( + + ); + } else if (action.startsWith('/')) { + return ( + + ); + } + + return ( + + {text} + + ); +} + +export default function UsageDataInformation() { + const formatMessage = useFormatMessage(); + const queryClient = useQueryClient(); + const { data: currentUser } = useCurrentUser(); + const currentUserId = currentUser?.data?.id; + const { data } = usePlanAndUsage(currentUserId); + const planAndUsage = data?.data; + const trial = useUserTrial(); + const subscriptionData = useSubscription(); + const subscription = subscriptionData?.data; + let billingInfo; + + React.useEffect(() => { + queryClient.invalidateQueries({ + queryKey: ['users', currentUserId, 'planAndUsage'], + }); + }, [subscription, queryClient, currentUserId]); + + if (trial.hasTrial) { + billingInfo = { + monthlyQuota: { + title: formatMessage('usageDataInformation.freeTrial'), + action: URLS.SETTINGS_PLAN_UPGRADE, + text: 'Upgrade plan', + }, + nextBillAmount: { + title: '---', + action: null, + text: null, + }, + nextBillDate: { + title: '---', + action: null, + text: null, + }, + }; + } else { + billingInfo = { + monthlyQuota: { + title: planAndUsage?.plan?.limit, + action: subscription?.cancelUrl, + text: formatMessage('usageDataInformation.cancelPlan'), + }, + nextBillAmount: { + title: `€${subscription?.nextBillAmount}`, + action: subscription?.updateUrl, + text: formatMessage('usageDataInformation.updatePaymentMethod'), + }, + nextBillDate: { + title: subscription?.nextBillDate, + action: formatMessage('usageDataInformation.monthlyPayment'), + text: formatMessage('usageDataInformation.monthlyPayment'), + }, + }; + } + + return ( + + + + + + + + + + + + + {formatMessage('usageDataInformation.subscriptionPlan')} + + + {subscription?.status && ( + + )} + + + + + + + + + + + + + + + + + + + + + {formatMessage('usageDataInformation.yourUsage')} + + + + + {formatMessage('usageDataInformation.yourUsageDescription')} + + + + + + + + {formatMessage('usageDataInformation.yourUsageTasks')} + + + + {planAndUsage?.usage.task} + + + + + + + {/* free plan has `null` status so that we can show the upgrade button */} + {subscription?.status === undefined && ( + + )} + + + + ); +} diff --git a/packages/web/src/components/UserList/TablePaginationActions/index.jsx b/packages/web/src/components/UserList/TablePaginationActions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c611d005175f2e8755576019e25571495f5690b5 --- /dev/null +++ b/packages/web/src/components/UserList/TablePaginationActions/index.jsx @@ -0,0 +1,67 @@ +import { useTheme } from '@mui/material'; +import IconButton from '@mui/material/IconButton'; +import FirstPageIcon from '@mui/icons-material/FirstPage'; +import KeyboardArrowLeft from '@mui/icons-material/KeyboardArrowLeft'; +import KeyboardArrowRight from '@mui/icons-material/KeyboardArrowRight'; +import LastPageIcon from '@mui/icons-material/LastPage'; +import Box from '@mui/material/Box'; +export default function TablePaginationActions(props) { + const theme = useTheme(); + const { count, page, rowsPerPage, onPageChange } = props; + const handleFirstPageButtonClick = (event) => { + onPageChange(event, 0); + }; + const handleBackButtonClick = (event) => { + onPageChange(event, page - 1); + }; + const handleNextButtonClick = (event) => { + onPageChange(event, page + 1); + }; + const handleLastPageButtonClick = (event) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }; + return ( + + + {theme.direction === 'rtl' ? : } + + + {theme.direction === 'rtl' ? ( + + ) : ( + + )} + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="next page" + data-test="next-page-button" + > + {theme.direction === 'rtl' ? ( + + ) : ( + + )} + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="last page" + data-test="last-page-button" + > + {theme.direction === 'rtl' ? : } + + + ); +} diff --git a/packages/web/src/components/UserList/index.jsx b/packages/web/src/components/UserList/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7c383fae7edc81ea5d7b745aaa395561ce243890 --- /dev/null +++ b/packages/web/src/components/UserList/index.jsx @@ -0,0 +1,142 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Stack from '@mui/material/Stack'; +import Table from '@mui/material/Table'; +import TableBody from '@mui/material/TableBody'; +import TableCell from '@mui/material/TableCell'; +import TableContainer from '@mui/material/TableContainer'; +import TableHead from '@mui/material/TableHead'; +import TableRow from '@mui/material/TableRow'; +import Paper from '@mui/material/Paper'; +import IconButton from '@mui/material/IconButton'; +import Typography from '@mui/material/Typography'; +import EditIcon from '@mui/icons-material/Edit'; +import TableFooter from '@mui/material/TableFooter'; +import DeleteUserButton from 'components/DeleteUserButton/index.ee'; +import ListLoader from 'components/ListLoader'; +import useAdminUsers from 'hooks/useAdminUsers'; +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import TablePaginationActions from './TablePaginationActions'; +import { TablePagination } from './style'; + +export default function UserList() { + const formatMessage = useFormatMessage(); + const [page, setPage] = React.useState(0); + const { data: usersData, isLoading } = useAdminUsers(page + 1); + const users = usersData?.data; + const { count } = usersData?.meta || {}; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + return ( + <> + + + + + + + {formatMessage('userList.fullName')} + + + + + + {formatMessage('userList.email')} + + + + + + {formatMessage('userList.role')} + + + + + + + + {isLoading && ( + + )} + {!isLoading && + users.map((user) => ( + + + + {user.fullName} + + + + + + {user.email} + + + + + + {user.role.name} + + + + + + + + + + + + + + ))} + + {!isLoading && typeof count === 'number' && ( + + + + + + )} +
    +
    + + ); +} diff --git a/packages/web/src/components/UserList/style.js b/packages/web/src/components/UserList/style.js new file mode 100644 index 0000000000000000000000000000000000000000..d08521825bb724fe52a8f030cfa972fb1a903979 --- /dev/null +++ b/packages/web/src/components/UserList/style.js @@ -0,0 +1,11 @@ +import { styled } from '@mui/material/styles'; +import MuiTablePagination, { + tablePaginationClasses, +} from '@mui/material/TablePagination'; +export const TablePagination = styled(MuiTablePagination)(() => ({ + [`& .${tablePaginationClasses.selectLabel}, & .${tablePaginationClasses.displayedRows}`]: + { + fontWeight: 400, + fontSize: 14, + }, +})); diff --git a/packages/web/src/components/WebhookUrlInfo/index.jsx b/packages/web/src/components/WebhookUrlInfo/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6cb98b5dbf9c7ca7cd73ed9f67d5ae0ba32ac650 --- /dev/null +++ b/packages/web/src/components/WebhookUrlInfo/index.jsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { FormattedMessage } from 'react-intl'; +import Typography from '@mui/material/Typography'; +import { generateExternalLink } from '../../helpers/translationValues'; +import { WEBHOOK_DOCS } from '../../config/urls'; +import TextField from '../TextField'; +import { Alert } from './style'; +function WebhookUrlInfo(props) { + const { webhookUrl, ...alertProps } = props; + return ( + + + + + + + + + + + } + /> + + ); +} +export default WebhookUrlInfo; diff --git a/packages/web/src/components/WebhookUrlInfo/style.js b/packages/web/src/components/WebhookUrlInfo/style.js new file mode 100644 index 0000000000000000000000000000000000000000..62466281ea68d662b74ba7d405995d1117d9cba6 --- /dev/null +++ b/packages/web/src/components/WebhookUrlInfo/style.js @@ -0,0 +1,13 @@ +import { styled } from '@mui/material/styles'; +import MuiAlert, { alertClasses } from '@mui/material/Alert'; +export const Alert = styled(MuiAlert)(() => ({ + [`&.${alertClasses.root}`]: { + fontWeight: 300, + width: '100%', + display: 'flex', + flexDirection: 'column', + }, + [`& .${alertClasses.message}`]: { + width: '100%', + }, +})); diff --git a/packages/web/src/config/app.js b/packages/web/src/config/app.js new file mode 100644 index 0000000000000000000000000000000000000000..1a31464058c249c9b60979684d97dfc6103da3d7 --- /dev/null +++ b/packages/web/src/config/app.js @@ -0,0 +1,24 @@ +const backendUrl = process.env.REACT_APP_BACKEND_URL; + +const computeUrl = (url, backendUrl) => { + /** + * In case `backendUrl` is a host, we append the url to it. + **/ + try { + return new URL(url, backendUrl).toString(); + } catch (e) { + /* + * In case `backendUrl` is not qualified, we utilize `url` alone. + **/ + return url; + } +}; + +const config = { + baseUrl: process.env.REACT_APP_BASE_URL, + graphqlUrl: computeUrl('/graphql', backendUrl), + restApiUrl: computeUrl('/api', backendUrl), + supportEmailAddress: 'support@automatisch.io', +}; + +export default config; diff --git a/packages/web/src/config/urls.js b/packages/web/src/config/urls.js new file mode 100644 index 0000000000000000000000000000000000000000..3d75786242e160a5b9dc5640055229e41768ea01 --- /dev/null +++ b/packages/web/src/config/urls.js @@ -0,0 +1,100 @@ +export const CONNECTIONS = '/connections'; +export const EXECUTIONS = '/executions'; +export const EXECUTION_PATTERN = '/executions/:executionId'; +export const EXECUTION = (executionId) => `/executions/${executionId}`; +export const LOGIN = '/login'; +export const LOGIN_CALLBACK = `${LOGIN}/callback`; +export const SIGNUP = '/sign-up'; +export const FORGOT_PASSWORD = '/forgot-password'; +export const RESET_PASSWORD = '/reset-password'; +export const APPS = '/apps'; +export const NEW_APP_CONNECTION = '/apps/new'; +export const APP = (appKey) => `/app/${appKey}`; +export const APP_PATTERN = '/app/:appKey'; +export const APP_CONNECTIONS = (appKey) => `/app/${appKey}/connections`; +export const APP_CONNECTIONS_PATTERN = '/app/:appKey/connections'; +export const APP_ADD_CONNECTION = (appKey, shared = false) => + `/app/${appKey}/connections/add?shared=${shared}`; +export const APP_ADD_CONNECTION_WITH_AUTH_CLIENT_ID = ( + appKey, + appAuthClientId, +) => `/app/${appKey}/connections/add?appAuthClientId=${appAuthClientId}`; +export const APP_ADD_CONNECTION_PATTERN = '/app/:appKey/connections/add'; +export const APP_RECONNECT_CONNECTION = ( + appKey, + connectionId, + appAuthClientId, +) => { + const path = `/app/${appKey}/connections/${connectionId}/reconnect`; + if (appAuthClientId) { + return `${path}?appAuthClientId=${appAuthClientId}`; + } + return path; +}; +export const APP_RECONNECT_CONNECTION_PATTERN = + '/app/:appKey/connections/:connectionId/reconnect'; +export const APP_FLOWS = (appKey) => `/app/${appKey}/flows`; +export const APP_FLOWS_FOR_CONNECTION = (appKey, connectionId) => + `/app/${appKey}/flows?connectionId=${connectionId}`; +export const APP_FLOWS_PATTERN = '/app/:appKey/flows'; +export const EDITOR = '/editor'; +export const CREATE_FLOW = '/editor/create'; +export const CREATE_FLOW_WITH_APP = (appKey) => + `/editor/create?appKey=${appKey}`; +export const CREATE_FLOW_WITH_APP_AND_CONNECTION = (appKey, connectionId) => { + const params = {}; + if (appKey) { + params.appKey = appKey; + } + if (connectionId) { + params.connectionId = connectionId; + } + const searchParams = new URLSearchParams(params).toString(); + return `/editor/create?${searchParams}`; +}; +export const FLOW_EDITOR = (flowId) => `/editor/${flowId}`; +export const FLOWS = '/flows'; +// TODO: revert this back to /flows/:flowId once we have a proper single flow page +export const FLOW = (flowId) => `/editor/${flowId}`; +export const FLOW_PATTERN = '/flows/:flowId'; +export const SETTINGS = '/settings'; +export const SETTINGS_DASHBOARD = SETTINGS; +export const PROFILE = 'profile'; +export const BILLING_AND_USAGE = 'billing'; +export const PLAN_UPGRADE = 'upgrade'; +export const UPDATES = '/updates'; +export const SETTINGS_PROFILE = `${SETTINGS}/${PROFILE}`; +export const SETTINGS_BILLING_AND_USAGE = `${SETTINGS}/${BILLING_AND_USAGE}`; +export const SETTINGS_PLAN_UPGRADE = `${SETTINGS_BILLING_AND_USAGE}/${PLAN_UPGRADE}`; +export const ADMIN_SETTINGS = '/admin-settings'; +export const ADMIN_SETTINGS_DASHBOARD = ADMIN_SETTINGS; +export const USERS = `${ADMIN_SETTINGS}/users`; +export const USER = (userId) => `${USERS}/${userId}`; +export const USER_PATTERN = `${USERS}/:userId`; +export const CREATE_USER = `${USERS}/create`; +export const ROLES = `${ADMIN_SETTINGS}/roles`; +export const ROLE = (roleId) => `${ROLES}/${roleId}`; +export const ROLE_PATTERN = `${ROLES}/:roleId`; +export const CREATE_ROLE = `${ROLES}/create`; +export const USER_INTERFACE = `${ADMIN_SETTINGS}/user-interface`; +export const AUTHENTICATION = `${ADMIN_SETTINGS}/authentication`; +export const ADMIN_APPS = `${ADMIN_SETTINGS}/apps`; +export const ADMIN_APP = (appKey) => `${ADMIN_SETTINGS}/apps/${appKey}`; +export const ADMIN_APP_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey`; +export const ADMIN_APP_SETTINGS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/settings`; +export const ADMIN_APP_AUTH_CLIENTS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/auth-clients`; +export const ADMIN_APP_CONNECTIONS_PATTERN = `${ADMIN_SETTINGS}/apps/:appKey/connections`; +export const ADMIN_APP_CONNECTIONS = (appKey) => + `${ADMIN_SETTINGS}/apps/${appKey}/connections`; +export const ADMIN_APP_SETTINGS = (appKey) => + `${ADMIN_SETTINGS}/apps/${appKey}/settings`; +export const ADMIN_APP_AUTH_CLIENTS = (appKey) => + `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients`; +export const ADMIN_APP_AUTH_CLIENT = (appKey, id) => + `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/${id}`; +export const ADMIN_APP_AUTH_CLIENTS_CREATE = (appKey) => + `${ADMIN_SETTINGS}/apps/${appKey}/auth-clients/create`; +export const DASHBOARD = FLOWS; +// External links +export const WEBHOOK_DOCS = + 'https://automatisch.io/docs/apps/webhooks/connection'; diff --git a/packages/web/src/contexts/Authentication.jsx b/packages/web/src/contexts/Authentication.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f4bd4a23bded26ebdb83408c688f508c847700ea --- /dev/null +++ b/packages/web/src/contexts/Authentication.jsx @@ -0,0 +1,38 @@ +import * as React from 'react'; +import { getItem, removeItem, setItem } from 'helpers/storage'; +import api from 'helpers/api.js'; + +export const AuthenticationContext = React.createContext({ + token: null, + updateToken: () => {}, + removeToken: () => {}, + isAuthenticated: false, +}); + +export const AuthenticationProvider = (props) => { + const { children } = props; + const [token, setToken] = React.useState(() => getItem('token')); + + const value = React.useMemo(() => { + return { + token, + updateToken: (newToken) => { + api.defaults.headers.Authorization = newToken; + setToken(newToken); + setItem('token', newToken); + }, + removeToken: () => { + delete api.defaults.headers.Authorization; + setToken(null); + removeItem('token'); + }, + isAuthenticated: Boolean(token), + }; + }, [token]); + + return ( + + {children} + + ); +}; diff --git a/packages/web/src/contexts/Editor.jsx b/packages/web/src/contexts/Editor.jsx new file mode 100644 index 0000000000000000000000000000000000000000..edc600cb5543e15c93ef4c681a07312e3aa311e4 --- /dev/null +++ b/packages/web/src/contexts/Editor.jsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +export const EditorContext = React.createContext({ + readOnly: false, +}); +export const EditorProvider = (props) => { + const { children, value } = props; + return ( + {children} + ); +}; diff --git a/packages/web/src/contexts/Paddle.ee.jsx b/packages/web/src/contexts/Paddle.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b37b5d132545fa7a6d6ad28fb22ada45e05de4c1 --- /dev/null +++ b/packages/web/src/contexts/Paddle.ee.jsx @@ -0,0 +1,102 @@ +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import * as URLS from 'config/urls'; +import useCloud from 'hooks/useCloud'; +import usePaddleInfo from 'hooks/usePaddleInfo.ee'; +import { useQueryClient } from '@tanstack/react-query'; + +export const PaddleContext = React.createContext({ + loaded: false, +}); + +export const PaddleProvider = (props) => { + const { children } = props; + const isCloud = useCloud(); + const navigate = useNavigate(); + const { data } = usePaddleInfo(); + const sandbox = data?.data?.sandbox; + const vendorId = data?.data?.vendorId; + const queryClient = useQueryClient(); + + const [loaded, setLoaded] = React.useState(false); + + const paddleEventHandler = React.useCallback( + async (payload) => { + const { event, eventData } = payload; + + if (event === 'Checkout.Close') { + const completed = eventData.checkout?.completed; + if (completed) { + // Paddle has side effects in the background, + // so we need to refetch the relevant queries + await queryClient.refetchQueries({ + queryKey: ['users', 'me', 'trial'], + }); + + await queryClient.refetchQueries({ + queryKey: ['users', 'me', 'subscription'], + }); + + navigate(URLS.SETTINGS_BILLING_AND_USAGE, { + state: { checkoutCompleted: true }, + }); + } + } + }, + [navigate, queryClient], + ); + + const value = React.useMemo(() => { + return { + loaded, + }; + }, [loaded]); + + React.useEffect( + function loadPaddleScript() { + if (!isCloud) return; + const isInjected = document.getElementById('paddle-js'); + + if (isInjected) { + setLoaded(true); + return; + } + const g = document.createElement('script'); + const s = document.getElementsByTagName('script')[0]; + g.src = 'https://cdn.paddle.com/paddle/paddle.js'; + g.defer = true; + g.async = true; + g.id = 'paddle-js'; + + if (s.parentNode) { + s.parentNode.insertBefore(g, s); + } + + g.onload = function () { + setLoaded(true); + }; + }, + [isCloud], + ); + + React.useEffect( + function initPaddleScript() { + if (!loaded || !vendorId) return; + + if (sandbox) { + window.Paddle.Environment.set('sandbox'); + } + + window.Paddle.Setup({ + vendor: vendorId, + eventCallback: paddleEventHandler, + }); + }, + [loaded, sandbox, vendorId, paddleEventHandler], + ); + + return ( + {children} + ); +}; diff --git a/packages/web/src/contexts/StepExecutions.jsx b/packages/web/src/contexts/StepExecutions.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d8ae1fc28b05c02e47ed8ed624682fb947c3c44d --- /dev/null +++ b/packages/web/src/contexts/StepExecutions.jsx @@ -0,0 +1,10 @@ +import * as React from 'react'; +export const StepExecutionsContext = React.createContext([]); +export const StepExecutionsProvider = (props) => { + const { children, value } = props; + return ( + + {children} + + ); +}; diff --git a/packages/web/src/graphql/cache.js b/packages/web/src/graphql/cache.js new file mode 100644 index 0000000000000000000000000000000000000000..cfa5ededd66616cd5995fcd398007d650a82110a --- /dev/null +++ b/packages/web/src/graphql/cache.js @@ -0,0 +1,42 @@ +import { InMemoryCache } from '@apollo/client'; +const cache = new InMemoryCache({ + typePolicies: { + App: { + keyFields: ['key'], + }, + Mutation: { + mutationType: true, + fields: { + verifyConnection: { + merge(existing, verifiedConnection, { readField, cache }) { + const appKey = readField('key', verifiedConnection); + const appCacheId = cache.identify({ + __typename: 'App', + key: appKey, + }); + cache.modify({ + id: appCacheId, + fields: { + connections: (existingConnections) => { + const existingConnectionIndex = existingConnections.findIndex( + (connection) => { + return connection.__ref === verifiedConnection.__ref; + } + ); + const connectionExists = existingConnectionIndex !== -1; + // newly created and verified connection + if (!connectionExists) { + return [verifiedConnection, ...existingConnections]; + } + return existingConnections; + }, + }, + }); + return verifiedConnection; + }, + }, + }, + }, + }, +}); +export default cache; diff --git a/packages/web/src/graphql/client.js b/packages/web/src/graphql/client.js new file mode 100644 index 0000000000000000000000000000000000000000..cbd2ee1b91bdf38b75723253b89a61c78beba889 --- /dev/null +++ b/packages/web/src/graphql/client.js @@ -0,0 +1,31 @@ +import { ApolloClient } from '@apollo/client'; + +import cache from './cache'; +import createLink from './link'; +import appConfig from 'config/app'; + +const client = new ApolloClient({ + cache, + link: createLink({ uri: appConfig.graphqlUrl }), + defaultOptions: { + watchQuery: { + fetchPolicy: 'cache-and-network', + }, + }, +}); + +export function mutateAndGetClient(options) { + const { onError, token } = options; + + const link = createLink({ + uri: appConfig.graphqlUrl, + token, + onError, + }); + + client.setLink(link); + + return client; +} + +export default client; diff --git a/packages/web/src/graphql/link.js b/packages/web/src/graphql/link.js new file mode 100644 index 0000000000000000000000000000000000000000..7e5d3c1d5770af523683d6d47cce49f408ec6832 --- /dev/null +++ b/packages/web/src/graphql/link.js @@ -0,0 +1,46 @@ +import { HttpLink, from } from '@apollo/client'; +import { onError } from '@apollo/client/link/error'; +import { setItem } from 'helpers/storage'; +import * as URLS from 'config/urls'; +const createHttpLink = (options) => { + const { uri, token } = options; + const headers = { + authorization: token, + }; + return new HttpLink({ uri, headers }); +}; +const NOT_AUTHORISED = 'Not Authorised!'; +const createErrorLink = (callback) => + onError(({ graphQLErrors, networkError, operation }) => { + const context = operation.getContext(); + const autoSnackbar = context.autoSnackbar ?? true; + if (graphQLErrors) + graphQLErrors.forEach(({ message, locations, path }) => { + if (autoSnackbar) { + callback?.(message); + } + console.error( + `[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`, + ); + if (message === NOT_AUTHORISED) { + setItem('token', ''); + if (window.location.pathname !== URLS.LOGIN) { + window.location.href = URLS.LOGIN; + } + } + }); + if (networkError) { + if (autoSnackbar) { + callback?.(networkError.toString()); + } + console.error(`[Network error]: ${networkError}`); + } + }); + +const noop = () => {}; +const createLink = (options) => { + const { uri, onError = noop, token } = options; + const httpOptions = { uri, token }; + return from([createErrorLink(onError), createHttpLink(httpOptions)]); +}; +export default createLink; diff --git a/packages/web/src/graphql/mutations/create-app-auth-client.js b/packages/web/src/graphql/mutations/create-app-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..edcbc1fd318b8d0b06b8efedc706fe6026fbfb40 --- /dev/null +++ b/packages/web/src/graphql/mutations/create-app-auth-client.js @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +export const CREATE_APP_AUTH_CLIENT = gql` + mutation CreateAppAuthClient($input: CreateAppAuthClientInput) { + createAppAuthClient(input: $input) { + id + appConfigId + name + active + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-app-config.js b/packages/web/src/graphql/mutations/create-app-config.js new file mode 100644 index 0000000000000000000000000000000000000000..f895014e132c670248d469f71293a8eed5291207 --- /dev/null +++ b/packages/web/src/graphql/mutations/create-app-config.js @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; +export const CREATE_APP_CONFIG = gql` + mutation CreateAppConfig($input: CreateAppConfigInput) { + createAppConfig(input: $input) { + id + key + allowCustomConnection + shared + disabled + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-connection.js b/packages/web/src/graphql/mutations/create-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..e3fb37c36ad5252f82d4b26c9e92b1b9c187d187 --- /dev/null +++ b/packages/web/src/graphql/mutations/create-connection.js @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; +export const CREATE_CONNECTION = gql` + mutation CreateConnection($input: CreateConnectionInput) { + createConnection(input: $input) { + id + key + verified + formattedData { + screenName + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-flow.js b/packages/web/src/graphql/mutations/create-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..912b8f13f767df27982d6f7c2718fec43cadd30f --- /dev/null +++ b/packages/web/src/graphql/mutations/create-flow.js @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; +export const CREATE_FLOW = gql` + mutation CreateFlow($input: CreateFlowInput) { + createFlow(input: $input) { + id + name + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-role.ee.js b/packages/web/src/graphql/mutations/create-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..be519627156023d39047cff6976a2292190c8fa5 --- /dev/null +++ b/packages/web/src/graphql/mutations/create-role.ee.js @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +export const CREATE_ROLE = gql` + mutation CreateRole($input: CreateRoleInput) { + createRole(input: $input) { + id + key + name + description + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-step.js b/packages/web/src/graphql/mutations/create-step.js new file mode 100644 index 0000000000000000000000000000000000000000..9b77b3f37560246b373392c8a7618dfc3c6edaba --- /dev/null +++ b/packages/web/src/graphql/mutations/create-step.js @@ -0,0 +1,19 @@ +import { gql } from '@apollo/client'; +export const CREATE_STEP = gql` + mutation CreateStep($input: CreateStepInput) { + createStep(input: $input) { + id + type + key + appKey + parameters + iconUrl + position + webhookUrl + status + connection { + id + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/create-user.ee.js b/packages/web/src/graphql/mutations/create-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..3945922e4b5dd5f642fc1e8f8a1a871ca4453d0b --- /dev/null +++ b/packages/web/src/graphql/mutations/create-user.ee.js @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; +export const CREATE_USER = gql` + mutation CreateUser($input: CreateUserInput) { + createUser(input: $input) { + id + email + fullName + role { + id + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/delete-connection.js b/packages/web/src/graphql/mutations/delete-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..734240e89528efd1d88839d3ff42d6b1eae0a378 --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-connection.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const DELETE_CONNECTION = gql` + mutation DeleteConnection($input: DeleteConnectionInput) { + deleteConnection(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/delete-current-user.ee.js b/packages/web/src/graphql/mutations/delete-current-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..da6054041150f86f24e472215365064d1c3f5850 --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-current-user.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const DELETE_CURRENT_USER = gql` + mutation DeleteCurrentUser { + deleteCurrentUser + } +`; diff --git a/packages/web/src/graphql/mutations/delete-flow.js b/packages/web/src/graphql/mutations/delete-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..4b7d1c72a39868f68e23247a73ec82e9e70cc27a --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-flow.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const DELETE_FLOW = gql` + mutation DeleteFlow($input: DeleteFlowInput) { + deleteFlow(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/delete-role.ee.js b/packages/web/src/graphql/mutations/delete-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..709f3b55ceb2a3d0f2cbfff5d7e3142dec6683ab --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-role.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const DELETE_ROLE = gql` + mutation DeleteRole($input: DeleteRoleInput) { + deleteRole(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/delete-step.js b/packages/web/src/graphql/mutations/delete-step.js new file mode 100644 index 0000000000000000000000000000000000000000..effb409ef01aab45a21161dbfa19e0a3447e584c --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-step.js @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; +export const DELETE_STEP = gql` + mutation DeleteStep($input: DeleteStepInput) { + deleteStep(input: $input) { + id + flow { + id + steps { + id + } + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/delete-user.ee.js b/packages/web/src/graphql/mutations/delete-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..c64916c3236dd94cb19f6c7938d55db35db0b1d5 --- /dev/null +++ b/packages/web/src/graphql/mutations/delete-user.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const DELETE_USER = gql` + mutation DeleteUser($input: DeleteUserInput) { + deleteUser(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/duplicate-flow.js b/packages/web/src/graphql/mutations/duplicate-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..6e359ff58dc9c5cfefa894e73326cc70be159b15 --- /dev/null +++ b/packages/web/src/graphql/mutations/duplicate-flow.js @@ -0,0 +1,27 @@ +import { gql } from '@apollo/client'; +export const DUPLICATE_FLOW = gql` + mutation DuplicateFlow($input: DuplicateFlowInput) { + duplicateFlow(input: $input) { + id + name + active + status + steps { + id + type + key + appKey + iconUrl + webhookUrl + status + position + connection { + id + verified + createdAt + } + parameters + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/execute-flow.js b/packages/web/src/graphql/mutations/execute-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..ba9c40b7cf3eda2a74f0ae2974743d24abaec53f --- /dev/null +++ b/packages/web/src/graphql/mutations/execute-flow.js @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; +export const EXECUTE_FLOW = gql` + mutation ExecuteFlow($input: ExecuteFlowInput) { + executeFlow(input: $input) { + step { + id + status + } + data + } + } +`; diff --git a/packages/web/src/graphql/mutations/forgot-password.ee.js b/packages/web/src/graphql/mutations/forgot-password.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..161a6fd673adf39f4e18b94654e3b6e787563f7c --- /dev/null +++ b/packages/web/src/graphql/mutations/forgot-password.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const FORGOT_PASSWORD = gql` + mutation ForgotPassword($input: ForgotPasswordInput) { + forgotPassword(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/generate-auth-url.js b/packages/web/src/graphql/mutations/generate-auth-url.js new file mode 100644 index 0000000000000000000000000000000000000000..44ce04310475fa4f1b31265d9dffd004373aadd9 --- /dev/null +++ b/packages/web/src/graphql/mutations/generate-auth-url.js @@ -0,0 +1,8 @@ +import { gql } from '@apollo/client'; +export const GENERATE_AUTH_URL = gql` + mutation generateAuthUrl($input: GenerateAuthUrlInput) { + generateAuthUrl(input: $input) { + url + } + } +`; diff --git a/packages/web/src/graphql/mutations/index.js b/packages/web/src/graphql/mutations/index.js new file mode 100644 index 0000000000000000000000000000000000000000..0033bc86a7fb0df197bc710fb0bd969b7c1fc876 --- /dev/null +++ b/packages/web/src/graphql/mutations/index.js @@ -0,0 +1,15 @@ +import { CREATE_CONNECTION } from './create-connection'; +import { UPDATE_CONNECTION } from './update-connection'; +import { VERIFY_CONNECTION } from './verify-connection'; +import { RESET_CONNECTION } from './reset-connection'; +import { DELETE_CONNECTION } from './delete-connection'; +import { GENERATE_AUTH_URL } from './generate-auth-url'; +const mutations = { + createConnection: CREATE_CONNECTION, + updateConnection: UPDATE_CONNECTION, + verifyConnection: VERIFY_CONNECTION, + resetConnection: RESET_CONNECTION, + deleteConnection: DELETE_CONNECTION, + generateAuthUrl: GENERATE_AUTH_URL, +}; +export default mutations; diff --git a/packages/web/src/graphql/mutations/login.js b/packages/web/src/graphql/mutations/login.js new file mode 100644 index 0000000000000000000000000000000000000000..c0f632a5ccf8ef763e91bc3989f761ab8a7bd170 --- /dev/null +++ b/packages/web/src/graphql/mutations/login.js @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; +export const LOGIN = gql` + mutation Login($input: LoginInput) { + login(input: $input) { + token + user { + id + email + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/register-user.ee.js b/packages/web/src/graphql/mutations/register-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..54f852ac699cd0950a922814ee73efb02a39d9c3 --- /dev/null +++ b/packages/web/src/graphql/mutations/register-user.ee.js @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; +export const REGISTER_USER = gql` + mutation RegisterUser($input: RegisterUserInput) { + registerUser(input: $input) { + id + email + fullName + } + } +`; diff --git a/packages/web/src/graphql/mutations/reset-connection.js b/packages/web/src/graphql/mutations/reset-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..adc02b24839c64320b18245162be9407d32fd792 --- /dev/null +++ b/packages/web/src/graphql/mutations/reset-connection.js @@ -0,0 +1,8 @@ +import { gql } from '@apollo/client'; +export const RESET_CONNECTION = gql` + mutation ResetConnection($input: ResetConnectionInput) { + resetConnection(input: $input) { + id + } + } +`; diff --git a/packages/web/src/graphql/mutations/reset-password.ee.js b/packages/web/src/graphql/mutations/reset-password.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..42360bcad1b92654d31f2b31f8f1264266f92199 --- /dev/null +++ b/packages/web/src/graphql/mutations/reset-password.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const RESET_PASSWORD = gql` + mutation ResetPassword($input: ResetPasswordInput) { + resetPassword(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/update-app-auth-client.js b/packages/web/src/graphql/mutations/update-app-auth-client.js new file mode 100644 index 0000000000000000000000000000000000000000..ff38749809b2dcfe75eb68204e4f29b50d446a95 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-app-auth-client.js @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client'; +export const UPDATE_APP_AUTH_CLIENT = gql` + mutation UpdateAppAuthClient($input: UpdateAppAuthClientInput) { + updateAppAuthClient(input: $input) { + id + appConfigId + name + active + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-app-config.js b/packages/web/src/graphql/mutations/update-app-config.js new file mode 100644 index 0000000000000000000000000000000000000000..76f5de10e792ad80184e77ae782afec01101844c --- /dev/null +++ b/packages/web/src/graphql/mutations/update-app-config.js @@ -0,0 +1,12 @@ +import { gql } from '@apollo/client'; +export const UPDATE_APP_CONFIG = gql` + mutation UpdateAppConfig($input: UpdateAppConfigInput) { + updateAppConfig(input: $input) { + id + key + allowCustomConnection + shared + disabled + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-config.ee.js b/packages/web/src/graphql/mutations/update-config.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..b916c62f56beae6dc15cd273b310b5a3bfcc4690 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-config.ee.js @@ -0,0 +1,6 @@ +import { gql } from '@apollo/client'; +export const UPDATE_CONFIG = gql` + mutation UpdateConfig($input: JSONObject) { + updateConfig(input: $input) + } +`; diff --git a/packages/web/src/graphql/mutations/update-connection.js b/packages/web/src/graphql/mutations/update-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..6b70cebec7e22a83a7099df7c0897e6f2235db1d --- /dev/null +++ b/packages/web/src/graphql/mutations/update-connection.js @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; +export const UPDATE_CONNECTION = gql` + mutation UpdateConnection($input: UpdateConnectionInput) { + updateConnection(input: $input) { + id + key + verified + formattedData { + screenName + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-current-user.js b/packages/web/src/graphql/mutations/update-current-user.js new file mode 100644 index 0000000000000000000000000000000000000000..936d5ee2d8fbba39ea18deff134d21fd6c4f767b --- /dev/null +++ b/packages/web/src/graphql/mutations/update-current-user.js @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; +export const UPDATE_CURRENT_USER = gql` + mutation UpdateCurrentUser($input: UpdateCurrentUserInput) { + updateCurrentUser(input: $input) { + id + fullName + email + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-flow-status.js b/packages/web/src/graphql/mutations/update-flow-status.js new file mode 100644 index 0000000000000000000000000000000000000000..862a5ad2314ce80fbfee075ee72fd3482a52c33b --- /dev/null +++ b/packages/web/src/graphql/mutations/update-flow-status.js @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; +export const UPDATE_FLOW_STATUS = gql` + mutation UpdateFlowStatus($input: UpdateFlowStatusInput) { + updateFlowStatus(input: $input) { + id + active + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-flow.js b/packages/web/src/graphql/mutations/update-flow.js new file mode 100644 index 0000000000000000000000000000000000000000..0ac5909ceeaedfea07633d96a88d910b3e33fdf5 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-flow.js @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; +export const UPDATE_FLOW = gql` + mutation UpdateFlow($input: UpdateFlowInput) { + updateFlow(input: $input) { + id + name + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-role.ee.js b/packages/web/src/graphql/mutations/update-role.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..15f741c6b3fcb6869c4facb45bdde2e1192f69cf --- /dev/null +++ b/packages/web/src/graphql/mutations/update-role.ee.js @@ -0,0 +1,16 @@ +import { gql } from '@apollo/client'; +export const UPDATE_ROLE = gql` + mutation UpdateRole($input: UpdateRoleInput) { + updateRole(input: $input) { + id + name + description + permissions { + id + action + subject + conditions + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-step.js b/packages/web/src/graphql/mutations/update-step.js new file mode 100644 index 0000000000000000000000000000000000000000..44d91a2b5fdf1585eb3912c369b4eec3f0972d00 --- /dev/null +++ b/packages/web/src/graphql/mutations/update-step.js @@ -0,0 +1,17 @@ +import { gql } from '@apollo/client'; +export const UPDATE_STEP = gql` + mutation UpdateStep($input: UpdateStepInput) { + updateStep(input: $input) { + id + type + key + appKey + parameters + status + webhookUrl + connection { + id + } + } + } +`; diff --git a/packages/web/src/graphql/mutations/update-user.ee.js b/packages/web/src/graphql/mutations/update-user.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..c73d8f1d91b3a326627436bfe04d555bf9d78f2e --- /dev/null +++ b/packages/web/src/graphql/mutations/update-user.ee.js @@ -0,0 +1,10 @@ +import { gql } from '@apollo/client'; +export const UPDATE_USER = gql` + mutation UpdateUser($input: UpdateUserInput) { + updateUser(input: $input) { + id + email + fullName + } + } +`; diff --git a/packages/web/src/graphql/mutations/upsert-saml-auth-provider.js b/packages/web/src/graphql/mutations/upsert-saml-auth-provider.js new file mode 100644 index 0000000000000000000000000000000000000000..575c846d91f1120deefd18dcf0cb8f0cdfb75d80 --- /dev/null +++ b/packages/web/src/graphql/mutations/upsert-saml-auth-provider.js @@ -0,0 +1,8 @@ +import { gql } from '@apollo/client'; +export const UPSERT_SAML_AUTH_PROVIDER = gql` + mutation UpsertSamlAuthProvider($input: UpsertSamlAuthProviderInput) { + upsertSamlAuthProvider(input: $input) { + id + } + } +`; diff --git a/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.js b/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.js new file mode 100644 index 0000000000000000000000000000000000000000..2d9180e08710d76bb5b397948379eea6d371d573 --- /dev/null +++ b/packages/web/src/graphql/mutations/upsert-saml-auth-providers-role-mappings.js @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client'; +export const UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS = gql` + mutation UpsertSamlAuthProvidersRoleMappings( + $input: UpsertSamlAuthProvidersRoleMappingsInput + ) { + upsertSamlAuthProvidersRoleMappings(input: $input) { + id + samlAuthProviderId + roleId + remoteRoleName + } + } +`; diff --git a/packages/web/src/graphql/mutations/verify-connection.js b/packages/web/src/graphql/mutations/verify-connection.js new file mode 100644 index 0000000000000000000000000000000000000000..954eed0bb89392ed5c6417a66c188724ccd22b02 --- /dev/null +++ b/packages/web/src/graphql/mutations/verify-connection.js @@ -0,0 +1,17 @@ +import { gql } from '@apollo/client'; +export const VERIFY_CONNECTION = gql` + mutation VerifyConnection($input: VerifyConnectionInput) { + verifyConnection(input: $input) { + id + key + verified + formattedData { + screenName + } + createdAt + app { + key + } + } + } +`; diff --git a/packages/web/src/graphql/pagination.js b/packages/web/src/graphql/pagination.js new file mode 100644 index 0000000000000000000000000000000000000000..3dd0a522e4008ea0a0643268614f0c3136e80201 --- /dev/null +++ b/packages/web/src/graphql/pagination.js @@ -0,0 +1,32 @@ +const makeEmptyData = () => { + return { + edges: [], + pageInfo: { + currentPage: 1, + totalPages: 1, + }, + }; +}; +function offsetLimitPagination(keyArgs = false) { + return { + keyArgs, + merge(existing, incoming, { args }) { + if (!existing) { + existing = makeEmptyData(); + } + if (!incoming || incoming === null) return existing; + const existingEdges = existing?.edges || []; + const incomingEdges = incoming.edges || []; + if (args) { + const newEdges = [...existingEdges, ...incomingEdges]; + return { + pageInfo: incoming.pageInfo, + edges: newEdges, + }; + } else { + return existing; + } + }, + }; +} +export default offsetLimitPagination; diff --git a/packages/web/src/graphql/queries/get-dynamic-fields.js b/packages/web/src/graphql/queries/get-dynamic-fields.js new file mode 100644 index 0000000000000000000000000000000000000000..1be206813bd50f24f39d93badc2fbfb25db94604 --- /dev/null +++ b/packages/web/src/graphql/queries/get-dynamic-fields.js @@ -0,0 +1,69 @@ +import { gql } from '@apollo/client'; +export const GET_DYNAMIC_FIELDS = gql` + query GetDynamicFields( + $stepId: String! + $key: String! + $parameters: JSONObject + ) { + getDynamicFields(stepId: $stepId, key: $key, parameters: $parameters) { + label + key + type + required + description + variables + dependsOn + value + options { + label + value + } + source { + type + name + arguments { + name + value + } + } + additionalFields { + type + name + arguments { + name + value + } + } + fields { + label + key + type + required + description + variables + value + dependsOn + options { + label + value + } + source { + type + name + arguments { + name + value + } + } + additionalFields { + type + name + arguments { + name + value + } + } + } + } + } +`; diff --git a/packages/web/src/helpers/api.js b/packages/web/src/helpers/api.js new file mode 100644 index 0000000000000000000000000000000000000000..354babddb0d1b6338fba433aacc7fb9abfd57017 --- /dev/null +++ b/packages/web/src/helpers/api.js @@ -0,0 +1,34 @@ +import axios from 'axios'; + +import appConfig from 'config/app.js'; +import * as URLS from 'config/urls.js'; +import { getItem, removeItem } from 'helpers/storage.js'; + +const api = axios.create({ + ...axios.defaults, + baseURL: appConfig.restApiUrl, + headers: { + Authorization: getItem('token'), + }, +}); + +api.interceptors.response.use( + (response) => response, + (error) => { + const status = error.response?.status; + + if (status === 401) { + removeItem('token'); + + // hard reload to clear all state + if (window.location.pathname !== URLS.LOGIN) { + window.location.href = URLS.LOGIN; + } + } + + // re-throw what's already intercepted here. + throw error; + }, +); + +export default api; diff --git a/packages/web/src/helpers/authenticationSteps.js b/packages/web/src/helpers/authenticationSteps.js new file mode 100644 index 0000000000000000000000000000000000000000..4e4941b0eed131284651c32c22f73b047f169230 --- /dev/null +++ b/packages/web/src/helpers/authenticationSteps.js @@ -0,0 +1,70 @@ +import apolloClient from 'graphql/client'; +import MUTATIONS from 'graphql/mutations'; +var AuthenticationSteps; +(function (AuthenticationSteps) { + AuthenticationSteps['Mutation'] = 'mutation'; + AuthenticationSteps['OpenWithPopup'] = 'openWithPopup'; +})(AuthenticationSteps || (AuthenticationSteps = {})); + +const processMutation = async (step, variables) => { + const mutation = MUTATIONS[step.name]; + const mutationResponse = await apolloClient.mutate({ + mutation, + variables: { input: variables }, + context: { + autoSnackbar: false, + }, + }); + const responseData = mutationResponse.data[step.name]; + return responseData; +}; +const parseUrlSearchParams = (event) => { + const searchParams = new URLSearchParams(event.data.payload.search); + const hashParams = new URLSearchParams(event.data.payload.hash.substring(1)); + const searchParamsObject = getObjectOfEntries(searchParams.entries()); + const hashParamsObject = getObjectOfEntries(hashParams.entries()); + return { + ...hashParamsObject, + ...searchParamsObject, + }; +}; +function getObjectOfEntries(iterator) { + const result = {}; + for (const [key, value] of iterator) { + result[key] = value; + } + return result; +} +const processOpenWithPopup = (step, variables) => { + return new Promise((resolve, reject) => { + const windowFeatures = + 'toolbar=no, titlebar=no, menubar=no, width=500, height=700, top=100, left=100'; + const url = variables.url; + const popup = window.open(url, '_blank', windowFeatures); + popup?.focus(); + const closeCheckIntervalId = setInterval(() => { + if (!popup) return; + if (popup?.closed) { + clearInterval(closeCheckIntervalId); + reject({ message: 'Error occured while verifying credentials!' }); + } + }, 1000); + const messageHandler = async (event) => { + if (event.data.source !== 'automatisch') { + return; + } + const data = parseUrlSearchParams(event); + window.removeEventListener('message', messageHandler); + clearInterval(closeCheckIntervalId); + resolve(data); + }; + window.addEventListener('message', messageHandler, false); + }); +}; +export const processStep = async (step, variables) => { + if (step.type === AuthenticationSteps.Mutation) { + return processMutation(step, variables); + } else if (step.type === AuthenticationSteps.OpenWithPopup) { + return processOpenWithPopup(step, variables); + } +}; diff --git a/packages/web/src/helpers/computeAuthStepVariables.js b/packages/web/src/helpers/computeAuthStepVariables.js new file mode 100644 index 0000000000000000000000000000000000000000..d49bdb5fb895a21d6a4caf432349a50e23b6fcd4 --- /dev/null +++ b/packages/web/src/helpers/computeAuthStepVariables.js @@ -0,0 +1,27 @@ +import template from 'lodash/template'; +const interpolate = /{([\s\S]+?)}/g; +const computeAuthStepVariables = (variableSchema, aggregatedData) => { + const variables = {}; + for (const variable of variableSchema) { + if (variable.properties) { + variables[variable.name] = computeAuthStepVariables( + variable.properties, + aggregatedData + ); + continue; + } + if (variable.value) { + if (variable.value.endsWith('.all}')) { + const key = variable.value.replace('{', '').replace('.all}', ''); + variables[variable.name] = aggregatedData[key]; + continue; + } + const computedVariable = template(variable.value, { interpolate })( + aggregatedData + ); + variables[variable.name] = computedVariable; + } + } + return variables; +}; +export default computeAuthStepVariables; diff --git a/packages/web/src/helpers/computePermissions.ee.js b/packages/web/src/helpers/computePermissions.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7c6ae20e0eabab434d93949825c7a0d8e6c097fe --- /dev/null +++ b/packages/web/src/helpers/computePermissions.ee.js @@ -0,0 +1,47 @@ +export function getRoleWithComputedPermissions(role) { + if (!role) return {}; + const computedPermissions = role.permissions?.reduce( + (computedPermissions, permission) => ({ + ...computedPermissions, + [permission.subject]: { + ...(computedPermissions[permission.subject] || {}), + [permission.action]: { + conditions: Object.fromEntries( + permission.conditions.map((condition) => [condition, true]), + ), + value: true, + }, + }, + }), + {}, + ); + + return { + ...role, + computedPermissions, + }; +} +export function getPermissions(computedPermissions) { + if (!computedPermissions) return []; + + return Object.entries(computedPermissions).reduce( + (permissions, computedPermissionEntry) => { + const [subject, actionsWithConditions] = computedPermissionEntry; + for (const action in actionsWithConditions) { + const { value: permitted, conditions = {} } = + actionsWithConditions[action]; + if (permitted) { + permissions.push({ + action, + subject, + conditions: Object.entries(conditions) + .filter(([, enabled]) => enabled) + .map(([condition]) => condition), + }); + } + } + return permissions; + }, + [], + ); +} diff --git a/packages/web/src/helpers/computeVariables.js b/packages/web/src/helpers/computeVariables.js new file mode 100644 index 0000000000000000000000000000000000000000..a73a65d08666fecf6ef4838c7073446f0972cb33 --- /dev/null +++ b/packages/web/src/helpers/computeVariables.js @@ -0,0 +1,22 @@ +import template from 'lodash/template'; +const interpolate = /{([\s\S]+?)}/g; +const computeAuthStepVariables = (variableSchema, aggregatedData) => { + const variables = {}; + for (const variable of variableSchema) { + if (variable.properties) { + variables[variable.name] = computeAuthStepVariables( + variable.properties, + aggregatedData + ); + continue; + } + if (variable.value) { + const computedVariable = template(variable.value, { interpolate })( + aggregatedData + ); + variables[variable.name] = computedVariable; + } + } + return variables; +}; +export default computeAuthStepVariables; diff --git a/packages/web/src/helpers/copyInputValue.js b/packages/web/src/helpers/copyInputValue.js new file mode 100644 index 0000000000000000000000000000000000000000..f2d269562f990c63cff7d42bc8c7357615b886a8 --- /dev/null +++ b/packages/web/src/helpers/copyInputValue.js @@ -0,0 +1,4 @@ +import copy from 'clipboard-copy'; +export default function copyInputValue(element) { + copy(element.value); +} diff --git a/packages/web/src/helpers/filterObject.js b/packages/web/src/helpers/filterObject.js new file mode 100644 index 0000000000000000000000000000000000000000..3e9f947321f6c7c2ef998c1083ac12355fa852c1 --- /dev/null +++ b/packages/web/src/helpers/filterObject.js @@ -0,0 +1,44 @@ +import get from 'lodash/get'; +import set from 'lodash/set'; +import forIn from 'lodash/forIn'; +import isPlainObject from 'lodash/isPlainObject'; +export default function filterObject( + data, + searchTerm, + result = {}, + prefix = [], + withinArray = false +) { + if (withinArray) { + const containerValue = get(result, prefix, []); + result = filterObject( + data, + searchTerm, + result, + prefix.concat(containerValue.length.toString()) + ); + return result; + } + if (isPlainObject(data)) { + forIn(data, (value, key) => { + const fullKey = [...prefix, key]; + if (key.toLowerCase().includes(searchTerm)) { + set(result, fullKey, value); + return; + } + result = filterObject(value, searchTerm, result, fullKey); + }); + } + if (Array.isArray(data)) { + forIn(data, (value) => { + result = filterObject(value, searchTerm, result, prefix, true); + }); + } + if ( + ['string', 'number'].includes(typeof data) && + String(data).toLowerCase().includes(searchTerm) + ) { + set(result, prefix, data); + } + return result; +} diff --git a/packages/web/src/helpers/isEmpty.js b/packages/web/src/helpers/isEmpty.js new file mode 100644 index 0000000000000000000000000000000000000000..6d55bc84dfaf1e7802d432698173f7fd2df268a7 --- /dev/null +++ b/packages/web/src/helpers/isEmpty.js @@ -0,0 +1,12 @@ +import lodashIsEmpty from 'lodash/isEmpty'; +export default function isEmpty(value) { + if (value === undefined && value === null) return true; + if (Array.isArray(value) || typeof value === 'string') { + return value.length === 0; + } + if (!Number.isNaN(value)) { + return false; + } + // covers objects and anything else possibly + return lodashIsEmpty(value); +} diff --git a/packages/web/src/helpers/nestObject.js b/packages/web/src/helpers/nestObject.js new file mode 100644 index 0000000000000000000000000000000000000000..aa23b1e687d1bfd45218ed8b08f79aa5f0d8aa9e --- /dev/null +++ b/packages/web/src/helpers/nestObject.js @@ -0,0 +1,12 @@ +import set from 'lodash/set'; +export default function nestObject(config) { + if (!config) return {}; + const result = {}; + for (const key in config) { + if (Object.prototype.hasOwnProperty.call(config, key)) { + const value = config[key]; + set(result, key, value); + } + } + return result; +} diff --git a/packages/web/src/helpers/storage.js b/packages/web/src/helpers/storage.js new file mode 100644 index 0000000000000000000000000000000000000000..25908ef5ce5c38eb00f2be8976c9883b24907fe7 --- /dev/null +++ b/packages/web/src/helpers/storage.js @@ -0,0 +1,14 @@ +const NAMESPACE = 'automatisch'; +const makeKey = (key) => `${NAMESPACE}.${key}`; + +export const setItem = (key, value) => { + return localStorage.setItem(makeKey(key), value); +}; + +export const getItem = (key) => { + return localStorage.getItem(makeKey(key)); +}; + +export const removeItem = (key) => { + return localStorage.removeItem(makeKey(key)); +}; diff --git a/packages/web/src/helpers/translationValues.jsx b/packages/web/src/helpers/translationValues.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f5c9855cb43d622607b710446606bc01429e7d42 --- /dev/null +++ b/packages/web/src/helpers/translationValues.jsx @@ -0,0 +1,12 @@ +import { Link as RouterLink } from 'react-router-dom'; +import Link from '@mui/material/Link'; +export const generateInternalLink = (link) => (str) => ( + + {str} + +); +export const generateExternalLink = (link) => (str) => ( + + {str} + +); diff --git a/packages/web/src/helpers/userAbility.js b/packages/web/src/helpers/userAbility.js new file mode 100644 index 0000000000000000000000000000000000000000..4114ad253c9e2f204971e055b191e6e36a9f89af --- /dev/null +++ b/packages/web/src/helpers/userAbility.js @@ -0,0 +1,19 @@ +import { + PureAbility, + fieldPatternMatcher, + mongoQueryMatcher, +} from '@casl/ability'; +// Must be kept in sync with `packages/backend/src/helpers/user-ability.ts`! +export default function userAbility(user) { + const permissions = user?.permissions; + const role = user?.role; + // We're not using mongo, but our fields, conditions match + const options = { + conditionsMatcher: mongoQueryMatcher, + fieldMatcher: fieldPatternMatcher, + }; + if (!role || !permissions) { + return new PureAbility([], options); + } + return new PureAbility(permissions, options); +} diff --git a/packages/web/src/hooks/useActionSubsteps.js b/packages/web/src/hooks/useActionSubsteps.js new file mode 100644 index 0000000000000000000000000000000000000000..9ff7a9510ae5003eb1dc291ab023042e50ad64eb --- /dev/null +++ b/packages/web/src/hooks/useActionSubsteps.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useActionSubsteps({ appKey, actionKey }) { + const query = useQuery({ + queryKey: ['apps', appKey, 'actions', actionKey, 'substeps'], + queryFn: async ({ signal }) => { + const { data } = await api.get( + `/v1/apps/${appKey}/actions/${actionKey}/substeps`, + { + signal, + }, + ); + + return data; + }, + enabled: !!appKey && !!actionKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useActions.js b/packages/web/src/hooks/useActions.js new file mode 100644 index 0000000000000000000000000000000000000000..51d56927de7b837a6999b73638899cdbe9eabc40 --- /dev/null +++ b/packages/web/src/hooks/useActions.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useActions(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'actions'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/actions`, { + signal, + }); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminAppAuthClient.ee.js b/packages/web/src/hooks/useAdminAppAuthClient.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..3ad618b9e06736de11a8dcf6ef8c3d9e94f48bd5 --- /dev/null +++ b/packages/web/src/hooks/useAdminAppAuthClient.ee.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAdminAppAuthClient(id) { + const query = useQuery({ + queryKey: ['admin', 'appAuthClients', id], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/app-auth-clients/${id}`, { + signal, + }); + + return data; + }, + enabled: !!id, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminAppAuthClients.js b/packages/web/src/hooks/useAdminAppAuthClients.js new file mode 100644 index 0000000000000000000000000000000000000000..22de022f9c05808b6c7afe2ebafd8f9f19d24b02 --- /dev/null +++ b/packages/web/src/hooks/useAdminAppAuthClients.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAdminAppAuthClient(appKey) { + const query = useQuery({ + queryKey: ['admin', 'apps', appKey, 'auth-clients'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/apps/${appKey}/auth-clients`, { + signal, + }); + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminSamlAuthProviderRoleMappings.js b/packages/web/src/hooks/useAdminSamlAuthProviderRoleMappings.js new file mode 100644 index 0000000000000000000000000000000000000000..a523374baff0f37d393805a3e42a06cd0c9913af --- /dev/null +++ b/packages/web/src/hooks/useAdminSamlAuthProviderRoleMappings.js @@ -0,0 +1,24 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAdminSamlAuthProviderRoleMappings({ + adminSamlAuthProviderId: providerId, +}) { + const query = useQuery({ + queryKey: ['admin', 'samlAuthProviders', providerId, 'roleMappings'], + queryFn: async ({ signal }) => { + const { data } = await api.get( + `/v1/admin/saml-auth-providers/${providerId}/role-mappings`, + { + signal, + }, + ); + + return data; + }, + enabled: !!providerId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminSamlAuthProviders.ee.js b/packages/web/src/hooks/useAdminSamlAuthProviders.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..c557fcfd461f6dba26b37198e6bc2c191ba0db31 --- /dev/null +++ b/packages/web/src/hooks/useAdminSamlAuthProviders.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAdminSamlAuthProviders() { + const query = useQuery({ + queryKey: ['admin', 'samlAuthProviders'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/admin/saml-auth-providers', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminUser.js b/packages/web/src/hooks/useAdminUser.js new file mode 100644 index 0000000000000000000000000000000000000000..1d010675180e2f2236b0011e061e6a8e97d680dc --- /dev/null +++ b/packages/web/src/hooks/useAdminUser.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAdminUser({ userId }) { + const query = useQuery({ + queryKey: ['admin', 'users', userId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/users/${userId}`, { + signal, + }); + return data; + }, + enabled: !!userId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAdminUsers.js b/packages/web/src/hooks/useAdminUsers.js new file mode 100644 index 0000000000000000000000000000000000000000..a2b5341bf8a1ac6345dcd3b0da177b3651f37ea6 --- /dev/null +++ b/packages/web/src/hooks/useAdminUsers.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAdminUsers(page) { + const query = useQuery({ + queryKey: ['admin', 'users', { page }], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/users`, { + signal, + params: { page }, + }); + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useApp.js b/packages/web/src/hooks/useApp.js new file mode 100644 index 0000000000000000000000000000000000000000..e9ae6f6dd9ccf081e547aff7f04a73a4ec5b9f28 --- /dev/null +++ b/packages/web/src/hooks/useApp.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useApp(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}`, { + signal, + }); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAppAuth.js b/packages/web/src/hooks/useAppAuth.js new file mode 100644 index 0000000000000000000000000000000000000000..0fbaf3c0d9f5704c47f8f1a6991723a17cfd85b7 --- /dev/null +++ b/packages/web/src/hooks/useAppAuth.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAppAuth(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'auth'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/auth`, { + signal, + }); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAppAuthClients.js b/packages/web/src/hooks/useAppAuthClients.js new file mode 100644 index 0000000000000000000000000000000000000000..1524c3ac2900700a4d36ae8a741c041f7bc88e01 --- /dev/null +++ b/packages/web/src/hooks/useAppAuthClients.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAppAuthClients(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'auth-clients'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/auth-clients`, { + signal, + }); + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAppConfig.ee.js b/packages/web/src/hooks/useAppConfig.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2aa9df81cca0f63b5c1d83fc9b02670ea06fa16f --- /dev/null +++ b/packages/web/src/hooks/useAppConfig.ee.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAppConfig(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'config'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/config`, { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAppConnections.js b/packages/web/src/hooks/useAppConnections.js new file mode 100644 index 0000000000000000000000000000000000000000..9e256d1c63de34303960170a8798a92211cb6ca8 --- /dev/null +++ b/packages/web/src/hooks/useAppConnections.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAppConnections(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'connections'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/connections`, { + signal, + }); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAppFlows.js b/packages/web/src/hooks/useAppFlows.js new file mode 100644 index 0000000000000000000000000000000000000000..b23af8e3e9bf4a7e40d6a8cb1dd301f4ac68fa3d --- /dev/null +++ b/packages/web/src/hooks/useAppFlows.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useAppFlows({ appKey, page }, { enabled }) { + const query = useQuery({ + queryKey: ['apps', appKey, 'flows', { page }], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/flows`, { + params: { + page, + }, + signal, + }); + + return data; + }, + enabled, + }); + + return query; +} diff --git a/packages/web/src/hooks/useApps.js b/packages/web/src/hooks/useApps.js new file mode 100644 index 0000000000000000000000000000000000000000..4ec5d3c4f46a12e06d18e122068568c60944949d --- /dev/null +++ b/packages/web/src/hooks/useApps.js @@ -0,0 +1,27 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useApps(variables) { + const trueOnlyVariables = + variables && + Object.fromEntries( + Object.entries(variables).filter( + ([key, value]) => value === true || key === 'name', + ), + ); + + const query = useQuery({ + queryKey: ['apps', trueOnlyVariables], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/apps', { + params: trueOnlyVariables, + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAuthenticateApp.ee.js b/packages/web/src/hooks/useAuthenticateApp.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..68c8af8a657b0a9813a47b9a47d40b65fb571f3b --- /dev/null +++ b/packages/web/src/hooks/useAuthenticateApp.ee.js @@ -0,0 +1,71 @@ +import * as React from 'react'; + +import { processStep } from 'helpers/authenticationSteps'; +import computeAuthStepVariables from 'helpers/computeAuthStepVariables'; +import useAppAuth from './useAppAuth'; + +function getSteps(auth, hasConnection, useShared) { + if (hasConnection) { + if (useShared) { + return auth?.sharedReconnectionSteps; + } + return auth?.reconnectionSteps; + } + + if (useShared) { + return auth?.sharedAuthenticationSteps; + } + + return auth?.authenticationSteps; +} + +export default function useAuthenticateApp(payload) { + const { appKey, appAuthClientId, connectionId, useShared = false } = payload; + const { data: auth } = useAppAuth(appKey); + const [authenticationInProgress, setAuthenticationInProgress] = + React.useState(false); + const steps = getSteps(auth?.data, !!connectionId, useShared); + + const authenticate = React.useMemo(() => { + if (!steps?.length) return; + + return async function authenticate(payload = {}) { + const { fields } = payload; + setAuthenticationInProgress(true); + const response = { + key: appKey, + appAuthClientId: appAuthClientId || payload.appAuthClientId, + connection: { + id: connectionId, + }, + fields, + }; + let stepIndex = 0; + + while (stepIndex < steps?.length) { + const step = steps[stepIndex]; + const variables = computeAuthStepVariables(step.arguments, response); + + try { + const stepResponse = await processStep(step, variables); + response[step.name] = stepResponse; + } catch (err) { + console.log(err); + setAuthenticationInProgress(false); + throw err; + } + stepIndex++; + + if (stepIndex === steps.length) { + return response; + } + setAuthenticationInProgress(false); + } + }; + }, [steps, appKey, appAuthClientId, connectionId]); + + return { + authenticate, + inProgress: authenticationInProgress, + }; +} diff --git a/packages/web/src/hooks/useAuthentication.js b/packages/web/src/hooks/useAuthentication.js new file mode 100644 index 0000000000000000000000000000000000000000..58aad244d2a97567ad1e6b3827cb7189eb72cbdb --- /dev/null +++ b/packages/web/src/hooks/useAuthentication.js @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { AuthenticationContext } from 'contexts/Authentication'; + +export default function useAuthentication() { + const authenticationContext = React.useContext(AuthenticationContext); + + return authenticationContext; +} diff --git a/packages/web/src/hooks/useAutomatischConfig.js b/packages/web/src/hooks/useAutomatischConfig.js new file mode 100644 index 0000000000000000000000000000000000000000..93d93dafacb2417d88210e7d6581ada3b504ab5c --- /dev/null +++ b/packages/web/src/hooks/useAutomatischConfig.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAutomatischConfig() { + const query = useQuery({ + queryKey: ['automatisch', 'config'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/automatisch/config`, { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAutomatischInfo.js b/packages/web/src/hooks/useAutomatischInfo.js new file mode 100644 index 0000000000000000000000000000000000000000..f7ee73b17fcca102d8f2f9e83800aad2eb5edd9c --- /dev/null +++ b/packages/web/src/hooks/useAutomatischInfo.js @@ -0,0 +1,20 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAutomatischInfo() { + const query = useQuery({ + /** + * The data doesn't change by user actions, but only by server deployments. + * So we can set the `staleTime` to Infinity + **/ + staleTime: Infinity, + queryKey: ['automatisch', 'info'], + queryFn: async (payload, signal) => { + const { data } = await api.get('/v1/automatisch/info', { signal }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useAutomatischNotifications.js b/packages/web/src/hooks/useAutomatischNotifications.js new file mode 100644 index 0000000000000000000000000000000000000000..3e52a7cedd1572f727870d45cc756bebd82a9738 --- /dev/null +++ b/packages/web/src/hooks/useAutomatischNotifications.js @@ -0,0 +1,16 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useAutomatischNotifications() { + const query = useQuery({ + queryKey: ['automatisch', 'notifications'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/automatisch/notifications`, { + signal, + }); + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useCloud.js b/packages/web/src/hooks/useCloud.js new file mode 100644 index 0000000000000000000000000000000000000000..b9b557cb58d46c10fb6d0d281c7333ce3a8a0b0b --- /dev/null +++ b/packages/web/src/hooks/useCloud.js @@ -0,0 +1,17 @@ +import { useNavigate } from 'react-router-dom'; + +import useAutomatischInfo from './useAutomatischInfo'; + +export default function useCloud(options) { + const redirect = options?.redirect || false; + const navigate = useNavigate(); + const { data: automatischInfo } = useAutomatischInfo(); + + const isCloud = automatischInfo?.data.isCloud; + + if (isCloud === false && redirect) { + navigate('/'); + } + + return isCloud; +} diff --git a/packages/web/src/hooks/useConnectionFlows.js b/packages/web/src/hooks/useConnectionFlows.js new file mode 100644 index 0000000000000000000000000000000000000000..02e7af65af04a4ad256c8ff2e3c3b43d8944ffe5 --- /dev/null +++ b/packages/web/src/hooks/useConnectionFlows.js @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useConnectionFlows( + { connectionId, page }, + { enabled } = {}, +) { + const query = useQuery({ + queryKey: ['connections', connectionId, 'flows', { page }], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/connections/${connectionId}/flows`, { + params: { + page, + }, + signal, + }); + + return data; + }, + enabled, + }); + + return query; +} diff --git a/packages/web/src/hooks/useCurrentUser.js b/packages/web/src/hooks/useCurrentUser.js new file mode 100644 index 0000000000000000000000000000000000000000..cdac09379b097f3f06efbdb538ea1fd8f820780d --- /dev/null +++ b/packages/web/src/hooks/useCurrentUser.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useCurrentUser() { + const query = useQuery({ + queryKey: ['users', 'me'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/users/me`, { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useCurrentUserAbility.js b/packages/web/src/hooks/useCurrentUserAbility.js new file mode 100644 index 0000000000000000000000000000000000000000..169d06a5c5573723c698bc3e349d527ee58ec6a4 --- /dev/null +++ b/packages/web/src/hooks/useCurrentUserAbility.js @@ -0,0 +1,8 @@ +import userAbility from 'helpers/userAbility'; +import useCurrentUser from 'hooks/useCurrentUser'; + +export default function useCurrentUserAbility() { + const { data: currentUser } = useCurrentUser(); + + return userAbility(currentUser?.data); +} diff --git a/packages/web/src/hooks/useDynamicData.js b/packages/web/src/hooks/useDynamicData.js new file mode 100644 index 0000000000000000000000000000000000000000..79b02f35ba8fab3da2c835a9c4335aefa66de669 --- /dev/null +++ b/packages/web/src/hooks/useDynamicData.js @@ -0,0 +1,104 @@ +import * as React from 'react'; +import { useFormContext } from 'react-hook-form'; +import set from 'lodash/set'; +import { useMutation } from '@tanstack/react-query'; +import isEqual from 'lodash/isEqual'; + +import api from 'helpers/api'; + +const variableRegExp = /({.*?})/; + +function computeArguments(args, getValues) { + const initialValue = {}; + + return args.reduce((result, { name, value }) => { + const isVariable = variableRegExp.test(value); + if (isVariable) { + const sanitizedFieldPath = value.replace(/{|}/g, ''); + const computedValue = getValues(sanitizedFieldPath); + if (computedValue === undefined) + throw new Error(`The ${sanitizedFieldPath} field is required.`); + set(result, name, computedValue); + return result; + } + + set(result, name, value); + return result; + }, initialValue); +} +/** + * Fetch the dynamic data for the given step. + * This hook must be within a react-hook-form context. + * + * @param stepId - the id of the step + * @param schema - the field that needs the dynamic data + */ +function useDynamicData(stepId, schema) { + const lastComputedVariables = React.useRef({}); + + const { + data, + isPending: isDynamicDataPending, + mutate: getDynamicData, + } = useMutation({ + mutationFn: async (variables) => { + const { data } = await api.post( + `/v1/steps/${stepId}/dynamic-data`, + variables, + ); + + return data; + }, + }); + + const { getValues } = useFormContext(); + const formValues = getValues(); + /** + * Return `null` when even a field is missing value. + * + * This must return the same reference if no computed variable is changed. + * Otherwise, it causes redundant network request! + */ + const computedVariables = React.useMemo(() => { + if (schema.type === 'dropdown' && schema.source) { + try { + const variables = computeArguments(schema.source.arguments, getValues); + // if computed variables are the same, return the last computed variables. + if (isEqual(variables, lastComputedVariables.current)) { + return lastComputedVariables.current; + } + lastComputedVariables.current = variables; + return variables; + } catch (err) { + return null; + } + } + return null; + /** + * `formValues` is to trigger recomputation when form is updated. + * `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`. + */ + }, [schema, formValues, getValues]); + + React.useEffect(() => { + if ( + schema.type === 'dropdown' && + stepId && + schema.source && + computedVariables + ) { + const { key, parameters } = computedVariables; + + getDynamicData({ + dynamicDataKey: key, + parameters, + }); + } + }, [getDynamicData, stepId, schema, computedVariables]); + + return { + data: data?.data, + loading: isDynamicDataPending, + }; +} +export default useDynamicData; diff --git a/packages/web/src/hooks/useDynamicFields.js b/packages/web/src/hooks/useDynamicFields.js new file mode 100644 index 0000000000000000000000000000000000000000..80a0a4008059aadf2cb1b4dac484cd522e7c4054 --- /dev/null +++ b/packages/web/src/hooks/useDynamicFields.js @@ -0,0 +1,96 @@ +import * as React from 'react'; +import { useFormContext } from 'react-hook-form'; +import set from 'lodash/set'; +import isEqual from 'lodash/isEqual'; +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +const variableRegExp = /({.*?})/; +// TODO: extract this function to a separate file +function computeArguments(args, getValues) { + const initialValue = {}; + + return args.reduce((result, { name, value }) => { + const isVariable = variableRegExp.test(value); + + if (isVariable) { + const sanitizedFieldPath = value.replace(/{|}/g, ''); + const computedValue = getValues(sanitizedFieldPath); + + if (computedValue === undefined || computedValue === '') + throw new Error(`The ${sanitizedFieldPath} field is required.`); + + set(result, name, computedValue); + return result; + } + + set(result, name, value); + return result; + }, initialValue); +} + +/** + * Fetch the dynamic fields for the given step. + * This hook must be within a react-hook-form context. + * + * @param stepId - the id of the step + * @param schema - the field schema that needs the dynamic fields + */ +function useDynamicFields(stepId, schema) { + const lastComputedVariables = React.useRef({}); + const { getValues } = useFormContext(); + const formValues = getValues(); + + /** + * Return `null` when even a field is missing value. + * + * This must return the same reference if no computed variable is changed. + * Otherwise, it causes redundant network request! + */ + const computedVariables = React.useMemo(() => { + if (schema.type === 'dropdown' && schema.additionalFields) { + try { + const variables = computeArguments( + schema.additionalFields.arguments, + getValues, + ); + // if computed variables are the same, return the last computed variables. + if (isEqual(variables, lastComputedVariables.current)) { + return lastComputedVariables.current; + } + lastComputedVariables.current = variables; + return variables; + } catch (err) { + return null; + } + } + + return null; + /** + * `formValues` is to trigger recomputation when form is updated. + * `getValues` is for convenience as it supports paths for fields like `getValues('foo.bar.baz')`. + */ + }, [schema, formValues, getValues]); + + const query = useQuery({ + queryKey: ['steps', stepId, 'dynamicFields', computedVariables], + queryFn: async ({ signal }) => { + const { data } = await api.post( + `/v1/steps/${stepId}/dynamic-fields`, + { + dynamicFieldsKey: computedVariables.key, + parameters: computedVariables.parameters, + }, + { signal }, + ); + + return data; + }, + enabled: !!stepId && !!computedVariables, + }); + + return query; +} + +export default useDynamicFields; diff --git a/packages/web/src/hooks/useEnqueueSnackbar.js b/packages/web/src/hooks/useEnqueueSnackbar.js new file mode 100644 index 0000000000000000000000000000000000000000..5010be909456d4919b410e631e1245bc2ec21f08 --- /dev/null +++ b/packages/web/src/hooks/useEnqueueSnackbar.js @@ -0,0 +1,18 @@ +import { useSnackbar } from 'notistack'; +export default function useEnqueueSnackbar() { + const { enqueueSnackbar, closeSnackbar } = useSnackbar(); + return function wrappedEnqueueSnackbar(message, options) { + const key = enqueueSnackbar(message, { + ...(options || {}), + SnackbarProps: { + onClick: () => closeSnackbar(key), + ...{ + 'data-test': 'snackbar', + 'data-snackbar-variant': `${options.variant}` || 'default', + }, + ...(options.SnackbarProps || {}), + }, + }); + return key; + }; +} diff --git a/packages/web/src/hooks/useExecution.js b/packages/web/src/hooks/useExecution.js new file mode 100644 index 0000000000000000000000000000000000000000..d0a4fdf630ac06f1d9f809474f81e90f0d4a6f41 --- /dev/null +++ b/packages/web/src/hooks/useExecution.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useExecution({ executionId }) { + const query = useQuery({ + queryKey: ['executions', executionId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/executions/${executionId}`, { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useExecutionSteps.js b/packages/web/src/hooks/useExecutionSteps.js new file mode 100644 index 0000000000000000000000000000000000000000..03e1d4353b8ce1f925ffba3ed0b75ed104df956b --- /dev/null +++ b/packages/web/src/hooks/useExecutionSteps.js @@ -0,0 +1,29 @@ +import { useInfiniteQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useExecutionSteps({ executionId }) { + const query = useInfiniteQuery({ + queryKey: ['executions', executionId, 'executionSteps'], + queryFn: async ({ pageParam = 1, signal }) => { + const { data } = await api.get( + `/v1/executions/${executionId}/execution-steps`, + { + params: { + page: pageParam, + }, + signal, + }, + ); + + return data; + }, + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage?.meta?.currentPage < lastPage?.meta?.totalPages + ? lastPage?.meta?.currentPage + 1 + : null, + }); + + return query; +} diff --git a/packages/web/src/hooks/useExecutions.js b/packages/web/src/hooks/useExecutions.js new file mode 100644 index 0000000000000000000000000000000000000000..c0f1fc2859cba444f360b657dded2c1637755f63 --- /dev/null +++ b/packages/web/src/hooks/useExecutions.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useExecutions({ page }, { refetchInterval } = {}) { + const query = useQuery({ + queryKey: ['executions', { page }], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/executions`, { + params: { + page, + }, + signal, + }); + + return data; + }, + refetchInterval, + }); + + return query; +} diff --git a/packages/web/src/hooks/useFlow.js b/packages/web/src/hooks/useFlow.js new file mode 100644 index 0000000000000000000000000000000000000000..8fb7ee05b4284e0c779cf6558b79fb7627e3d0a6 --- /dev/null +++ b/packages/web/src/hooks/useFlow.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useFlow(flowId) { + const query = useQuery({ + queryKey: ['flows', flowId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/flows/${flowId}`, { + signal, + }); + + return data; + }, + enabled: !!flowId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useFormatMessage.js b/packages/web/src/hooks/useFormatMessage.js new file mode 100644 index 0000000000000000000000000000000000000000..ef76fe0c3206df2a87031b4701902457c335f051 --- /dev/null +++ b/packages/web/src/hooks/useFormatMessage.js @@ -0,0 +1,5 @@ +import { useIntl } from 'react-intl'; +export default function useFormatMessage() { + const { formatMessage } = useIntl(); + return (id, values = {}) => formatMessage({ id }, values); +} diff --git a/packages/web/src/hooks/useInvoices.ee.js b/packages/web/src/hooks/useInvoices.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..424686485778905e6891bf333420259aaa9ffcce --- /dev/null +++ b/packages/web/src/hooks/useInvoices.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useInvoices() { + const query = useQuery({ + queryKey: ['users', 'invoices'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/users/invoices', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useLazyApps.js b/packages/web/src/hooks/useLazyApps.js new file mode 100644 index 0000000000000000000000000000000000000000..0f72050d75ca4c885d826f4239f734705538ac89 --- /dev/null +++ b/packages/web/src/hooks/useLazyApps.js @@ -0,0 +1,30 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; +import React from 'react'; + +export default function useLazyApps({ appName } = {}, { onSuccess } = {}) { + const abortControllerRef = React.useRef(new AbortController()); + + React.useEffect(() => { + abortControllerRef.current = new AbortController(); + + return () => { + abortControllerRef.current?.abort(); + }; + }, [appName]); + + const query = useMutation({ + mutationFn: async () => { + const { data } = await api.get('/v1/apps', { + params: { name: appName }, + signal: abortControllerRef.current.signal, + }); + + return data; + }, + onSuccess, + }); + + return query; +} diff --git a/packages/web/src/hooks/useLazyFlows.js b/packages/web/src/hooks/useLazyFlows.js new file mode 100644 index 0000000000000000000000000000000000000000..66ca03e9a5616dc184f1a1deb5402fdd7866324a --- /dev/null +++ b/packages/web/src/hooks/useLazyFlows.js @@ -0,0 +1,30 @@ +import * as React from 'react'; + +import api from 'helpers/api'; +import { useMutation } from '@tanstack/react-query'; + +export default function useLazyFlows({ flowName, page }, { onSettled }) { + const abortControllerRef = React.useRef(new AbortController()); + + React.useEffect(() => { + abortControllerRef.current = new AbortController(); + + return () => { + abortControllerRef.current?.abort(); + }; + }, [flowName]); + + const query = useMutation({ + mutationFn: async () => { + const { data } = await api.get('/v1/flows', { + params: { name: flowName, page }, + signal: abortControllerRef.current.signal, + }); + + return data; + }, + onSettled, + }); + + return query; +} diff --git a/packages/web/src/hooks/usePaddle.ee.js b/packages/web/src/hooks/usePaddle.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7a85cc1bed6956e9b74195d05413a6633ed27544 --- /dev/null +++ b/packages/web/src/hooks/usePaddle.ee.js @@ -0,0 +1,8 @@ +import * as React from 'react'; +import { PaddleContext } from 'contexts/Paddle.ee'; +export default function usePaddle() { + const paddleContext = React.useContext(PaddleContext); + return { + loaded: paddleContext.loaded, + }; +} diff --git a/packages/web/src/hooks/usePaddleInfo.ee.js b/packages/web/src/hooks/usePaddleInfo.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..bbfe933bc8752aff563e82348fd5aeebf247ecad --- /dev/null +++ b/packages/web/src/hooks/usePaddleInfo.ee.js @@ -0,0 +1,17 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function usePaddleInfo() { + const query = useQuery({ + queryKey: ['payment', 'paddleInfo'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/payment/paddle-info', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/usePaymentPlans.ee.js b/packages/web/src/hooks/usePaymentPlans.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..d887516a1a7e762d9bd4d47d071b90094522efc4 --- /dev/null +++ b/packages/web/src/hooks/usePaymentPlans.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function usePaymentPlans() { + const query = useQuery({ + queryKey: ['payment', 'plans'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/payment/plans', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/usePermissionCatalog.ee.js b/packages/web/src/hooks/usePermissionCatalog.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..489a398252d77f1ab49f48f8aa5088d5947998da --- /dev/null +++ b/packages/web/src/hooks/usePermissionCatalog.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function usePermissionCatalog() { + const query = useQuery({ + queryKey: ['admin', 'permissions', 'catalog'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/admin/permissions/catalog', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/usePlanAndUsage.js b/packages/web/src/hooks/usePlanAndUsage.js new file mode 100644 index 0000000000000000000000000000000000000000..861fa64bfef6663915292c228c4d837e634426cc --- /dev/null +++ b/packages/web/src/hooks/usePlanAndUsage.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function usePlanAndUsage(userId) { + const query = useQuery({ + queryKey: ['users', userId, 'planAndUsage'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/users/${userId}/plan-and-usage`, { + signal, + }); + + return data; + }, + enabled: !!userId, + }); + + return query; +} diff --git a/packages/web/src/hooks/usePrevious.js b/packages/web/src/hooks/usePrevious.js new file mode 100644 index 0000000000000000000000000000000000000000..e97d6f9e6be9fff928cd64f24f949413a783b694 --- /dev/null +++ b/packages/web/src/hooks/usePrevious.js @@ -0,0 +1,9 @@ +import { useEffect, useRef } from "react"; + +export const usePrevious = (value) => { + const ref = useRef(); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; diff --git a/packages/web/src/hooks/useRevokeAccessToken.js b/packages/web/src/hooks/useRevokeAccessToken.js new file mode 100644 index 0000000000000000000000000000000000000000..87cb5f6936360f775e88ecd92e78158f58145eac --- /dev/null +++ b/packages/web/src/hooks/useRevokeAccessToken.js @@ -0,0 +1,15 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useRevokeAccessToken(token) { + const query = useMutation({ + mutationFn: async () => { + const { data } = await api.delete(`/v1/access-tokens/${token}`); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useRole.ee.js b/packages/web/src/hooks/useRole.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..dcd589105844b53af442ef80705da0fa7a1895d8 --- /dev/null +++ b/packages/web/src/hooks/useRole.ee.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useRole({ roleId }) { + const query = useQuery({ + queryKey: ['admin', 'roles', roleId], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/admin/roles/${roleId}`, { + signal, + }); + + return data; + }, + enabled: !!roleId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useRoles.ee.js b/packages/web/src/hooks/useRoles.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..bbfe1d4a9ac5579110991cbbb0b32578d4eb38a8 --- /dev/null +++ b/packages/web/src/hooks/useRoles.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; +import api from 'helpers/api'; + +export default function useRoles() { + const query = useQuery({ + staleTime: 0, + queryKey: ['admin', 'roles'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/admin/roles', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useSamlAuthProvider.js b/packages/web/src/hooks/useSamlAuthProvider.js new file mode 100644 index 0000000000000000000000000000000000000000..c293eafd94f4ee441055bd296e8afaa9340fb76a --- /dev/null +++ b/packages/web/src/hooks/useSamlAuthProvider.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useSamlAuthProvider({ samlAuthProviderId } = {}) { + const query = useQuery({ + queryKey: ['samlAuthProviders', samlAuthProviderId], + queryFn: async ({ signal }) => { + const { data } = await api.get( + `/v1/admin/saml-auth-providers/${samlAuthProviderId}`, + { + signal, + }, + ); + + return data; + }, + enabled: !!samlAuthProviderId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useSamlAuthProviders.ee.js b/packages/web/src/hooks/useSamlAuthProviders.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..2aab241af142fa966552f2e48f0ef2aba148664d --- /dev/null +++ b/packages/web/src/hooks/useSamlAuthProviders.ee.js @@ -0,0 +1,18 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useSamlAuthProviders() { + const query = useQuery({ + queryKey: ['samlAuthProviders'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/saml-auth-providers', { + signal, + }); + + return data; + }, + }); + + return query; +} diff --git a/packages/web/src/hooks/useStepConnection.js b/packages/web/src/hooks/useStepConnection.js new file mode 100644 index 0000000000000000000000000000000000000000..8ed66f71b906f8321f01f0517ecf26d35a0e629b --- /dev/null +++ b/packages/web/src/hooks/useStepConnection.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useStepConnection(stepId) { + const query = useQuery({ + queryKey: ['steps', stepId, 'connection'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/steps/${stepId}/connection`, { + signal, + }); + + return data; + }, + enabled: !!stepId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useStepWithTestExecutions.js b/packages/web/src/hooks/useStepWithTestExecutions.js new file mode 100644 index 0000000000000000000000000000000000000000..48966c981b63c3571ddd87c8f9daf1ae1e1ac5e8 --- /dev/null +++ b/packages/web/src/hooks/useStepWithTestExecutions.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useStepWithTestExecutions(stepId) { + const query = useQuery({ + queryKey: ['steps', stepId, 'previousSteps'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/steps/${stepId}/previous-steps`, { + signal, + }); + + return data; + }, + enabled: false, + }); + + return query; +} diff --git a/packages/web/src/hooks/useSubscription.ee.js b/packages/web/src/hooks/useSubscription.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..7a3a711c2941643337b39ac1603c3edc5ee76d6c --- /dev/null +++ b/packages/web/src/hooks/useSubscription.ee.js @@ -0,0 +1,55 @@ +import { useQuery } from '@tanstack/react-query'; +import { useLocation } from 'react-router-dom'; +import { DateTime } from 'luxon'; +import * as React from 'react'; + +import api from 'helpers/api'; + +function transform(subscription) { + const nextBillDate = subscription?.nextBillDate; + const nextBillDateTitleDateObject = DateTime.fromISO(nextBillDate); + const formattedNextBillDateTitle = nextBillDateTitleDateObject.isValid + ? nextBillDateTitleDateObject.toFormat('LLL dd, yyyy') + : nextBillDate; + + return { + ...subscription, + nextBillDate: formattedNextBillDateTitle, + }; +} + +export default function useSubscription() { + const location = useLocation(); + const state = location.state; + const checkoutCompleted = state?.checkoutCompleted; + const [isPolling, setIsPolling] = React.useState(false); + + const { data } = useQuery({ + queryKey: ['users', 'me', 'subscription'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/users/me/subscription`, { + signal, + }); + + return data; + }, + refetchInterval: isPolling ? 1000 : false, + }); + + const subscription = data?.data; + + const hasSubscription = subscription?.status === 'active'; + + React.useEffect( + function pollDataUntilSubscriptionIsCreated() { + if (checkoutCompleted) { + setIsPolling(!hasSubscription); + } + }, + [checkoutCompleted, hasSubscription], + ); + + return { + data: transform(subscription), + }; +} diff --git a/packages/web/src/hooks/useTestConnection.js b/packages/web/src/hooks/useTestConnection.js new file mode 100644 index 0000000000000000000000000000000000000000..7e5aa0b3086ccbeec712f3334f92759cff416f7b --- /dev/null +++ b/packages/web/src/hooks/useTestConnection.js @@ -0,0 +1,19 @@ +import { useMutation } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useTestConnection( + { connectionId }, + { onSettled } = {}, +) { + const query = useMutation({ + mutationFn: async () => { + const { data } = await api.post(`/v1/connections/${connectionId}/test`); + + return data; + }, + onSettled, + }); + + return query; +} diff --git a/packages/web/src/hooks/useTriggerSubsteps.js b/packages/web/src/hooks/useTriggerSubsteps.js new file mode 100644 index 0000000000000000000000000000000000000000..b77f631a6f403961ff8da640f8afdfd25e59224c --- /dev/null +++ b/packages/web/src/hooks/useTriggerSubsteps.js @@ -0,0 +1,22 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useTriggerSubsteps({ appKey, triggerKey }) { + const query = useQuery({ + queryKey: ['apps', appKey, 'triggers', triggerKey, 'substeps'], + queryFn: async ({ signal }) => { + const { data } = await api.get( + `/v1/apps/${appKey}/triggers/${triggerKey}/substeps`, + { + signal, + }, + ); + + return data; + }, + enabled: !!appKey && !!triggerKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useTriggers.js b/packages/web/src/hooks/useTriggers.js new file mode 100644 index 0000000000000000000000000000000000000000..e2641f4e14f45b56bce8504cccf67dc1a6547a33 --- /dev/null +++ b/packages/web/src/hooks/useTriggers.js @@ -0,0 +1,19 @@ +import { useQuery } from '@tanstack/react-query'; + +import api from 'helpers/api'; + +export default function useTriggers(appKey) { + const query = useQuery({ + queryKey: ['apps', appKey, 'triggers'], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/apps/${appKey}/triggers`, { + signal, + }); + + return data; + }, + enabled: !!appKey, + }); + + return query; +} diff --git a/packages/web/src/hooks/useUserApps.js b/packages/web/src/hooks/useUserApps.js new file mode 100644 index 0000000000000000000000000000000000000000..3c0824457c7a071ffa1edc498827032c5a6d30f7 --- /dev/null +++ b/packages/web/src/hooks/useUserApps.js @@ -0,0 +1,25 @@ +import { useQuery } from '@tanstack/react-query'; +import useCurrentUser from 'hooks/useCurrentUser'; +import api from 'helpers/api'; + +export default function useUserApps(appName) { + const { data } = useCurrentUser(); + const userId = data?.data.id; + + const query = useQuery({ + queryKey: ['users', userId, 'apps', appName], + queryFn: async ({ signal }) => { + const { data } = await api.get(`/v1/users/${userId}/apps`, { + signal, + params: { + ...(appName && { name: appName }), + }, + }); + + return data; + }, + enabled: !!userId, + }); + + return query; +} diff --git a/packages/web/src/hooks/useUserTrial.ee.js b/packages/web/src/hooks/useUserTrial.ee.js new file mode 100644 index 0000000000000000000000000000000000000000..fdd0da1fdf3aea9033a5068f3063ef6fa9d78a89 --- /dev/null +++ b/packages/web/src/hooks/useUserTrial.ee.js @@ -0,0 +1,71 @@ +import { DateTime } from 'luxon'; +import { useQuery } from '@tanstack/react-query'; + +import useFormatMessage from './useFormatMessage'; +import api from 'helpers/api'; + +function getDiffInDays(date) { + const today = DateTime.now().startOf('day'); + const diffInDays = date.diff(today, 'days').days; + const roundedDiffInDays = Math.round(diffInDays); + + return roundedDiffInDays; +} + +function getFeedbackPayload(date) { + const diffInDays = getDiffInDays(date); + + if (diffInDays <= -1) { + return { + translationEntryId: 'trialBadge.over', + status: 'error', + over: true, + }; + } else if (diffInDays <= 0) { + return { + translationEntryId: 'trialBadge.endsToday', + status: 'warning', + over: false, + }; + } else { + return { + translationEntryId: 'trialBadge.xDaysLeft', + translationEntryValues: { + remainingDays: diffInDays, + }, + status: 'warning', + over: false, + }; + } +} +export default function useUserTrial() { + const formatMessage = useFormatMessage(); + + const { data } = useQuery({ + queryKey: ['users', 'me', 'trial'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/users/me/trial', { + signal, + }); + + return data; + }, + }); + + const userTrial = data?.data; + + const hasTrial = userTrial?.inTrial; + + const expireAt = DateTime.fromISO(userTrial?.expireAt).startOf('day'); + + const { translationEntryId, translationEntryValues, status, over } = + getFeedbackPayload(expireAt); + + return { + message: formatMessage(translationEntryId, translationEntryValues), + expireAt, + over, + status, + hasTrial, + }; +} diff --git a/packages/web/src/hooks/useVersion.js b/packages/web/src/hooks/useVersion.js new file mode 100644 index 0000000000000000000000000000000000000000..51f955db627404ab888482d0fd876d033554ff32 --- /dev/null +++ b/packages/web/src/hooks/useVersion.js @@ -0,0 +1,37 @@ +import { compare } from 'compare-versions'; +import { useQuery } from '@tanstack/react-query'; + +import useAutomatischNotifications from 'hooks/useAutomatischNotifications'; +import api from 'helpers/api'; + +export default function useVersion() { + const { data: notificationsData } = useAutomatischNotifications(); + const { data } = useQuery({ + queryKey: ['automatisch', 'version'], + queryFn: async ({ signal }) => { + const { data } = await api.get('/v1/automatisch/version', { + signal, + }); + + return data; + }, + }); + const version = data?.data?.version; + const notifications = notificationsData?.data || []; + + const newVersionCount = notifications.reduce((count, notification) => { + if (!version) return 0; + // an unexpectedly invalid version would throw and thus, try-catch. + try { + const isNewer = compare(version, notification.name, '<'); + return isNewer ? count + 1 : count; + } catch { + return count; + } + }, 0); + + return { + version, + newVersionCount, + }; +} diff --git a/packages/web/src/index.jsx b/packages/web/src/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..17ad9ef8bcea6206c85f713f1cd7d9e502633f96 --- /dev/null +++ b/packages/web/src/index.jsx @@ -0,0 +1,36 @@ +import { createRoot } from 'react-dom/client'; +import ThemeProvider from 'components/ThemeProvider'; +import IntlProvider from 'components/IntlProvider'; +import ApolloProvider from 'components/ApolloProvider'; +import SnackbarProvider from 'components/SnackbarProvider'; +import MetadataProvider from 'components/MetadataProvider'; +import { AuthenticationProvider } from 'contexts/Authentication'; +import QueryClientProvider from 'components/QueryClientProvider'; +import Router from 'components/Router'; +import routes from 'routes'; +import reportWebVitals from './reportWebVitals'; + +const container = document.getElementById('root'); +const root = createRoot(container); + +root.render( + + + + + + + + {routes} + + + + + + + , +); +// If you want to start measuring performance in your app, pass a function +// to log results (for example: reportWebVitals(console.log)) +// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals +reportWebVitals(); diff --git a/packages/web/src/locales/en.json b/packages/web/src/locales/en.json new file mode 100644 index 0000000000000000000000000000000000000000..20b423b0b44b9b630e55013a2e5306e3f03761e3 --- /dev/null +++ b/packages/web/src/locales/en.json @@ -0,0 +1,276 @@ +{ + "brandText": "Automatisch", + "searchPlaceholder": "Search", + "accountDropdownMenu.settings": "Settings", + "accountDropdownMenu.adminSettings": "Admin", + "accountDropdownMenu.logout": "Logout", + "drawer.dashboard": "Dashboard", + "drawer.flows": "Flows", + "drawer.apps": "My Apps", + "drawer.executions": "Executions", + "drawer.explore": "Explore", + "settingsDrawer.myProfile": "My Profile", + "settingsDrawer.goBack": "Go to the dashboard", + "settingsDrawer.notifications": "Notifications", + "settingsDrawer.billingAndUsage": "Billing and usage", + "adminSettingsDrawer.users": "Users", + "adminSettingsDrawer.roles": "Roles", + "adminSettingsDrawer.authentication": "Authentication", + "adminSettingsDrawer.userInterface": "User Interface", + "adminSettingsDrawer.goBack": "Go to the dashboard", + "adminSettingsDrawer.apps": "Applications", + "app.connectionCount": "{count} connections", + "app.flowCount": "{count} flows", + "app.addConnection": "Add connection", + "app.addCustomConnection": "Add custom connection", + "app.reconnectConnection": "Reconnect connection", + "app.createFlow": "Create flow", + "app.settings": "Settings", + "app.connections": "Connections", + "app.noConnections": "You don't have any connections yet.", + "app.flows": "Flows", + "app.noFlows": "You don't have any flows yet.", + "apps.title": "Apps", + "apps.addConnection": "Add connection", + "apps.addNewAppConnection": "Add a new app connection", + "apps.searchApp": "Search for app", + "apps.noConnections": "You don't have any connections yet.", + "addAppConnection.submit": "Submit", + "addAppConnection.callToDocs": "Visit our documentation to see how to add connection for {appName}.", + "connection.flowCount": "{count} flows", + "connection.viewFlows": "View flows", + "connection.testConnection": "Test connection", + "connection.testSuccessful": "Test successful", + "connection.testFailed": "Test failed", + "connection.testing": "Testing...", + "connection.reconnect": "Reconnect", + "connection.delete": "Delete", + "connection.deletedMessage": "The connection has been deleted.", + "connection.addedAt": "added {datetime}", + "createFlow.creating": "Creating a flow...", + "flow.active": "ON", + "flow.inactive": "OFF", + "flow.published": "Published", + "flow.paused": "Paused", + "flow.draft": "Draft", + "flow.successfullyDeleted": "The flow and associated executions have been deleted.", + "flow.successfullyDuplicated": "The flow has been successfully duplicated.", + "flowEditor.publish": "PUBLISH", + "flowEditor.unpublish": "UNPUBLISH", + "flowEditor.publishedFlowCannotBeUpdated": "To edit this flow, you must first unpublish it.", + "flowEditor.noTestDataTitle": "We couldn't find matching data", + "flowEditor.noTestDataMessage": "Create a sample in the associated service and test the step again.", + "flowEditor.testAndContinue": "Test & Continue", + "flowEditor.continue": "Continue", + "flowEditor.chooseApp": "Choose an app", + "flowEditor.chooseEvent": "Choose an event", + "flowEditor.pollIntervalLabel": "Poll interval", + "flowEditor.pollIntervalValue": "Every {minutes} minutes", + "flowEditor.triggerEvent": "Trigger event", + "flowEditor.actionEvent": "Action event", + "flowEditor.instantTriggerType": "Instant", + "filterConditions.onlyContinueIf": "Only continue if…", + "filterConditions.orContinueIf": "OR continue if…", + "chooseConnectionSubstep.continue": "Continue", + "chooseConnectionSubstep.addNewConnection": "Add new connection", + "chooseConnectionSubstep.addNewSharedConnection": "Add new shared connection", + "chooseConnectionSubstep.chooseConnection": "Choose connection", + "flow.createdAt": "created {datetime}", + "flow.updatedAt": "updated {datetime}", + "flow.view": "View", + "flow.duplicate": "Duplicate", + "flow.delete": "Delete", + "flowStep.triggerType": "Trigger", + "flowStep.actionType": "Action", + "flows.create": "Create flow", + "flows.title": "Flows", + "flows.noFlows": "You don't have any flows yet.", + "flowEditor.goBack": "Go back to flows", + "executions.title": "Executions", + "executions.noExecutions": "There is no execution data point to show.", + "execution.id": "Execution ID: {id}", + "execution.createdAt": "created {datetime}", + "execution.test": "Test run", + "execution.statusSuccess": "Success", + "execution.statusFailure": "Failure", + "execution.noDataTitle": "No data", + "execution.noDataMessage": "We successfully ran the execution, but there was no new data to process.", + "executionStep.id": "ID: {id}", + "executionStep.executedAt": "executed {datetime}", + "profileSettings.title": "My Profile", + "profileSettings.fullName": "Full name", + "profileSettings.email": "Email", + "profileSettings.updateProfile": "Update your profile", + "profileSettings.updatedProfile": "Your profile has been updated.", + "profileSettings.newPassword": "New password", + "profileSettings.confirmNewPassword": "Confirm new password", + "profileSettings.deleteMyAccount": "Delete my account", + "profileSettings.deleteAccount": "Delete account", + "profileSettings.deleteAccountSubtitle": "This will permanently delete...", + "profileSettings.deleteAccountResult1": "Your account", + "profileSettings.deleteAccountResult2": "All your flows", + "profileSettings.deleteAccountResult3": "All your connections", + "profileSettings.deleteAccountResult4": "All execution history", + "billingAndUsageSettings.title": "Billing and usage", + "billingAndUsageSettings.paymentInformation": "Payment information", + "billingAndUsageSettings.paymentPortalInformation": "To manage your subscription, click here to go to the payment portal.", + "deleteAccountDialog.title": "Delete account?", + "deleteAccountDialog.description": "This will permanently delete your account and all the associated data with it.", + "deleteAccountDialog.cancel": "Cancel?", + "deleteAccountDialog.confirm": "Yes, delete it", + "notifications.title": "Notifications", + "notification.releasedAt": "Released {relativeDate}", + "webhookUrlInfo.title": "Your webhook URL", + "webhookUrlInfo.description": "You'll need to configure your application with this webhook URL.", + "webhookUrlInfo.helperText": "We've generated a custom webhook URL for you to send requests to. Learn more about webhooks.", + "webhookUrlInfo.copy": "Copy", + "signupForm.title": "Sign up", + "signupForm.fullNameFieldLabel": "Full name", + "signupForm.emailFieldLabel": "Email", + "signupForm.passwordFieldLabel": "Password", + "signupForm.confirmPasswordFieldLabel": "Confirm password", + "signupForm.submit": "Sign up", + "signupForm.validateEmail": "Email must be valid.", + "signupForm.passwordsMustMatch": "Passwords must match.", + "signupForm.mandatoryInput": "{inputName} is required.", + "loginForm.title": "Login", + "loginForm.emailFieldLabel": "Email", + "loginForm.passwordFieldLabel": "Password", + "loginForm.forgotPasswordText": "Forgot password?", + "loginForm.submit": "Login", + "loginForm.noAccount": "Don't have an Automatisch account yet?", + "loginForm.signUp": "Sign up", + "loginPage.divider": "OR", + "ssoProviders.loginWithProvider": "Login with {providerName}", + "forgotPasswordForm.title": "Forgot password", + "forgotPasswordForm.submit": "Send reset instructions", + "forgotPasswordForm.instructionsSent": "The instructions have been sent!", + "forgotPasswordForm.emailFieldLabel": "Email", + "resetPasswordForm.passwordsMustMatch": "Passwords must match.", + "resetPasswordForm.mandatoryInput": "{inputName} is required.", + "resetPasswordForm.title": "Reset password", + "resetPasswordForm.submit": "Reset password", + "resetPasswordForm.passwordFieldLabel": "Password", + "resetPasswordForm.confirmPasswordFieldLabel": "Confirm password", + "resetPasswordForm.passwordUpdated": "The password has been updated. Now, you can login.", + "usageAlert.informationText": "Tasks: {consumedTaskCount}/{allowedTaskCount} (Resets {relativeResetDate})", + "usageAlert.viewPlans": "View plans", + "jsonViewer.noDataFound": "We couldn't find anything matching your search", + "planUpgrade.title": "Upgrade your plan", + "usageDataInformation.subscriptionPlan": "Subscription plan", + "usageDataInformation.monthlyQuota": "Monthly quota", + "usageDataInformation.nextBillAmount": "Next bill amount", + "usageDataInformation.nextBillDate": "Next bill date", + "usageDataInformation.yourUsage": "Your usage", + "usageDataInformation.yourUsageDescription": "Last 30 days total usage", + "usageDataInformation.yourUsageTasks": "Tasks", + "usageDataInformation.upgrade": "Upgrade", + "usageDataInformation.freeTrial": "Free trial", + "usageDataInformation.cancelPlan": "Cancel plan", + "usageDataInformation.updatePaymentMethod": "Update payment method", + "usageDataInformation.monthlyPayment": "(monthly payment)", + "usageDataInformation.upgradePlan": "Upgrade plan", + "invoices.invoices": "Invoices", + "invoices.date": "Date", + "invoices.amount": "Amount", + "invoices.invoice": "Invoice", + "invoices.link": "Link", + "trialBadge.xDaysLeft": "{remainingDays} trial {remainingDays, plural, one {day} other {days}} left", + "trialBadge.endsToday": "Trial ends today", + "trialBadge.over": "Trial is over", + "trialOverAlert.text": "Your free trial is over. Please upgrade your plan to continue using Automatisch.", + "checkoutCompletedAlert.text": "Thank you for upgrading your subscription and supporting our self-funded business!", + "subscriptionCancelledAlert.text": "Your subscription is cancelled, but you can continue using Automatisch until {date}.", + "customAutocomplete.noOptions": "No options available.", + "powerInputSuggestions.noOptions": "No options available.", + "usersPage.title": "User management", + "usersPage.createUser": "Create user", + "deleteUserButton.title": "Delete user", + "deleteUserButton.description": "This will permanently delete the user and all the associated data with it.", + "deleteUserButton.cancel": "Cancel", + "deleteUserButton.confirm": "Delete", + "deleteUserButton.successfullyDeleted": "The user has been deleted.", + "editUserPage.title": "Edit user", + "createUserPage.title": "Create user", + "userForm.fullName": "Full name", + "userForm.email": "Email", + "userForm.role": "Role", + "userForm.password": "Password", + "createUser.submit": "Create", + "createUser.successfullyCreated": "The user has been created.", + "editUser.submit": "Update", + "editUser.successfullyUpdated": "The user has been updated.", + "userList.fullName": "Full name", + "userList.email": "Email", + "userList.role": "Role", + "rolesPage.title": "Role management", + "rolesPage.createRole": "Create role", + "deleteRoleButton.title": "Delete role", + "deleteRoleButton.description": "This will permanently delete the role.", + "deleteRoleButton.cancel": "Cancel", + "deleteRoleButton.confirm": "Delete", + "deleteRoleButton.successfullyDeleted": "The role has been deleted.", + "editRolePage.title": "Edit role", + "createRolePage.title": "Create role", + "roleForm.name": "Name", + "roleForm.description": "Description", + "createRole.submit": "Create", + "createRole.successfullyCreated": "The role has been created.", + "editRole.submit": "Update", + "editRole.successfullyUpdated": "The role has been updated.", + "roleList.name": "Name", + "roleList.description": "Description", + "permissionSettings.cancel": "Cancel", + "permissionSettings.apply": "Apply", + "permissionSettings.title": "Conditions", + "appAuthClientsDialog.title": "Choose your authentication client", + "userInterfacePage.title": "User Interface", + "userInterfacePage.successfullyUpdated": "User interface has been updated.", + "userInterfacePage.titleFieldLabel": "Title", + "userInterfacePage.primaryMainColorFieldLabel": "Primary main color", + "userInterfacePage.primaryDarkColorFieldLabel": "Primary dark color", + "userInterfacePage.primaryLightColorFieldLabel": "Primary light color", + "userInterfacePage.svgDataFieldLabel": "Logo SVG code", + "userInterfacePage.submit": "Update", + "authenticationPage.title": "Single Sign-On with SAML", + "authenticationForm.active": "Active", + "authenticationForm.name": "Name", + "authenticationForm.certificate": "Certificate", + "authenticationForm.signatureAlgorithm": "Signature algorithm", + "authenticationForm.issuer": "Issuer", + "authenticationForm.entryPoint": "Entry point", + "authenticationForm.firstnameAttributeName": "Firstname attribute name", + "authenticationForm.surnameAttributeName": "Surname attribute name", + "authenticationForm.emailAttributeName": "Email attribute name", + "authenticationForm.roleAttributeName": "Role attribute name", + "authenticationForm.defaultRole": "Default role", + "authenticationForm.successfullySaved": "The provider has been saved.", + "authenticationForm.save": "Save", + "roleMappingsForm.title": "Role mappings", + "roleMappingsForm.remoteRoleName": "Remote role name", + "roleMappingsForm.role": "Role", + "roleMappingsForm.appendRoleMapping": "Append", + "roleMappingsForm.save": "Save", + "roleMappingsForm.notFound": "No role mappings have found.", + "roleMappingsForm.successfullySaved": "Role mappings have been saved.", + "adminApps.title": "Apps", + "adminApps.connections": "Connections", + "adminApps.authClients": "Auth clients", + "adminApps.settings": "Settings", + "adminAppsSettings.allowCustomConnection": "Allow custom connection", + "adminAppsSettings.shared": "Shared", + "adminAppsSettings.disabled": "Disabled", + "adminAppsSettings.save": "Save", + "adminAppsSettings.successfullySaved": "Settings have been saved.", + "adminAppsAuthClients.noAuthClients": "You don't have any auth clients yet.", + "adminAppsAuthClients.statusActive": "Active", + "adminAppsAuthClients.statusInactive": "Inactive", + "createAuthClient.button": "Create auth client", + "createAuthClient.title": "Create auth client", + "authClient.buttonSubmit": "Submit", + "authClient.inputName": "Name", + "authClient.inputActive": "Active", + "updateAuthClient.title": "Update auth client", + "notFoundPage.title": "We can't seem to find a page you're looking for.", + "notFoundPage.button": "Back to home page" +} diff --git a/packages/web/src/pages/AdminApplication/index.jsx b/packages/web/src/pages/AdminApplication/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..44e7308571ebbccce49896b9467a1f8cb3934e72 --- /dev/null +++ b/packages/web/src/pages/AdminApplication/index.jsx @@ -0,0 +1,152 @@ +import * as React from 'react'; +import { + Link, + Route, + Navigate, + Routes, + useParams, + useMatch, + useNavigate, +} from 'react-router-dom'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import AppIcon from 'components/AppIcon'; +import Container from 'components/Container'; +import PageTitle from 'components/PageTitle'; +import AdminApplicationSettings from 'components/AdminApplicationSettings'; +import AdminApplicationAuthClients from 'components/AdminApplicationAuthClients'; +import AdminApplicationCreateAuthClient from 'components/AdminApplicationCreateAuthClient'; +import AdminApplicationUpdateAuthClient from 'components/AdminApplicationUpdateAuthClient'; +import useApp from 'hooks/useApp'; + +export default function AdminApplication() { + const theme = useTheme(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md')); + const formatMessage = useFormatMessage(); + const navigate = useNavigate(); + const connectionsPathMatch = useMatch({ + path: URLS.ADMIN_APP_CONNECTIONS_PATTERN, + end: false, + }); + const settingsPathMatch = useMatch({ + path: URLS.ADMIN_APP_SETTINGS_PATTERN, + end: false, + }); + const authClientsPathMatch = useMatch({ + path: URLS.ADMIN_APP_AUTH_CLIENTS_PATTERN, + end: false, + }); + const { appKey } = useParams(); + + const { data, loading } = useApp(appKey); + + const app = data?.data || {}; + + const goToAuthClientsPage = () => navigate('auth-clients'); + + if (loading) return null; + + return ( + <> + + + + + + + + {app.name} + + + + + + + + + + + + + + } + /> + } + /> + App connections
    } + /> + + } + /> + + + + + + + + } + /> + + } + /> + + + ); +} diff --git a/packages/web/src/pages/AdminApplications/index.jsx b/packages/web/src/pages/AdminApplications/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..22fb5f46160c32cdd7bc241376879f69c0dbdec7 --- /dev/null +++ b/packages/web/src/pages/AdminApplications/index.jsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import Grid from '@mui/material/Grid'; +import CircularProgress from '@mui/material/CircularProgress'; +import Divider from '@mui/material/Divider'; + +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import SearchInput from 'components/SearchInput'; +import AppRow from 'components/AppRow'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useApps from 'hooks/useApps'; + +function AdminApplications() { + const formatMessage = useFormatMessage(); + const [appName, setAppName] = React.useState(''); + + const { data: apps, isLoading: isAppsLoading } = useApps({ + name: appName, + }); + + const onSearchChange = React.useCallback((event) => { + setAppName(event.target.value); + }, []); + + return ( + + + + + {formatMessage('adminApps.title')} + + + + + + + + + + + {isAppsLoading && ( + + )} + + {!isAppsLoading && + apps?.data?.map((app) => ( + + + + ))} + + + ); +} +export default AdminApplications; diff --git a/packages/web/src/pages/Application/index.jsx b/packages/web/src/pages/Application/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e3084fabaaf190516ca2bfb55b95a24b62abdf71 --- /dev/null +++ b/packages/web/src/pages/Application/index.jsx @@ -0,0 +1,237 @@ +import * as React from 'react'; +import { + Link, + Route, + Navigate, + Routes, + useParams, + useSearchParams, + useMatch, + useNavigate, +} from 'react-router-dom'; +import { useTheme } from '@mui/material/styles'; +import useMediaQuery from '@mui/material/useMediaQuery'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Tabs from '@mui/material/Tabs'; +import Tab from '@mui/material/Tab'; +import AddIcon from '@mui/icons-material/Add'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import useAppConfig from 'hooks/useAppConfig.ee'; +import useCurrentUserAbility from 'hooks/useCurrentUserAbility'; +import * as URLS from 'config/urls'; +import SplitButton from 'components/SplitButton'; +import ConditionalIconButton from 'components/ConditionalIconButton'; +import AppConnections from 'components/AppConnections'; +import AppFlows from 'components/AppFlows'; +import AddAppConnection from 'components/AddAppConnection'; +import AppIcon from 'components/AppIcon'; +import Container from 'components/Container'; +import PageTitle from 'components/PageTitle'; +import useApp from 'hooks/useApp'; + +const ReconnectConnection = (props) => { + const { application, onClose } = props; + const { connectionId } = useParams(); + + return ( + + ); +}; + +export default function Application() { + const theme = useTheme(); + const matchSmallScreens = useMediaQuery(theme.breakpoints.down('md')); + const formatMessage = useFormatMessage(); + const connectionsPathMatch = useMatch({ + path: URLS.APP_CONNECTIONS_PATTERN, + end: false, + }); + const flowsPathMatch = useMatch({ path: URLS.APP_FLOWS_PATTERN, end: false }); + const [searchParams] = useSearchParams(); + const { appKey } = useParams(); + const navigate = useNavigate(); + + const { data, loading } = useApp(appKey); + const app = data?.data || {}; + + const { data: appConfig } = useAppConfig(appKey); + const connectionId = searchParams.get('connectionId') || undefined; + + const currentUserAbility = useCurrentUserAbility(); + + const goToApplicationPage = () => navigate('connections'); + + const connectionOptions = React.useMemo(() => { + const shouldHaveCustomConnection = + appConfig?.data?.canConnect && appConfig?.data?.canCustomConnect; + + const options = [ + { + label: formatMessage('app.addConnection'), + key: 'addConnection', + 'data-test': 'add-connection-button', + to: URLS.APP_ADD_CONNECTION(appKey, appConfig?.data?.canConnect), + disabled: !currentUserAbility.can('create', 'Connection'), + }, + ]; + + if (shouldHaveCustomConnection) { + options.push({ + label: formatMessage('app.addCustomConnection'), + key: 'addCustomConnection', + 'data-test': 'add-custom-connection-button', + to: URLS.APP_ADD_CONNECTION(appKey), + disabled: !currentUserAbility.can('create', 'Connection'), + }); + } + + return options; + }, [appKey, appConfig?.data, currentUserAbility]); + + if (loading) return null; + + return ( + <> + + + + + + + + + {app.name} + + + + + } + disabled={!currentUserAbility.can('create', 'Flow')} + > + {formatMessage('app.createFlow')} + + } + /> + + disabled) + } + options={connectionOptions} + /> + } + /> + + + + + + + + + + + + + + + + } + /> + + } + /> + + + } + /> + + + + + + + + + } + /> + + + } + /> + + + ); +} diff --git a/packages/web/src/pages/Applications/index.jsx b/packages/web/src/pages/Applications/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ed5940bb7e7e757ca196cc11ff8c68cde16940bb --- /dev/null +++ b/packages/web/src/pages/Applications/index.jsx @@ -0,0 +1,107 @@ +import * as React from 'react'; +import { Link, Routes, Route, useNavigate } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Divider from '@mui/material/Divider'; +import CircularProgress from '@mui/material/CircularProgress'; +import AddIcon from '@mui/icons-material/Add'; +import Can from 'components/Can'; +import NoResultFound from 'components/NoResultFound'; +import ConditionalIconButton from 'components/ConditionalIconButton'; +import Container from 'components/Container'; +import AddNewAppConnection from 'components/AddNewAppConnection'; +import PageTitle from 'components/PageTitle'; +import AppRow from 'components/AppRow'; +import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import useUserApps from 'hooks/useUserApps'; + +export default function Applications() { + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const [appName, setAppName] = React.useState(null); + const { data, isLoading } = useUserApps(appName); + const apps = data?.data; + const hasApps = apps?.length; + + const onSearchChange = React.useCallback((event) => { + setAppName(event.target.value); + }, []); + + const goToApps = React.useCallback(() => { + navigate(URLS.APPS); + }, [navigate]); + + return ( + + + + + {formatMessage('apps.title')} + + + + + + + + + {(allowed) => ( + } + data-test="add-connection-button" + > + {formatMessage('apps.addConnection')} + + )} + + + + + + + {isLoading && ( + + )} + + {!isLoading && !hasApps && ( + + )} + + {!isLoading && + apps?.map((app) => ( + + ))} + + + } + /> + + + + ); +} diff --git a/packages/web/src/pages/Authentication/RoleMappings.jsx b/packages/web/src/pages/Authentication/RoleMappings.jsx new file mode 100644 index 0000000000000000000000000000000000000000..78ee244ec3bc8e67120c4fe675d76d0be28e3728 --- /dev/null +++ b/packages/web/src/pages/Authentication/RoleMappings.jsx @@ -0,0 +1,108 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Divider from '@mui/material/Divider'; +import Stack from '@mui/material/Stack'; +import Typography from '@mui/material/Typography'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import { useMemo } from 'react'; + +import Form from 'components/Form'; +import { UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS } from 'graphql/mutations/upsert-saml-auth-providers-role-mappings'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useAdminSamlAuthProviderRoleMappings from 'hooks/useAdminSamlAuthProviderRoleMappings'; +import RoleMappingsFieldArray from './RoleMappingsFieldsArray'; + +function generateFormRoleMappings(roleMappings) { + if (roleMappings?.length === 0) { + return [{ roleId: '', remoteRoleName: '' }]; + } + + return roleMappings?.map(({ roleId, remoteRoleName }) => ({ + roleId, + remoteRoleName, + })); +} + +function RoleMappings({ provider, providerLoading }) { + const formatMessage = useFormatMessage(); + const enqueueSnackbar = useEnqueueSnackbar(); + + const { data, isLoading: isAdminSamlAuthProviderRoleMappingsLoading } = + useAdminSamlAuthProviderRoleMappings({ + adminSamlAuthProviderId: provider?.id, + }); + const roleMappings = data?.data; + + const [ + upsertSamlAuthProvidersRoleMappings, + { loading: upsertRoleMappingsLoading }, + ] = useMutation(UPSERT_SAML_AUTH_PROVIDERS_ROLE_MAPPINGS); + + const handleRoleMappingsUpdate = async (values) => { + try { + if (provider?.id) { + await upsertSamlAuthProvidersRoleMappings({ + variables: { + input: { + samlAuthProviderId: provider.id, + samlAuthProvidersRoleMappings: values.roleMappings.map( + ({ roleId, remoteRoleName }) => ({ + roleId, + remoteRoleName, + }), + ), + }, + }, + }); + + enqueueSnackbar(formatMessage('roleMappingsForm.successfullySaved'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-update-role-mappings-success', + }, + }); + } + } catch (error) { + throw new Error('Failed while saving!'); + } + }; + + const defaultValues = useMemo( + () => ({ + roleMappings: generateFormRoleMappings(roleMappings), + }), + [roleMappings], + ); + + if ( + providerLoading || + !provider?.id || + isAdminSamlAuthProviderRoleMappingsLoading + ) { + return null; + } + + return ( + <> + + + {formatMessage('roleMappingsForm.title')} + + + + + + {formatMessage('roleMappingsForm.save')} + + + + + ); +} +export default RoleMappings; diff --git a/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.jsx b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.jsx new file mode 100644 index 0000000000000000000000000000000000000000..be6c08fc816dc887f83eca82576d79d220b47171 --- /dev/null +++ b/packages/web/src/pages/Authentication/RoleMappingsFieldsArray.jsx @@ -0,0 +1,94 @@ +import { useFieldArray, useFormContext } from 'react-hook-form'; +import MuiTextField from '@mui/material/TextField'; +import Stack from '@mui/material/Stack'; +import DeleteIcon from '@mui/icons-material/Delete'; +import IconButton from '@mui/material/IconButton'; +import Button from '@mui/material/Button'; +import { Divider, Typography } from '@mui/material'; + +import useRoles from 'hooks/useRoles.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import TextField from 'components/TextField'; + +function generateRoleOptions(roles) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +function RoleMappingsFieldArray() { + const formatMessage = useFormatMessage(); + const { control } = useFormContext(); + const { data, isLoading: isRolesLoading } = useRoles(); + const roles = data?.data; + + const { fields, append, remove } = useFieldArray({ + control, + name: 'roleMappings', + }); + + const handleAppendMapping = () => append({ roleId: '', remoteRoleName: '' }); + + const handleRemoveMapping = (index) => () => remove(index); + + return ( + <> + {fields.length === 0 && ( + {formatMessage('roleMappingsForm.notFound')} + )} + {fields.map((field, index) => ( +
    + + + + ( + + )} + loading={isRolesLoading} + required + /> + + + + + + {index < fields.length - 1 && } +
    + ))} + + + ); +} +export default RoleMappingsFieldArray; diff --git a/packages/web/src/pages/Authentication/SamlConfiguration.jsx b/packages/web/src/pages/Authentication/SamlConfiguration.jsx new file mode 100644 index 0000000000000000000000000000000000000000..30bef0a07963c746062923be59234c5b7b504528 --- /dev/null +++ b/packages/web/src/pages/Authentication/SamlConfiguration.jsx @@ -0,0 +1,195 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Stack from '@mui/material/Stack'; +import MuiTextField from '@mui/material/TextField'; +import * as React from 'react'; + +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import Form from 'components/Form'; +import Switch from 'components/Switch'; +import TextField from 'components/TextField'; +import { UPSERT_SAML_AUTH_PROVIDER } from 'graphql/mutations/upsert-saml-auth-provider'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRoles from 'hooks/useRoles.ee'; + +const defaultValues = { + active: false, + name: '', + certificate: '', + signatureAlgorithm: 'sha1', + issuer: '', + entryPoint: '', + firstnameAttributeName: '', + surnameAttributeName: '', + emailAttributeName: '', + roleAttributeName: '', + defaultRoleId: '', +}; + +function generateRoleOptions(roles) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +function SamlConfiguration({ provider, providerLoading }) { + const formatMessage = useFormatMessage(); + const { data, loading: isRolesLoading } = useRoles(); + const roles = data?.data; + const enqueueSnackbar = useEnqueueSnackbar(); + + const [upsertSamlAuthProvider, { loading }] = useMutation( + UPSERT_SAML_AUTH_PROVIDER, + ); + + const handleProviderUpdate = async (providerDataToUpdate) => { + try { + const { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + active, + defaultRoleId, + } = providerDataToUpdate; + await upsertSamlAuthProvider({ + variables: { + input: { + name, + certificate, + signatureAlgorithm, + issuer, + entryPoint, + firstnameAttributeName, + surnameAttributeName, + emailAttributeName, + roleAttributeName, + active, + defaultRoleId, + }, + }, + }); + + enqueueSnackbar(formatMessage('authenticationForm.successfullySaved'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-save-saml-provider-success', + }, + }); + } catch (error) { + throw new Error('Failed while saving!'); + } + }; + + if (providerLoading) { + return null; + } + + return ( +
    + + + + + ( + + )} + /> + + + + + + + ( + + )} + loading={isRolesLoading} + /> + + {formatMessage('authenticationForm.save')} + + +
    + ); +} +export default SamlConfiguration; diff --git a/packages/web/src/pages/Authentication/index.jsx b/packages/web/src/pages/Authentication/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7b6e68262d6f7a9a9d73908370a00ea937a6ce28 --- /dev/null +++ b/packages/web/src/pages/Authentication/index.jsx @@ -0,0 +1,43 @@ +import Grid from '@mui/material/Grid'; +import Stack from '@mui/material/Stack'; +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useSamlAuthProvider from 'hooks/useSamlAuthProvider'; +import SamlConfiguration from './SamlConfiguration'; +import RoleMappings from './RoleMappings'; +import useAdminSamlAuthProviders from 'hooks/useAdminSamlAuthProviders.ee'; +function AuthenticationPage() { + const formatMessage = useFormatMessage(); + + const { data: adminSamlAuthProviders } = useAdminSamlAuthProviders(); + const samlAuthProviderId = adminSamlAuthProviders?.data?.[0]?.id; + + const { data, isLoading: isProviderLoading } = useSamlAuthProvider({ + samlAuthProviderId, + }); + const provider = data?.data; + + return ( + + + + {formatMessage('authenticationPage.title')} + + + + + + + + + + ); +} +export default AuthenticationPage; diff --git a/packages/web/src/pages/BillingAndUsageSettings/index.ee.jsx b/packages/web/src/pages/BillingAndUsageSettings/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..98c24e33b1d023624b12d3a904d8024e1d879508 --- /dev/null +++ b/packages/web/src/pages/BillingAndUsageSettings/index.ee.jsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import { Navigate } from 'react-router-dom'; +import Grid from '@mui/material/Grid'; +import * as URLS from 'config/urls'; +import UsageDataInformation from 'components/UsageDataInformation/index.ee'; +import Invoices from 'components/Invoices/index.ee'; +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useCloud from 'hooks/useCloud'; +function BillingAndUsageSettings() { + const isCloud = useCloud(); + const formatMessage = useFormatMessage(); + // redirect to the initial settings page + if (isCloud === false) { + return ; + } + // render nothing until we know if it's cloud or not + // here, `isCloud` is not `false`, but `undefined` + if (!isCloud) return ; + return ( + + + + + {formatMessage('billingAndUsageSettings.title')} + + + + + + + + + + + + + ); +} +export default BillingAndUsageSettings; diff --git a/packages/web/src/pages/CreateRole/index.ee.jsx b/packages/web/src/pages/CreateRole/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..62e54affd124cfcd48f587af5d93a53605eb0bba --- /dev/null +++ b/packages/web/src/pages/CreateRole/index.ee.jsx @@ -0,0 +1,93 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Grid from '@mui/material/Grid'; +import Stack from '@mui/material/Stack'; +import PermissionCatalogField from 'components/PermissionCatalogField/index.ee'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import Container from 'components/Container'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import TextField from 'components/TextField'; +import * as URLS from 'config/urls'; +import { CREATE_ROLE } from 'graphql/mutations/create-role.ee'; +import { getPermissions } from 'helpers/computePermissions.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +export default function CreateRole() { + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const [createRole, { loading }] = useMutation(CREATE_ROLE); + const enqueueSnackbar = useEnqueueSnackbar(); + const handleRoleCreation = async (roleData) => { + try { + const permissions = getPermissions(roleData.computedPermissions); + await createRole({ + variables: { + input: { + name: roleData.name, + description: roleData.description, + permissions, + }, + }, + }); + enqueueSnackbar(formatMessage('createRole.successfullyCreated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-create-role-success', + }, + }); + navigate(URLS.ROLES); + } catch (error) { + throw new Error('Failed while creating!'); + } + }; + return ( + + + + + {formatMessage('createRolePage.title')} + + + + +
    + + + + + + + + + {formatMessage('createRole.submit')} + + +
    +
    +
    +
    + ); +} diff --git a/packages/web/src/pages/CreateUser/index.jsx b/packages/web/src/pages/CreateUser/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8033d3ab6fc9f1bbf967958cf513eee5f0b46ef1 --- /dev/null +++ b/packages/web/src/pages/CreateUser/index.jsx @@ -0,0 +1,134 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Grid from '@mui/material/Grid'; +import Stack from '@mui/material/Stack'; +import MuiTextField from '@mui/material/TextField'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; + +import Can from 'components/Can'; +import Container from 'components/Container'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import TextField from 'components/TextField'; +import * as URLS from 'config/urls'; +import { CREATE_USER } from 'graphql/mutations/create-user.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRoles from 'hooks/useRoles.ee'; + +function generateRoleOptions(roles) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +export default function CreateUser() { + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const [createUser, { loading }] = useMutation(CREATE_USER); + const { data, loading: isRolesLoading } = useRoles(); + const roles = data?.data; + const enqueueSnackbar = useEnqueueSnackbar(); + const queryClient = useQueryClient(); + + const handleUserCreation = async (userData) => { + try { + await createUser({ + variables: { + input: { + fullName: userData.fullName, + password: userData.password, + email: userData.email, + role: { + id: userData.role?.id, + }, + }, + }, + }); + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + enqueueSnackbar(formatMessage('createUser.successfullyCreated'), { + variant: 'success', + persist: true, + SnackbarProps: { + 'data-test': 'snackbar-create-user-success', + }, + }); + + navigate(URLS.USERS); + } catch (error) { + throw new Error('Failed while creating!'); + } + }; + + return ( + + + + + {formatMessage('createUserPage.title')} + + + + +
    + + + + + + + + + ( + + )} + loading={isRolesLoading} + /> + + + + {formatMessage('createUser.submit')} + + +
    +
    +
    +
    + ); +} diff --git a/packages/web/src/pages/Dashboard/index.jsx b/packages/web/src/pages/Dashboard/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..66f42ce5d80775d346ac38a247d0850232ae560d --- /dev/null +++ b/packages/web/src/pages/Dashboard/index.jsx @@ -0,0 +1,3 @@ +export default function Dashboard() { + return <>Dashboard; +} diff --git a/packages/web/src/pages/EditRole/index.ee.jsx b/packages/web/src/pages/EditRole/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e52ced944be45e2d2019d85ee2f76edeb3c477fb --- /dev/null +++ b/packages/web/src/pages/EditRole/index.ee.jsx @@ -0,0 +1,126 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Grid from '@mui/material/Grid'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; + +import Container from 'components/Container'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import PermissionCatalogField from 'components/PermissionCatalogField/index.ee'; +import TextField from 'components/TextField'; +import * as URLS from 'config/urls'; +import { UPDATE_ROLE } from 'graphql/mutations/update-role.ee'; +import { + getPermissions, + getRoleWithComputedPermissions, +} from 'helpers/computePermissions.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRole from 'hooks/useRole.ee'; + +export default function EditRole() { + const formatMessage = useFormatMessage(); + const [updateRole, { loading }] = useMutation(UPDATE_ROLE); + const navigate = useNavigate(); + const { roleId } = useParams(); + const { data, loading: isRoleLoading } = useRole({ roleId }); + const role = data?.data; + const enqueueSnackbar = useEnqueueSnackbar(); + + const handleRoleUpdate = async (roleData) => { + try { + const newPermissions = getPermissions(roleData.computedPermissions); + await updateRole({ + variables: { + input: { + id: roleId, + name: roleData.name, + description: roleData.description, + permissions: newPermissions, + }, + }, + }); + + enqueueSnackbar(formatMessage('editRole.successfullyUpdated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-edit-role-success', + }, + }); + + navigate(URLS.ROLES); + } catch (error) { + throw new Error('Failed while updating!'); + } + }; + + const roleWithComputedPermissions = getRoleWithComputedPermissions(role); + + return ( + + + + + {formatMessage('editRolePage.title')} + + + + +
    + + {isRoleLoading && ( + <> + + + + )} + {!isRoleLoading && role && ( + <> + + + + + )} + + + + + {formatMessage('editRole.submit')} + + +
    +
    +
    +
    + ); +} diff --git a/packages/web/src/pages/EditUser/index.jsx b/packages/web/src/pages/EditUser/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..72094845c54ca7efe03353b0e44ca6575919e23b --- /dev/null +++ b/packages/web/src/pages/EditUser/index.jsx @@ -0,0 +1,142 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Grid from '@mui/material/Grid'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import MuiTextField from '@mui/material/TextField'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useQueryClient } from '@tanstack/react-query'; + +import Can from 'components/Can'; +import Container from 'components/Container'; +import ControlledAutocomplete from 'components/ControlledAutocomplete'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import TextField from 'components/TextField'; +import * as URLS from 'config/urls'; +import { UPDATE_USER } from 'graphql/mutations/update-user.ee'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useRoles from 'hooks/useRoles.ee'; +import useAdminUser from 'hooks/useAdminUser'; + +function generateRoleOptions(roles) { + return roles?.map(({ name: label, id: value }) => ({ label, value })); +} + +export default function EditUser() { + const formatMessage = useFormatMessage(); + const [updateUser, { loading }] = useMutation(UPDATE_USER); + const { userId } = useParams(); + const { data: userData, isLoading: isUserLoading } = useAdminUser({ userId }); + const user = userData?.data; + const { data, isLoading: isRolesLoading } = useRoles(); + const roles = data?.data; + const enqueueSnackbar = useEnqueueSnackbar(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + const handleUserUpdate = async (userDataToUpdate) => { + try { + await updateUser({ + variables: { + input: { + id: userId, + fullName: userDataToUpdate.fullName, + email: userDataToUpdate.email, + role: { + id: userDataToUpdate.role?.id, + }, + }, + }, + }); + queryClient.invalidateQueries({ queryKey: ['admin', 'users'] }); + + enqueueSnackbar(formatMessage('editUser.successfullyUpdated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-edit-user-success', + persist: true, + }, + }); + + navigate(URLS.USERS); + } catch (error) { + throw new Error('Failed while updating!'); + } + }; + + return ( + + + + + {formatMessage('editUserPage.title')} + + + + + {isUserLoading && ( + + + + + + + )} + + {!isUserLoading && ( +
    + + + + + + + ( + + )} + loading={isRolesLoading} + /> + + + + {formatMessage('editUser.submit')} + + +
    + )} +
    +
    +
    + ); +} diff --git a/packages/web/src/pages/Editor/create.jsx b/packages/web/src/pages/Editor/create.jsx new file mode 100644 index 0000000000000000000000000000000000000000..06eeccb6995caefae760b7879d6c7df41e3d4b51 --- /dev/null +++ b/packages/web/src/pages/Editor/create.jsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import { useMutation } from '@apollo/client'; +import CircularProgress from '@mui/material/CircularProgress'; +import Typography from '@mui/material/Typography'; +import * as URLS from 'config/urls'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { CREATE_FLOW } from 'graphql/mutations/create-flow'; +import Box from '@mui/material/Box'; +export default function CreateFlow() { + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const [createFlow] = useMutation(CREATE_FLOW); + const appKey = searchParams.get('appKey'); + const connectionId = searchParams.get('connectionId'); + React.useEffect(() => { + async function initiate() { + const variables = {}; + if (appKey) { + variables.triggerAppKey = appKey; + } + if (connectionId) { + variables.connectionId = connectionId; + } + const response = await createFlow({ + variables: { + input: variables, + }, + }); + const flowId = response.data?.createFlow?.id; + navigate(URLS.FLOW_EDITOR(flowId), { replace: true }); + } + initiate(); + }, [createFlow, navigate, appKey, connectionId]); + return ( + + + + + {formatMessage('createFlow.creating')} + + + ); +} diff --git a/packages/web/src/pages/Editor/index.jsx b/packages/web/src/pages/Editor/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a0260648e647a17d6ed403ee0722cdf323f35328 --- /dev/null +++ b/packages/web/src/pages/Editor/index.jsx @@ -0,0 +1,5 @@ +import * as React from 'react'; +import EditorLayout from 'components/EditorLayout'; +export default function FlowEditor() { + return ; +} diff --git a/packages/web/src/pages/Editor/routes.jsx b/packages/web/src/pages/Editor/routes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..23c5d3d5348bef51f4158c4f250879bb751a00d8 --- /dev/null +++ b/packages/web/src/pages/Editor/routes.jsx @@ -0,0 +1,13 @@ +import * as React from 'react'; +import { Routes, Route } from 'react-router-dom'; +import CreateFlowPage from './create'; +import EditorPage from './index'; +export default function EditorRoutes() { + return ( + + } /> + + } /> + + ); +} diff --git a/packages/web/src/pages/Execution/index.jsx b/packages/web/src/pages/Execution/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..39cec79a0f416d65eefcccffac9db991f6ef977f --- /dev/null +++ b/packages/web/src/pages/Execution/index.jsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import Grid from '@mui/material/Grid'; +import Box from '@mui/material/Box'; +import AlertTitle from '@mui/material/AlertTitle'; +import Alert from '@mui/material/Alert'; + +import useFormatMessage from 'hooks/useFormatMessage'; +import ExecutionHeader from 'components/ExecutionHeader'; +import ExecutionStep from 'components/ExecutionStep'; +import Container from 'components/Container'; +import useExecutionSteps from 'hooks/useExecutionSteps'; +import useExecution from 'hooks/useExecution'; + +export default function Execution() { + const { executionId } = useParams(); + const formatMessage = useFormatMessage(); + + const { data: execution } = useExecution({ executionId }); + + const { + data, + isLoading: isExecutionStepsLoading, + fetchNextPage, + hasNextPage, + isFetching, + isFetchingNextPage, + } = useExecutionSteps({ + executionId: executionId, + }); + + React.useEffect(() => { + if (!isFetching && !isFetchingNextPage && hasNextPage) { + fetchNextPage(); + } + }, [isFetching, isFetchingNextPage, hasNextPage, fetchNextPage]); + + return ( + + + + + {!isExecutionStepsLoading && !data?.pages?.[0].data.length && ( + + + {formatMessage('execution.noDataTitle')} + + + + {formatMessage('execution.noDataMessage')} + + + )} + + {data?.pages?.map((group, i) => ( + + {group?.data?.map((executionStep) => ( + + ))} + + ))} + + + ); +} diff --git a/packages/web/src/pages/Executions/index.jsx b/packages/web/src/pages/Executions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5ca6ea12937dd496208ea3fa59af1192a8b0ab2e --- /dev/null +++ b/packages/web/src/pages/Executions/index.jsx @@ -0,0 +1,85 @@ +import * as React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import CircularProgress from '@mui/material/CircularProgress'; +import Divider from '@mui/material/Divider'; +import Pagination from '@mui/material/Pagination'; +import PaginationItem from '@mui/material/PaginationItem'; + +import NoResultFound from 'components/NoResultFound'; +import ExecutionRow from 'components/ExecutionRow'; +import Container from 'components/Container'; +import PageTitle from 'components/PageTitle'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useExecutions from 'hooks/useExecutions'; + +export default function Executions() { + const formatMessage = useFormatMessage(); + const [searchParams, setSearchParams] = useSearchParams(); + const page = parseInt(searchParams.get('page') || '', 10) || 1; + + const { data, isLoading: isExecutionsLoading } = useExecutions( + { page: page }, + { refetchInterval: 5000 }, + ); + + const { data: executions, meta: pageInfo } = data || {}; + + const hasExecutions = executions?.length; + + return ( + + + + + {formatMessage('executions.title')} + + + + + + {isExecutionsLoading && ( + + )} + + {!isExecutionsLoading && !hasExecutions && ( + + )} + + {!isExecutionsLoading && + executions?.map((execution) => ( + + ))} + + {pageInfo && pageInfo.totalPages > 1 && ( + + setSearchParams({ page: page.toString() }) + } + renderItem={(item) => ( + + )} + /> + )} + + + ); +} diff --git a/packages/web/src/pages/Flow/index.jsx b/packages/web/src/pages/Flow/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..922dfe6bbcc82c972ed9e684c97e7a5dd46ebd9b --- /dev/null +++ b/packages/web/src/pages/Flow/index.jsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import { useParams } from 'react-router-dom'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import Container from 'components/Container'; +export default function Flow() { + const { flowId } = useParams(); + return ( + + + + + {flowId} + + + + + ); +} diff --git a/packages/web/src/pages/Flows/index.jsx b/packages/web/src/pages/Flows/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..22544394f5ea24fc5f4b4aca4cfa315ca5527b7b --- /dev/null +++ b/packages/web/src/pages/Flows/index.jsx @@ -0,0 +1,150 @@ +import * as React from 'react'; +import { Link, useSearchParams } from 'react-router-dom'; +import debounce from 'lodash/debounce'; +import Box from '@mui/material/Box'; +import Grid from '@mui/material/Grid'; +import AddIcon from '@mui/icons-material/Add'; +import CircularProgress from '@mui/material/CircularProgress'; +import Divider from '@mui/material/Divider'; +import Pagination from '@mui/material/Pagination'; +import PaginationItem from '@mui/material/PaginationItem'; + +import Can from 'components/Can'; +import FlowRow from 'components/FlowRow'; +import NoResultFound from 'components/NoResultFound'; +import ConditionalIconButton from 'components/ConditionalIconButton'; +import Container from 'components/Container'; +import PageTitle from 'components/PageTitle'; +import SearchInput from 'components/SearchInput'; +import useFormatMessage from 'hooks/useFormatMessage'; +import * as URLS from 'config/urls'; +import useLazyFlows from 'hooks/useLazyFlows'; + +export default function Flows() { + const formatMessage = useFormatMessage(); + const [searchParams, setSearchParams] = useSearchParams(); + const page = parseInt(searchParams.get('page') || '', 10) || 1; + const [flowName, setFlowName] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(false); + + const { data, mutate: fetchFlows } = useLazyFlows( + { flowName, page }, + { + onSettled: () => { + setIsLoading(false); + }, + }, + ); + + const fetchData = React.useMemo( + () => debounce(fetchFlows, 300), + [fetchFlows], + ); + + React.useEffect(() => { + setIsLoading(true); + + fetchData({ flowName, page }); + + return () => { + fetchData.cancel(); + }; + }, [fetchData, flowName, page]); + + React.useEffect( + function resetPageOnSearch() { + // reset search params which only consists of `page` + setSearchParams({}); + }, + [flowName], + ); + + const flows = data?.data || []; + const pageInfo = data?.meta; + const hasFlows = flows?.length; + + const onSearchChange = React.useCallback((event) => { + setFlowName(event.target.value); + }, []); + + return ( + + + + + {formatMessage('flows.title')} + + + + + + + + + {(allowed) => ( + } + to={URLS.CREATE_FLOW} + data-test="create-flow-button" + > + {formatMessage('flows.create')} + + )} + + + + + + {isLoading && ( + + )} + {!isLoading && + flows?.map((flow) => ( + + ))} + {!isLoading && !hasFlows && ( + + )} + {!isLoading && pageInfo && pageInfo.totalPages > 1 && ( + + setSearchParams({ page: page.toString() }) + } + renderItem={(item) => ( + + )} + /> + )} + + + ); +} diff --git a/packages/web/src/pages/ForgotPassword/index.ee.jsx b/packages/web/src/pages/ForgotPassword/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..89cbb144dd4e5622e710b0b305329ba2dd9eaf0b --- /dev/null +++ b/packages/web/src/pages/ForgotPassword/index.ee.jsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import useCloud from 'hooks/useCloud'; +import Container from 'components/Container'; +import ForgotPasswordForm from 'components/ForgotPasswordForm/index.ee'; +export default function ForgotPassword() { + useCloud({ redirect: true }); + return ( + + + + + + ); +} diff --git a/packages/web/src/pages/Login/index.jsx b/packages/web/src/pages/Login/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..07e9cc44286da7df6ead515b751ced2dbfdeab09 --- /dev/null +++ b/packages/web/src/pages/Login/index.jsx @@ -0,0 +1,19 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import Container from 'components/Container'; +import LoginForm from 'components/LoginForm'; +import SsoProviders from 'components/SsoProviders/index.ee'; +export default function Login() { + return ( + + + + + + + + + + ); +} diff --git a/packages/web/src/pages/LoginCallback/index.jsx b/packages/web/src/pages/LoginCallback/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e874c537482214fd323901793f0c98812d2040fc --- /dev/null +++ b/packages/web/src/pages/LoginCallback/index.jsx @@ -0,0 +1,22 @@ +import * as React from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import useAuthentication from 'hooks/useAuthentication'; +import * as URLS from 'config/urls'; +export default function LoginCallback() { + const navigate = useNavigate(); + const authentication = useAuthentication(); + const [searchParams] = useSearchParams(); + React.useEffect(() => { + if (authentication.isAuthenticated) { + navigate(URLS.DASHBOARD); + } + }, [authentication.isAuthenticated]); + React.useEffect(() => { + const token = searchParams.get('token'); + if (token) { + authentication.updateToken(token); + } + // TODO: handle non-existing token scenario + }, []); + return <>; +} diff --git a/packages/web/src/pages/Notifications/index.jsx b/packages/web/src/pages/Notifications/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eb4b8316e2adb356debff241a655eb298a595f5c --- /dev/null +++ b/packages/web/src/pages/Notifications/index.jsx @@ -0,0 +1,54 @@ +import Box from '@mui/material/Box'; +import Stack from '@mui/material/Stack'; +import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; + +import Container from 'components/Container'; +import NotificationCard from 'components/NotificationCard'; +import PageTitle from 'components/PageTitle'; +import * as URLS from 'config/urls'; +import useAutomatischInfo from 'hooks/useAutomatischInfo'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useAutomatischNotifications from 'hooks/useAutomatischNotifications'; + +export default function Updates() { + const navigate = useNavigate(); + const formatMessage = useFormatMessage(); + const { data: notificationsData } = useAutomatischNotifications(); + const { data: automatischInfo, isPending } = useAutomatischInfo(); + const isMation = automatischInfo?.data.isMation; + const notifications = notificationsData?.data || []; + + React.useEffect( + function redirectToHomepageInMation() { + if (!navigate) return; + + if (!isPending && isMation) { + navigate(URLS.DASHBOARD); + } + }, + [isPending, isMation, navigate], + ); + + return ( + + + + {formatMessage('notifications.title')} + + + + {notifications.map((notification) => ( + + ))} + + + + ); +} diff --git a/packages/web/src/pages/PlanUpgrade/index.ee.jsx b/packages/web/src/pages/PlanUpgrade/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..73c2f9a8ef8350d0fd809dc5a318f57b40cbef43 --- /dev/null +++ b/packages/web/src/pages/PlanUpgrade/index.ee.jsx @@ -0,0 +1,34 @@ +import * as React from 'react'; +import { Navigate } from 'react-router-dom'; +import Grid from '@mui/material/Grid'; +import * as URLS from 'config/urls'; +import UpgradeFreeTrial from 'components/UpgradeFreeTrial/index.ee'; +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useCloud from 'hooks/useCloud'; +function PlanUpgrade() { + const isCloud = useCloud(); + const formatMessage = useFormatMessage(); + // redirect to the initial settings page + if (isCloud === false) { + return ; + } + // render nothing until we know if it's cloud or not + // here, `isCloud` is not `false`, but `undefined` + if (!isCloud) return ; + return ( + + + + {formatMessage('planUpgrade.title')} + + + + + + + + ); +} +export default PlanUpgrade; diff --git a/packages/web/src/pages/ProfileSettings/index.jsx b/packages/web/src/pages/ProfileSettings/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0f5acc0443ebc9fd414973274a453fea78c792b7 --- /dev/null +++ b/packages/web/src/pages/ProfileSettings/index.jsx @@ -0,0 +1,210 @@ +import { useMutation } from '@apollo/client'; +import { yupResolver } from '@hookform/resolvers/yup'; +import Alert from '@mui/material/Alert'; +import AlertTitle from '@mui/material/AlertTitle'; +import Button from '@mui/material/Button'; +import Grid from '@mui/material/Grid'; +import Typography from '@mui/material/Typography'; +import { styled } from '@mui/material/styles'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import * as React from 'react'; +import * as yup from 'yup'; + +import Container from 'components/Container'; +import DeleteAccountDialog from 'components/DeleteAccountDialog/index.ee'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import TextField from 'components/TextField'; +import { UPDATE_CURRENT_USER } from 'graphql/mutations/update-current-user'; +import useCurrentUser from 'hooks/useCurrentUser'; +import useFormatMessage from 'hooks/useFormatMessage'; +import { useQueryClient } from '@tanstack/react-query'; + +const validationSchema = yup + .object({ + fullName: yup.string().required(), + email: yup.string().email().required(), + password: yup.string(), + confirmPassword: yup + .string() + .oneOf([yup.ref('password')], 'Passwords must match'), + }) + .required(); + +const StyledForm = styled(Form)` + display: flex; + align-items: end; + flex-direction: column; +`; + +function ProfileSettings() { + const [showDeleteAccountConfirmation, setShowDeleteAccountConfirmation] = + React.useState(false); + const enqueueSnackbar = useEnqueueSnackbar(); + const { data } = useCurrentUser(); + const currentUser = data?.data; + const formatMessage = useFormatMessage(); + const [updateCurrentUser] = useMutation(UPDATE_CURRENT_USER); + const queryClient = useQueryClient(); + + const handleProfileSettingsUpdate = async (data) => { + const { fullName, password, email } = data; + const mutationInput = { + fullName, + email, + }; + + if (password) { + mutationInput.password = password; + } + + await updateCurrentUser({ + variables: { + input: mutationInput, + }, + optimisticResponse: { + updateCurrentUser: { + __typename: 'User', + id: currentUser.id, + fullName, + email, + }, + }, + }); + + await queryClient.invalidateQueries({ queryKey: ['users', 'me'] }); + + enqueueSnackbar(formatMessage('profileSettings.updatedProfile'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-update-profile-settings-success', + }, + }); + }; + + return ( + + + + {formatMessage('profileSettings.title')} + + + + ( + <> + + + + + + + + + + + )} + /> + + + + + + {formatMessage('profileSettings.deleteMyAccount')} + + + + {formatMessage('profileSettings.deleteAccountSubtitle')} + + +
      +
    1. {formatMessage('profileSettings.deleteAccountResult1')}
    2. +
    3. {formatMessage('profileSettings.deleteAccountResult2')}
    4. +
    5. {formatMessage('profileSettings.deleteAccountResult3')}
    6. +
    7. {formatMessage('profileSettings.deleteAccountResult4')}
    8. +
    + + + + {showDeleteAccountConfirmation && ( + setShowDeleteAccountConfirmation(false)} + /> + )} +
    +
    +
    +
    + ); +} +export default ProfileSettings; diff --git a/packages/web/src/pages/ResetPassword/index.ee.jsx b/packages/web/src/pages/ResetPassword/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..98e9e78394d651b021c6f3fd5ad3d78678ceda99 --- /dev/null +++ b/packages/web/src/pages/ResetPassword/index.ee.jsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import useCloud from 'hooks/useCloud'; +import Container from 'components/Container'; +import ResetPasswordForm from 'components/ResetPasswordForm/index.ee'; +export default function ResetPassword() { + useCloud({ redirect: true }); + return ( + + + + + + ); +} diff --git a/packages/web/src/pages/Roles/index.ee.jsx b/packages/web/src/pages/Roles/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..03b21df3f6899b8df4db4d79a133bd39299842c9 --- /dev/null +++ b/packages/web/src/pages/Roles/index.ee.jsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Grid from '@mui/material/Grid'; +import AddIcon from '@mui/icons-material/Add'; +import * as URLS from 'config/urls'; +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import RoleList from 'components/RoleList/index.ee'; +import ConditionalIconButton from 'components/ConditionalIconButton'; +import useFormatMessage from 'hooks/useFormatMessage'; +function RolesPage() { + const formatMessage = useFormatMessage(); + return ( + + + + + + {formatMessage('rolesPage.title')} + + + + + } + data-test="create-role" + > + {formatMessage('rolesPage.createRole')} + + + + + + + + + + ); +} +export default RolesPage; diff --git a/packages/web/src/pages/SignUp/index.ee.jsx b/packages/web/src/pages/SignUp/index.ee.jsx new file mode 100644 index 0000000000000000000000000000000000000000..90bd7c9e6610595527f2e15056125135636b3881 --- /dev/null +++ b/packages/web/src/pages/SignUp/index.ee.jsx @@ -0,0 +1,15 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import useCloud from 'hooks/useCloud'; +import Container from 'components/Container'; +import SignUpForm from 'components/SignUpForm/index.ee'; +export default function SignUp() { + useCloud({ redirect: true }); + return ( + + + + + + ); +} diff --git a/packages/web/src/pages/UserInterface/index.jsx b/packages/web/src/pages/UserInterface/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..cf5bdd199458b111237f5c3a47591c4f7fd0b1c7 --- /dev/null +++ b/packages/web/src/pages/UserInterface/index.jsx @@ -0,0 +1,165 @@ +import { useMutation } from '@apollo/client'; +import LoadingButton from '@mui/lab/LoadingButton'; +import Grid from '@mui/material/Grid'; +import Skeleton from '@mui/material/Skeleton'; +import Stack from '@mui/material/Stack'; +import merge from 'lodash/merge'; +import * as React from 'react'; +import { useQueryClient } from '@tanstack/react-query'; + +import ColorInput from 'components/ColorInput'; +import Container from 'components/Container'; +import Form from 'components/Form'; +import PageTitle from 'components/PageTitle'; +import TextField from 'components/TextField'; +import { UPDATE_CONFIG } from 'graphql/mutations/update-config.ee'; +import nestObject from 'helpers/nestObject'; +import useAutomatischConfig from 'hooks/useAutomatischConfig'; +import useFormatMessage from 'hooks/useFormatMessage'; +import useEnqueueSnackbar from 'hooks/useEnqueueSnackbar'; +import { + primaryDarkColor, + primaryLightColor, + primaryMainColor, +} from 'styles/theme'; + +const getPrimaryMainColor = (color) => color || primaryMainColor; +const getPrimaryDarkColor = (color) => color || primaryDarkColor; +const getPrimaryLightColor = (color) => color || primaryLightColor; + +const defaultValues = { + title: 'Automatisch', + 'palette.primary.main': primaryMainColor, + 'palette.primary.dark': primaryDarkColor, + 'palette.primary.light': primaryLightColor, +}; + +export default function UserInterface() { + const formatMessage = useFormatMessage(); + const [updateConfig, { loading }] = useMutation(UPDATE_CONFIG); + const { data: configData, isLoading: configLoading } = useAutomatischConfig(); + const config = configData?.data; + const queryClient = useQueryClient(); + + const enqueueSnackbar = useEnqueueSnackbar(); + const configWithDefaults = merge({}, defaultValues, nestObject(config)); + const handleUserInterfaceUpdate = async (uiData) => { + try { + const input = { + title: uiData?.title, + 'palette.primary.main': getPrimaryMainColor( + uiData?.palette?.primary.main, + ), + 'palette.primary.dark': getPrimaryDarkColor( + uiData?.palette?.primary.dark, + ), + 'palette.primary.light': getPrimaryLightColor( + uiData?.palette?.primary.light, + ), + 'logo.svgData': uiData?.logo?.svgData, + }; + await updateConfig({ + variables: { + input, + }, + optimisticResponse: { + updateConfig: input, + }, + update: async function () { + queryClient.invalidateQueries({ + queryKey: ['automatisch', 'config'], + }); + }, + }); + enqueueSnackbar(formatMessage('userInterfacePage.successfullyUpdated'), { + variant: 'success', + SnackbarProps: { + 'data-test': 'snackbar-update-user-interface-success', + }, + }); + } catch (error) { + throw new Error('Failed while updating!'); + } + }; + return ( + + + + {formatMessage('userInterfacePage.title')} + + + + {configLoading && ( + + + + + + + + )} + {!configLoading && ( +
    + + + + + + + + + + + + + {formatMessage('userInterfacePage.submit')} + + +
    + )} +
    +
    +
    + ); +} diff --git a/packages/web/src/pages/Users/index.jsx b/packages/web/src/pages/Users/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9fe3893ab54ed8a7c747f36ad8916c27fa781d40 --- /dev/null +++ b/packages/web/src/pages/Users/index.jsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import { Link } from 'react-router-dom'; +import Grid from '@mui/material/Grid'; +import AddIcon from '@mui/icons-material/Add'; +import * as URLS from 'config/urls'; +import PageTitle from 'components/PageTitle'; +import Container from 'components/Container'; +import UserList from 'components/UserList'; +import ConditionalIconButton from 'components/ConditionalIconButton'; +import useFormatMessage from 'hooks/useFormatMessage'; +function UsersPage() { + const formatMessage = useFormatMessage(); + return ( + + + + + + {formatMessage('usersPage.title')} + + + + + } + data-test="create-user" + > + {formatMessage('usersPage.createUser')} + + + + + + + + + + ); +} +export default UsersPage; diff --git a/packages/web/src/propTypes/propTypes.js b/packages/web/src/propTypes/propTypes.js new file mode 100644 index 0000000000000000000000000000000000000000..29629d237729c94cf00f9f9d878cf6de3e273a05 --- /dev/null +++ b/packages/web/src/propTypes/propTypes.js @@ -0,0 +1,483 @@ +import PropTypes from 'prop-types'; + +export const JSONValuePropType = PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.bool, + PropTypes.object, + PropTypes.array, +]); + +export const AuthenticationStepFieldPropType = PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.oneOfType([PropTypes.string]), + properties: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.string, + }), + ), +}); + +export const AuthenticationStepPropType = PropTypes.shape({ + type: PropTypes.oneOf(['mutation', 'openWithPopup']), + name: PropTypes.string, + arguments: PropTypes.arrayOf(AuthenticationStepFieldPropType), +}); + +export const FieldTextPropType = PropTypes.shape({ + key: PropTypes.string, + label: PropTypes.string, + type: PropTypes.oneOf(['string']), + required: PropTypes.bool, + readOnly: PropTypes.bool, + value: PropTypes.string, + placeholder: PropTypes.oneOfType([PropTypes.string]), + description: PropTypes.string, + docUrl: PropTypes.string, + clickToCopy: PropTypes.bool, + variables: PropTypes.bool, + dependsOn: PropTypes.arrayOf(PropTypes.string), +}); + +export const FieldDropdownSourcePropType = PropTypes.shape({ + type: PropTypes.string, + name: PropTypes.string, + arguments: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.string, + }), + ), +}); + +export const FieldDropdownAdditionalFieldsPropType = PropTypes.shape({ + type: PropTypes.string, + name: PropTypes.string, + arguments: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string, + value: PropTypes.string, + }), + ), +}); + +export const FieldDropdownOptionPropType = PropTypes.shape({ + label: PropTypes.string, + value: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, + PropTypes.number, + ]), +}); + +export const FieldDropdownPropType = PropTypes.shape({ + key: PropTypes.string, + label: PropTypes.string, + type: PropTypes.oneOf(['dropdown']), + required: PropTypes.bool, + readOnly: PropTypes.bool, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]), + placeholder: PropTypes.oneOfType([PropTypes.string]), + description: PropTypes.string, + docUrl: PropTypes.string, + clickToCopy: PropTypes.bool, + variables: PropTypes.bool, + dependsOn: PropTypes.arrayOf(PropTypes.string), + options: PropTypes.arrayOf(FieldDropdownOptionPropType), + source: FieldDropdownSourcePropType, + additionalFields: FieldDropdownAdditionalFieldsPropType, +}); + +export const FieldsPropType = PropTypes.arrayOf( + PropTypes.oneOfType([FieldDropdownPropType, FieldTextPropType]), +); + +export const FieldDynamicPropType = PropTypes.shape({ + key: PropTypes.string, + label: PropTypes.string, + type: PropTypes.oneOf(['dynamic']), + required: PropTypes.bool, + readOnly: PropTypes.bool, + description: PropTypes.string, + value: PropTypes.arrayOf(PropTypes.object), + fields: FieldsPropType, +}); + +export const FieldPropType = PropTypes.oneOfType([ + FieldDropdownPropType, + FieldTextPropType, + FieldDynamicPropType, +]); + +export const SubstepPropType = PropTypes.shape({ + key: PropTypes.string, + name: PropTypes.string, + arguments: PropTypes.arrayOf(FieldPropType), +}); + +export const RawTriggerPropType = PropTypes.shape({ + name: PropTypes.string, + key: PropTypes.string, + type: PropTypes.oneOf(['webhook', 'polling']), + showWebhookUrl: PropTypes.bool, + pollInterval: PropTypes.number, + description: PropTypes.string, + useSingletonWebhook: PropTypes.bool, + singletonWebhookRefValueParameter: PropTypes.string, + getInterval: PropTypes.func, + run: PropTypes.func, + testRun: PropTypes.func, + registerHook: PropTypes.func, + unregisterHook: PropTypes.func, + arguments: PropTypes.arrayOf(FieldPropType), +}); + +export const TriggerPropType = PropTypes.shape({ + name: PropTypes.string, + key: PropTypes.string, + type: PropTypes.oneOf(['webhook', 'polling']), + showWebhookUrl: PropTypes.bool, + pollInterval: PropTypes.number, + description: PropTypes.string, + useSingletonWebhook: PropTypes.bool, + singletonWebhookRefValueParameter: PropTypes.string, + getInterval: PropTypes.func, + run: PropTypes.func, + testRun: PropTypes.func, + registerHook: PropTypes.func, + unregisterHook: PropTypes.func, + substeps: PropTypes.arrayOf(SubstepPropType), +}); + +export const RawActionPropType = PropTypes.shape({ + name: PropTypes.string, + key: PropTypes.string, + description: PropTypes.string, + run: PropTypes.func, + arguments: PropTypes.arrayOf(FieldPropType), +}); + +export const ActionPropType = PropTypes.shape({ + name: PropTypes.string, + key: PropTypes.string, + description: PropTypes.string, + run: PropTypes.func, + substeps: PropTypes.arrayOf(SubstepPropType), +}); + +export const AuthPropType = PropTypes.shape({ + generateAuthUrl: PropTypes.func, + verifyCredentials: PropTypes.func, + isStillVerified: PropTypes.func, + refreshToken: PropTypes.func, + verifyWebhook: PropTypes.func, + isRefreshTokenRequested: PropTypes.bool, + fields: PropTypes.arrayOf(FieldPropType), + authenticationSteps: PropTypes.arrayOf(AuthenticationStepPropType), + reconnectionSteps: PropTypes.arrayOf(AuthenticationStepPropType), + sharedAuthenticationSteps: PropTypes.arrayOf(AuthenticationStepPropType), + sharedReconnectionSteps: PropTypes.arrayOf(AuthenticationStepPropType), +}); + +export const AppPropType = PropTypes.shape({ + name: PropTypes.string, + key: PropTypes.string, + iconUrl: PropTypes.string, + docUrl: PropTypes.string, + authDocUrl: PropTypes.string, + primaryColor: PropTypes.string, + supportsConnections: PropTypes.bool, + apiBaseUrl: PropTypes.string, + baseUrl: PropTypes.string, + auth: AuthPropType, + connectionCount: PropTypes.number, + flowCount: PropTypes.number, + beforeRequest: PropTypes.arrayOf(PropTypes.func), + dynamicData: PropTypes.object, + dynamicFields: PropTypes.object, + triggers: PropTypes.arrayOf(TriggerPropType), + actions: PropTypes.arrayOf(ActionPropType), +}); + +export const ConnectionPropType = PropTypes.shape({ + id: PropTypes.string, + key: PropTypes.string, + data: PropTypes.string, + formattedData: PropTypes.object, + userId: PropTypes.string, + verified: PropTypes.bool, + count: PropTypes.number, + flowCount: PropTypes.number, + appData: AppPropType, + createdAt: PropTypes.number, + reconnectable: PropTypes.bool, + appAuthClientId: PropTypes.string, +}); + +AppPropType.connection = PropTypes.arrayOf(ConnectionPropType); + +export const ExecutionStepPropType = PropTypes.shape({ + id: PropTypes.string, + executionId: PropTypes.string, + stepId: PropTypes.string, + dataIn: PropTypes.object, + dataOut: PropTypes.object, + errorDetails: PropTypes.object, + status: PropTypes.string, + createdAt: PropTypes.number, + updatedAt: PropTypes.number, +}); + +export const FlowPropType = PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + userId: PropTypes.string, + active: PropTypes.bool, + status: PropTypes.oneOf(['paused', 'published', 'draft']), + createdAt: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + PropTypes.instanceOf(Date), + ]), + updatedAt: PropTypes.oneOfType([ + PropTypes.number, + PropTypes.string, + PropTypes.instanceOf(Date), + ]), + remoteWebhookId: PropTypes.string, + lastInternalId: PropTypes.func, +}); + +export const StepPropType = PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + flowId: PropTypes.string, + key: PropTypes.string, + appKey: PropTypes.string, + iconUrl: PropTypes.string, + webhookUrl: PropTypes.string, + type: PropTypes.oneOf(['action', 'trigger']), + connectionId: PropTypes.string, + status: PropTypes.string, + position: PropTypes.number, + parameters: PropTypes.object, + connection: ConnectionPropType, + flow: FlowPropType, + executionSteps: PropTypes.arrayOf(ExecutionStepPropType), + output: PropTypes.object, + appData: AppPropType, +}); + +ExecutionStepPropType.step = StepPropType; +FlowPropType.steps = PropTypes.arrayOf(StepPropType); + +export const ExecutionPropType = PropTypes.shape({ + id: PropTypes.string, + flowId: PropTypes.string, + flow: FlowPropType, + testRun: PropTypes.bool, + status: PropTypes.oneOf(['success', 'failure']), + executionSteps: PropTypes.arrayOf(ExecutionStepPropType), + updatedAt: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date), + ]), + createdAt: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + PropTypes.instanceOf(Date), + ]), +}); + +export const PermissionPropType = PropTypes.shape({ + id: PropTypes.string, + action: PropTypes.string, + subject: PropTypes.string, + conditions: PropTypes.arrayOf(PropTypes.string), +}); + +export const RolePropType = PropTypes.shape({ + id: PropTypes.string, + key: PropTypes.string, + name: PropTypes.string, + description: PropTypes.string, + isAdmin: PropTypes.bool, + permissions: PropTypes.arrayOf(PermissionPropType), +}); + +export const UserPropType = PropTypes.shape({ + id: PropTypes.string, + fullName: PropTypes.string, + email: PropTypes.string, + password: PropTypes.string, + connections: PropTypes.arrayOf(ConnectionPropType), + flows: PropTypes.arrayOf(FlowPropType), + steps: PropTypes.arrayOf(StepPropType), + role: RolePropType, + permissions: PropTypes.arrayOf(PermissionPropType), + createdAt: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Date), + ]), + updatedAt: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Date), + ]), + trialExpiryDate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.instanceOf(Date), + ]), +}); + +export const PermissionCatalogPropType = PropTypes.shape({ + actions: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + key: PropTypes.string, + subjects: PropTypes.arrayOf(PropTypes.string), + }), + ), + subjects: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + key: PropTypes.string, + }), + ), + conditions: PropTypes.arrayOf( + PropTypes.shape({ + label: PropTypes.string, + key: PropTypes.string, + }), + ), +}); + +export const ConfigPropType = PropTypes.shape({ + id: PropTypes.string, + key: PropTypes.string, + value: PropTypes.object, +}); + +export const TriggerItemPropType = PropTypes.shape({ + raw: PropTypes.object, + meta: PropTypes.shape({ + internalId: PropTypes.string, + }), +}); + +export const TriggerOutputPropType = PropTypes.shape({ + data: PropTypes.arrayOf(TriggerItemPropType), + error: PropTypes.object, +}); + +export const ActionItemPropType = PropTypes.shape({ + raw: PropTypes.object, +}); + +export const IActionOutputPropType = PropTypes.shape({ + data: ActionItemPropType, + error: PropTypes.object, +}); + +export const AuthenticationPropType = PropTypes.shape({ + client: PropTypes.any, + verifyCredentials: PropTypes.func, + isStillVerified: PropTypes.func, +}); + +export const PaymentPlanPropType = PropTypes.shape({ + price: PropTypes.string, + name: PropTypes.string, + limit: PropTypes.string, + productId: PropTypes.string, +}); + +export const BillingTextCardActionPropType = PropTypes.shape({ + type: PropTypes.oneOf(['text']), + text: PropTypes.string, +}); + +export const BillingLinkCardActionPropType = PropTypes.shape({ + type: PropTypes.oneOf(['link']), + text: PropTypes.string, + src: PropTypes.string, +}); + +export const BillingCardActionPropType = PropTypes.oneOfType([ + BillingTextCardActionPropType, + BillingLinkCardActionPropType, +]); + +export const SubscriptionPropType = PropTypes.shape({ + status: PropTypes.string, + monthlyQuota: PropTypes.shape({ + title: PropTypes.string, + action: BillingCardActionPropType, + }), + nextBillDate: PropTypes.shape({ + title: PropTypes.string, + action: BillingCardActionPropType, + }), + nextBillAmount: PropTypes.shape({ + title: PropTypes.string, + action: BillingCardActionPropType, + }), +}); + +export const InvoicePropType = PropTypes.shape({ + id: PropTypes.number, + amount: PropTypes.number, + currency: PropTypes.string, + payout_date: PropTypes.string, + receipt_url: PropTypes.string, +}); + +export const SamlAuthProviderPropType = PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + certificate: PropTypes.string, + signatureAlgorithm: PropTypes.oneOf(['sha1', 'sha256', 'sha512']), + issuer: PropTypes.string, + entryPoint: PropTypes.string, + firstnameAttributeName: PropTypes.string, + surnameAttributeName: PropTypes.string, + emailAttributeName: PropTypes.string, + roleAttributeName: PropTypes.string, + defaultRoleId: PropTypes.string, + active: PropTypes.bool, + loginUrl: PropTypes.string, +}); + +export const SamlAuthProviderRolePropType = PropTypes.shape({ + id: PropTypes.string, + samlAuthProviderId: PropTypes.string, + roleId: PropTypes.string, + remoteRoleName: PropTypes.string, +}); + +export const AppConfigPropType = PropTypes.shape({ + id: PropTypes.string, + key: PropTypes.string, + allowCustomConnection: PropTypes.bool, + canConnect: PropTypes.bool, + canCustomConnect: PropTypes.bool, + shared: PropTypes.bool, + disabled: PropTypes.bool, +}); + +export const AppAuthClientPropType = PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + appConfigId: PropTypes.string, + authDefaults: PropTypes.string, + formattedAuthDefaults: PropTypes.object, + active: PropTypes.bool, +}); + +export const NotificationPropType = PropTypes.shape({ + name: PropTypes.string, + createdAt: PropTypes.string, + documentationUrl: PropTypes.string, + description: PropTypes.string, +}); diff --git a/packages/web/src/reportWebVitals.js b/packages/web/src/reportWebVitals.js new file mode 100644 index 0000000000000000000000000000000000000000..192e8336370b7f8d4168c9088dad0f66438227f0 --- /dev/null +++ b/packages/web/src/reportWebVitals.js @@ -0,0 +1,12 @@ +const reportWebVitals = (onPerfEntry) => { + if (onPerfEntry && onPerfEntry instanceof Function) { + import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + getCLS(onPerfEntry); + getFID(onPerfEntry); + getFCP(onPerfEntry); + getLCP(onPerfEntry); + getTTFB(onPerfEntry); + }); + } +}; +export default reportWebVitals; diff --git a/packages/web/src/routes.jsx b/packages/web/src/routes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..152b4e7a1b49081400116730ff95953dac953d59 --- /dev/null +++ b/packages/web/src/routes.jsx @@ -0,0 +1,155 @@ +import { Route, Routes as ReactRouterRoutes, Navigate } from 'react-router-dom'; + +import Layout from 'components/Layout'; +import NoResultFound from 'components/NotFound'; +import PublicLayout from 'components/PublicLayout'; +import AdminSettingsLayout from 'components/AdminSettingsLayout'; +import Applications from 'pages/Applications'; +import Application from 'pages/Application'; +import Executions from 'pages/Executions'; +import Execution from 'pages/Execution'; +import Flows from 'pages/Flows'; +import Flow from 'pages/Flow'; +import Login from 'pages/Login'; +import LoginCallback from 'pages/LoginCallback'; +import SignUp from 'pages/SignUp/index.ee'; +import ForgotPassword from 'pages/ForgotPassword/index.ee'; +import ResetPassword from 'pages/ResetPassword/index.ee'; +import EditorRoutes from 'pages/Editor/routes'; +import * as URLS from 'config/urls'; +import settingsRoutes from './settingsRoutes'; +import adminSettingsRoutes from './adminSettingsRoutes'; +import Notifications from 'pages/Notifications'; +import useAutomatischConfig from 'hooks/useAutomatischConfig'; +import useAuthentication from 'hooks/useAuthentication'; + +function Routes() { + const { data: configData } = useAutomatischConfig(); + const { isAuthenticated } = useAuthentication(); + const config = configData?.data; + + return ( + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + } /> + + + + + } + /> + + } /> + + + + + } + /> + + + + + } + /> + + + + + } + /> + + {!config?.disableNotificationsPage && ( + + + + } + /> + )} + + + } + /> + + {settingsRoutes} + + }> + {adminSettingsRoutes} + + } /> + + ); +} + +export default ; diff --git a/packages/web/src/settingsRoutes.jsx b/packages/web/src/settingsRoutes.jsx new file mode 100644 index 0000000000000000000000000000000000000000..2b2ab77733172cb33d22a9be92f8572780b4efe6 --- /dev/null +++ b/packages/web/src/settingsRoutes.jsx @@ -0,0 +1,44 @@ +import { Route, Navigate } from 'react-router-dom'; +import SettingsLayout from 'components/SettingsLayout'; +import { PaddleProvider } from 'contexts/Paddle.ee'; +import ProfileSettings from 'pages/ProfileSettings'; +import BillingAndUsageSettings from 'pages/BillingAndUsageSettings/index.ee'; +import PlanUpgrade from 'pages/PlanUpgrade/index.ee'; +import * as URLS from 'config/urls'; +export default ( + <> + + + + } + /> + + + + + } + /> + + + + + + + } + /> + + } + /> + +); diff --git a/packages/web/src/setupTests.js b/packages/web/src/setupTests.js new file mode 100644 index 0000000000000000000000000000000000000000..8f2609b7b3e0e3897ab3bcaad13caf6876e48699 --- /dev/null +++ b/packages/web/src/setupTests.js @@ -0,0 +1,5 @@ +// jest-dom adds custom jest matchers for asserting on DOM nodes. +// allows you to do things like: +// expect(element).toHaveTextContent(/react/i) +// learn more: https://github.com/testing-library/jest-dom +import '@testing-library/jest-dom'; diff --git a/packages/web/src/styles/theme.js b/packages/web/src/styles/theme.js new file mode 100644 index 0000000000000000000000000000000000000000..bf60c1d4b690ffc647a3c8bb7b80841ea28c5409 --- /dev/null +++ b/packages/web/src/styles/theme.js @@ -0,0 +1,304 @@ +import { deepmerge } from '@mui/utils'; +import { createTheme, alpha } from '@mui/material/styles'; +import { cardActionAreaClasses } from '@mui/material/CardActionArea'; +const referenceTheme = createTheme(); +export const primaryMainColor = '#0059F7'; +export const primaryLightColor = '#4286FF'; +export const primaryDarkColor = '#001F52'; +export const defaultTheme = createTheme({ + palette: { + primary: { + main: primaryMainColor, + light: primaryLightColor, + dark: primaryDarkColor, + contrastText: '#fff', + }, + divider: 'rgba(194, 194, 194, .2)', + common: { + black: '#1D1D1D', + white: '#fff', + }, + text: { + primary: '#001F52', + secondary: '#5C5C5C', + disabled: '#C2C2C2', + }, + error: { + main: '#F44336', + light: '#F88078', + dark: '#E31B0C', + contrastText: '#fff', + }, + success: { + main: '#4CAF50', + light: '#7BC67E', + dark: '#3B873E', + contrastText: '#fff', + }, + warning: { + main: '#ED6C02', + light: '#FFB547', + dark: '#C77700', + contrastText: 'rgba(0, 0, 0, 0.87)', + }, + secondary: { + main: '#F50057', + light: '#FF4081', + dark: '#C51162', + contrastText: '#fff', + }, + info: { + main: '#6B6F8D', + light: '#CED0E4', + dark: '#484B6C', + contrastText: '#fff', + }, + background: { + paper: '#fff', + default: '#FAFAFA', + }, + }, + shape: { + borderRadius: 4, + }, + typography: { + fontFamily: [ + 'Inter', + '"Segoe UI"', + 'Roboto', + '"Helvetica Neue"', + 'Arial', + 'sans-serif', + ].join(','), + h1: { + fontSize: referenceTheme.typography.pxToRem(72), + lineHeight: 1.11, + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(48), + }, + }, + h2: { + fontSize: referenceTheme.typography.pxToRem(48), + lineHeight: 1.16, + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(24), + }, + }, + h3: { + fontSize: referenceTheme.typography.pxToRem(32), + lineHeight: 1.16, + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(24), + }, + }, + h4: { + fontSize: referenceTheme.typography.pxToRem(32), + lineHeight: 1.3, + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(16), + }, + }, + h5: { + fontSize: referenceTheme.typography.pxToRem(24), + lineHeight: 1.3, + fontWeight: 400, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(16), + }, + }, + h6: { + fontSize: referenceTheme.typography.pxToRem(20), + lineHeight: 1.2, + fontWeight: 500, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(20), + }, + }, + subtitle1: { + fontSize: referenceTheme.typography.pxToRem(14), + lineHeight: 1.71, + fontWeight: 400, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(14), + }, + textTransform: 'uppercase', + }, + subtitle2: { + fontSize: referenceTheme.typography.pxToRem(14), + lineHeight: 1.14, + fontWeight: 400, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(14), + }, + }, + body1: { + fontSize: referenceTheme.typography.pxToRem(16), + lineHeight: 1.5, + fontWeight: 400, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(16), + }, + }, + body2: { + fontSize: referenceTheme.typography.pxToRem(16), + lineHeight: 1.5, + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(16), + }, + }, + button: { + fontSize: referenceTheme.typography.pxToRem(16), + fontWeight: 700, + [referenceTheme.breakpoints.down('sm')]: { + fontSize: referenceTheme.typography.pxToRem(16), + }, + }, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: ({ theme }) => ({ + background: theme.palette.primary.dark, + zIndex: theme.zIndex.drawer + 1, + }), + }, + defaultProps: { + elevation: 2, + }, + }, + MuiBackdrop: { + styleOverrides: { + root: { + background: 'rgba(0, 8, 20, 0.64)', + }, + invisible: { + background: 'transparent', + }, + }, + }, + MuiButton: { + styleOverrides: { + root: { + textTransform: 'none', + }, + sizeLarge: { + padding: '14px 22px', + }, + sizeMedium: { + padding: '10px 16px', + }, + sizeSmall: { + padding: '6px 10px', + }, + }, + }, + MuiCardActionArea: { + styleOverrides: { + root: ({ theme }) => ({ + borderRadius: theme.shape.borderRadius, + [`& .${cardActionAreaClasses.focusHighlight}`]: { + background: 'unset', + border: `1px solid ${alpha(theme.palette.primary.light, 1)}`, + }, + [`&:hover .${cardActionAreaClasses.focusHighlight}`]: { + opacity: 1, + }, + }), + }, + }, + MuiContainer: { + defaultProps: { + maxWidth: 'xl', + }, + }, + MuiCssBaseline: { + styleOverrides: { + html: { + scrollBehavior: 'smooth', + }, + a: { + textDecoration: 'none', + }, + '#root': { + minHeight: '100vh', + display: 'flex', + flexDirection: 'column', + }, + }, + }, + MuiDialog: { + styleOverrides: { + paperWidthSm: ({ theme }) => ({ + margin: theme.spacing(4, 3), + width: `calc(100% - ${theme.spacing(6)})`, + }), + }, + }, + MuiDialogContent: { + styleOverrides: { + root: ({ theme }) => ({ + '&&': { + paddingTop: theme.spacing(2), + }, + }), + }, + }, + MuiDialogTitle: { + styleOverrides: { + root: ({ theme }) => ({ + paddingTop: theme.spacing(3), + }), + }, + }, + MuiUseMediaQuery: { + defaultProps: { + noSsr: true, + }, + }, + MuiTab: { + styleOverrides: { + root: ({ theme }) => ({ + [theme.breakpoints.up('sm')]: { + padding: theme.spacing(1.5, 3), + }, + }), + }, + }, + MuiToolbar: { + styleOverrides: { + root: ({ theme }) => ({ + '@media all': { + paddingRight: theme.spacing(1.5), + }, + }), + }, + }, + }, +}); +export const mationTheme = createTheme( + deepmerge(defaultTheme, { + palette: { + primary: { + main: '#2962FF', + light: '#448AFF', + dark: '#2962FF', + contrastText: '#fff', + }, + }, + components: { + MuiAppBar: { + styleOverrides: { + root: ({ theme }) => ({ + zIndex: theme.zIndex.drawer + 1, + }), + }, + }, + }, + }), +); +export default defaultTheme; diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000000000000000000000000000000000000..eaaa45c962d530d40ea3e29ce3c3c85c2c989586 --- /dev/null +++ b/render.yaml @@ -0,0 +1,113 @@ +services: + - type: web + name: automatisch-main + env: docker + dockerfilePath: ./docker/Dockerfile + dockerContext: . + repo: https://github.com/automatisch/automatisch + autoDeploy: false + envVars: + - key: HOST + fromService: + name: automatisch-main + type: web + envVarKey: RENDER_EXTERNAL_HOSTNAME + - key: POSTGRES_HOST + fromDatabase: + name: automatisch-database + property: host + - key: POSTGRES_PORT + fromDatabase: + name: automatisch-database + property: port + - key: POSTGRES_DATABASE + fromDatabase: + name: automatisch-database + property: database + - key: POSTGRES_USERNAME + fromDatabase: + name: automatisch-database + property: user + - key: POSTGRES_PASSWORD + fromDatabase: + name: automatisch-database + property: password + - key: REDIS_HOST + fromService: + type: redis + name: automatisch-redis + property: host + - key: REDIS_PORT + fromService: + type: redis + name: automatisch-redis + property: port + - fromGroup: common-env-vars + - type: worker + name: automatisch-worker + env: docker + dockerfilePath: ./docker/Dockerfile + dockerContext: . + repo: https://github.com/automatisch/automatisch + autoDeploy: false + envVars: + - key: WORKER + value: true + - key: HOST + fromService: + name: automatisch-main + type: web + envVarKey: RENDER_EXTERNAL_HOSTNAME + - key: POSTGRES_HOST + fromDatabase: + name: automatisch-database + property: host + - key: POSTGRES_PORT + fromDatabase: + name: automatisch-database + property: port + - key: POSTGRES_DATABASE + fromDatabase: + name: automatisch-database + property: database + - key: POSTGRES_USERNAME + fromDatabase: + name: automatisch-database + property: user + - key: POSTGRES_PASSWORD + fromDatabase: + name: automatisch-database + property: password + - key: REDIS_HOST + fromService: + type: redis + name: automatisch-redis + property: host + - key: REDIS_PORT + fromService: + type: redis + name: automatisch-redis + property: port + - fromGroup: common-env-vars + - type: redis + name: automatisch-redis + ipAllowList: [] # allow only internal connections + maxmemoryPolicy: noeviction +databases: + - name: automatisch-database + databaseName: automatisch +envVarGroups: + - name: common-env-vars + envVars: + - key: APP_ENV + value: production + - key: PROTOCOL + value: https + - key: PORT + value: 443 + - key: ENCRYPTION_KEY + generateValue: true + - key: WEBHOOK_SECRET_KEY + generateValue: true + - key: APP_SECRET_KEY + generateValue: true diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000000000000000000000000000000000000..b4f61f02505517c6705960dde3ef4e6e2be000e6 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,17334 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@algolia/autocomplete-core@1.7.1": + version "1.7.1" + resolved "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.7.1.tgz" + integrity sha512-eiZw+fxMzNQn01S8dA/hcCpoWCOCwcIIEUtHHdzN5TGB3IpzLbuhqFeTfh2OUhhgkE8Uo17+wH+QJ/wYyQmmzg== + dependencies: + "@algolia/autocomplete-shared" "1.7.1" + +"@algolia/autocomplete-preset-algolia@1.7.1": + version "1.7.1" + resolved "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.1.tgz" + integrity sha512-pJwmIxeJCymU1M6cGujnaIYcY3QPOVYZOXhFkWVM7IxKzy272BwCvMFMyc5NpG/QmiObBxjo7myd060OeTNJXg== + dependencies: + "@algolia/autocomplete-shared" "1.7.1" + +"@algolia/autocomplete-shared@1.7.1": + version "1.7.1" + resolved "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.1.tgz" + integrity sha512-eTmGVqY3GeyBTT8IWiB2K5EuURAqhnumfktAEoHxfDY2o7vg2rSnO16ZtIG0fMgt3py28Vwgq42/bVEuaQV7pg== + +"@algolia/cache-browser-local-storage@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.12.0.tgz" + integrity sha512-l+G560B6N1k0rIcOjTO1yCzFUbg2Zy2HCii9s03e13jGgqduVQmk79UUCYszjsJ5GPJpUEKcVEtAIpP7tjsXVA== + dependencies: + "@algolia/cache-common" "4.12.0" + +"@algolia/cache-common@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.12.0.tgz" + integrity sha512-2Z8BV+NX7oN7RmmQbLqmW8lfN9aAjOexX1FJjzB0YfKC9ifpi9Jl4nSxlnbU+iLR6QhHo0IfuyQ7wcnucCGCGQ== + +"@algolia/cache-in-memory@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.12.0.tgz" + integrity sha512-b6ANkZF6vGAo+sYv6g25W5a0u3o6F549gEAgtTDTVA1aHcdWwe/HG/dTJ7NsnHbuR+A831tIwnNYQjRp3/V/Jw== + dependencies: + "@algolia/cache-common" "4.12.0" + +"@algolia/client-account@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.12.0.tgz" + integrity sha512-gzXN75ZydNheNXUN3epS+aLsKnB/PHFVlGUUjXL8WHs4lJP3B5FtHvaA/NCN5DsM3aamhuY5p0ff1XIA+Lbcrw== + dependencies: + "@algolia/client-common" "4.12.0" + "@algolia/client-search" "4.12.0" + "@algolia/transporter" "4.12.0" + +"@algolia/client-analytics@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.12.0.tgz" + integrity sha512-rO2cZCt00Opk66QBZb7IBGfCq4ZE3EiuGkXssf2Monb5urujy0r8CknK2i7bzaKtPbd2vlvhmLP4CEHQqF6SLQ== + dependencies: + "@algolia/client-common" "4.12.0" + "@algolia/client-search" "4.12.0" + "@algolia/requester-common" "4.12.0" + "@algolia/transporter" "4.12.0" + +"@algolia/client-common@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.12.0.tgz" + integrity sha512-fcrFN7FBmxiSyjeu3sF4OnPkC1l7/8oyQ8RMM8CHpVY8cad6/ay35MrfRfgfqdzdFA8LzcBYO7fykuJv0eOqxw== + dependencies: + "@algolia/requester-common" "4.12.0" + "@algolia/transporter" "4.12.0" + +"@algolia/client-personalization@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.12.0.tgz" + integrity sha512-wCJfSQEmX6ZOuJBJGjy+sbXiW0iy7tMNAhsVMV9RRaJE4727e5WAqwFWZssD877WQ74+/nF/VyTaB1+wejo33Q== + dependencies: + "@algolia/client-common" "4.12.0" + "@algolia/requester-common" "4.12.0" + "@algolia/transporter" "4.12.0" + +"@algolia/client-search@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.12.0.tgz" + integrity sha512-ik6dswcTQtOdZN+8aKntI9X2E6Qpqjtyda/+VANiHThY9GD2PBXuNuuC2HvlF26AbBYp5xaSE/EKxn1DIiIJ4Q== + dependencies: + "@algolia/client-common" "4.12.0" + "@algolia/requester-common" "4.12.0" + "@algolia/transporter" "4.12.0" + +"@algolia/logger-common@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.12.0.tgz" + integrity sha512-V//9rzLdJujA3iZ/tPhmKR/m2kjSZrymxOfUiF3024u2/7UyOpH92OOCrHUf023uMGYHRzyhBz5ESfL1oCdh7g== + +"@algolia/logger-console@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.12.0.tgz" + integrity sha512-pHvoGv53KXRIJHLk9uxBwKirwEo12G9+uo0sJLWESThAN3v5M+ycliU1AkUXQN8+9rds2KxfULAb+vfyfBKf8A== + dependencies: + "@algolia/logger-common" "4.12.0" + +"@algolia/requester-browser-xhr@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.12.0.tgz" + integrity sha512-rGlHNMM3jIZBwSpz33CVkeXHilzuzHuFXEEW1icP/k3KW7kwBrKFJwBy42RzAJa5BYlLsTCFTS3xkPhYwTQKLg== + dependencies: + "@algolia/requester-common" "4.12.0" + +"@algolia/requester-common@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.12.0.tgz" + integrity sha512-qgfdc73nXqpVyOMr6CMTx3nXvud9dP6GcMGDqPct+fnxogGcJsp24cY2nMqUrAfgmTJe9Nmy7Lddv0FyHjONMg== + +"@algolia/requester-node-http@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.12.0.tgz" + integrity sha512-mOTRGf/v/dXshBoZKNhMG00ZGxoUH9QdSpuMKYnuWwIgstN24uj3DQx+Ho3c+uq0TYfq7n2v71uoJWuiW32NMQ== + dependencies: + "@algolia/requester-common" "4.12.0" + +"@algolia/transporter@4.12.0": + version "4.12.0" + resolved "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.12.0.tgz" + integrity sha512-MOQVHZ4BcBpf3LtOY/3fqXHAcvI8MahrXDHk9QrBE/iGensQhDiZby5Dn3o2JN/zd9FMnVbdPQ8gnkiMwZiakQ== + dependencies: + "@algolia/cache-common" "4.12.0" + "@algolia/logger-common" "4.12.0" + "@algolia/requester-common" "4.12.0" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.2" + resolved "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.2.tgz" + integrity sha512-JdEazx7qiVqTBzzBl5rolRwl5cmhihjfIcpqRzIZjtT6b18liVmDn/VlWpqW4C/qP2hrFFMLRV1wlex8ZVBPTg== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@apollo/client@^3.6.9": + version "3.6.9" + resolved "https://registry.npmjs.org/@apollo/client/-/client-3.6.9.tgz" + integrity sha512-Y1yu8qa2YeaCUBVuw08x8NHenFi0sw2I3KCu7Kw9mDSu86HmmtHJkCAifKVrN2iPgDTW/BbP3EpSV8/EQCcxZA== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + "@wry/context" "^0.6.0" + "@wry/equality" "^0.5.0" + "@wry/trie" "^0.3.0" + graphql-tag "^2.12.6" + hoist-non-react-statics "^3.3.2" + optimism "^0.16.1" + prop-types "^15.7.2" + symbol-observable "^4.0.0" + ts-invariant "^0.10.3" + tslib "^2.3.0" + zen-observable-ts "^1.2.5" + +"@apollo/client@~3.2.5 || ~3.3.0 || ~3.4.0": + version "3.4.17" + resolved "https://registry.npmjs.org/@apollo/client/-/client-3.4.17.tgz" + integrity sha512-MDt2rwMX1GqodiVEKJqmDmAz8xr0qJmq5PdWeIt0yDaT4GOkKYWZiWkyfhfv3raTk8PyJvbsNG9q2CqmUrlGfg== + dependencies: + "@graphql-typed-document-node/core" "^3.0.0" + "@wry/context" "^0.6.0" + "@wry/equality" "^0.5.0" + "@wry/trie" "^0.3.0" + graphql-tag "^2.12.3" + hoist-non-react-statics "^3.3.2" + optimism "^0.16.1" + prop-types "^15.7.2" + symbol-observable "^4.0.0" + ts-invariant "^0.9.0" + tslib "^2.3.0" + zen-observable-ts "~1.1.0" + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.16.0", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.8.3": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/compat-data@^7.13.11", "@babel/compat-data@^7.16.4", "@babel/compat-data@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.16.8.tgz" + integrity sha512-m7OkX0IdKLKPpBlJtF561YJal5y/jyI5fNfWbPxh2D/nbzzGI4qRyrD8xO2jB24u7l+5I2a43scCG2IrfjC50Q== + +"@babel/core@^7.1.0", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.8.0": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.16.7.tgz" + integrity sha512-aeLaqcqThRNZYmbMqtulsetOQZ/5gbR/dWruUCJcpas4Qoyy+QeagfDsPdMrqwsPRDNxJvBlRiZxxX7THO7qtA== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.7" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/core@^7.11.1", "@babel/core@^7.7.2": + version "7.16.12" + resolved "https://registry.npmjs.org/@babel/core/-/core-7.16.12.tgz" + integrity sha512-dK5PtG1uiN2ikk++5OzSYsitZKny4wOCD0nrO4TqnW4BVBTQ2NGS3NgilvT/TEyxTST7LNyWV/T4tXDoD3fOgg== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helpers" "^7.16.7" + "@babel/parser" "^7.16.12" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.10" + "@babel/types" "^7.16.8" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.1.2" + semver "^6.3.0" + source-map "^0.5.0" + +"@babel/eslint-parser@^7.16.3": + version "7.16.5" + resolved "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.16.5.tgz" + integrity sha512-mUqYa46lgWqHKQ33Q6LNCGp/wPR3eqOYTUixHFsfrSQqRxH0+WOzca75iEjFr5RDGH1dDz622LaHhLOzOuQRUA== + dependencies: + eslint-scope "^5.1.1" + eslint-visitor-keys "^2.1.0" + semver "^6.3.0" + +"@babel/generator@^7.16.7", "@babel/generator@^7.16.8", "@babel/generator@^7.7.2": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.16.8.tgz" + integrity sha512-1ojZwE9+lOXzcWdWmO6TbUzDfqLD39CmEhN8+2cX9XkDo5yW1OpgfejfliysR2AWLpMamTiOiAp/mtroaymhpw== + dependencies: + "@babel/types" "^7.16.8" + jsesc "^2.5.1" + source-map "^0.5.0" + +"@babel/helper-annotate-as-pure@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.7.tgz" + integrity sha512-s6t2w/IPQVTAET1HitoowRGXooX8mCgtuP5195wD/QJPV6wYjpujCGF7JuMODVX2ZAJOf1GT6DT9MHEZvLOFSw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.16.7.tgz" + integrity sha512-C6FdbRaxYjwVu/geKW4ZeQ0Q31AftgRcdSnZ5/jsH6BzCJbtvXvhpfkbkThYSuutZA7nCXpPR6AD9zd1dprMkA== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-compilation-targets@^7.13.0", "@babel/helper-compilation-targets@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.16.7.tgz" + integrity sha512-mGojBwIWcwGD6rfqgRXVlVYmPAv7eOpIemUG3dGnDdCY4Pae70ROij3XmfrH6Fa1h1aiDylpglbZyktfzyo/hA== + dependencies: + "@babel/compat-data" "^7.16.4" + "@babel/helper-validator-option" "^7.16.7" + browserslist "^4.17.5" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.16.10": + version "7.16.10" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.10.tgz" + integrity sha512-wDeej0pu3WN/ffTxMNCPW5UCiOav8IcLRxSIyp/9+IF2xJUM9h/OYjg0IJLHaL6F8oU8kqMz9nc1vryXhMsgXg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-class-features-plugin@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.16.7.tgz" + integrity sha512-kIFozAvVfK05DM4EVQYKK+zteWvY85BFdGBRQBytRyY3y+6PX0DkDOn/CZ3lEuczCfrCxEzwt0YtP/87YPTWSw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + +"@babel/helper-create-regexp-features-plugin@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.16.7.tgz" + integrity sha512-fk5A6ymfp+O5+p2yCkXAu5Kyj6v0xh0RBeNcAkYUMDvvAAoxvSKXn+Jb37t/yWFiQVDFK1ELpUTD8/aLhCPu+g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + regexpu-core "^4.7.1" + +"@babel/helper-define-polyfill-provider@^0.3.1": + version "0.3.1" + resolved "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.1.tgz" + integrity sha512-J9hGMpJQmtWmj46B3kBHmL38UhJGhYX7eqkcq+2gsstyYt341HmPeWspihX43yVRA0mS+8GGk2Gckc7bY/HCmA== + dependencies: + "@babel/helper-compilation-targets" "^7.13.0" + "@babel/helper-module-imports" "^7.12.13" + "@babel/helper-plugin-utils" "^7.13.0" + "@babel/traverse" "^7.13.0" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz" + integrity sha512-SLLb0AAn6PkUeAfKJCCOl9e1R53pQlGAfc4y4XuMRZfqeMYLE0dM1LMhqbGAlGQY0lfw5/ohoYWAe9V1yibRag== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-explode-assignable-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.16.7.tgz" + integrity sha512-KyUenhWMC8VrxzkGP0Jizjo4/Zx+1nNZhgocs+gLzyZyB8SHidhoq9KK/8Ato4anhwsivfkBLftky7gvzbZMtQ== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz" + integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA== + dependencies: + "@babel/helper-get-function-arity" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-get-function-arity@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz" + integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-hoist-variables@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz" + integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-member-expression-to-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.16.7.tgz" + integrity sha512-VtJ/65tYiU/6AbMTDwyoXGPKHgTsfRarivm+YbB5uAzKUyuPjgZSgAFeG87FCigc7KNHu2Pegh1XIT3lXjvz3Q== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz" + integrity sha512-LVtS6TqjJHFc+nYeITRo6VLXve70xmq7wPhWTqDJusJEgGmkAACWwMiTNrvfoQo6hEhFwAIixNkvB0jPXDL8Wg== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-module-transforms@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.16.7.tgz" + integrity sha512-gaqtLDxJEFCeQbYp9aLAefjhkKdjKcdh6DB7jniIGU3Pz52WAmP268zK0VgPz9hUNkMSYeH976K2/Y6yPadpng== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-optimise-call-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.16.7.tgz" + integrity sha512-EtgBhg7rd/JcnpZFXpBy0ze1YRfdm7BnBX4uKMBd3ixa3RGAE002JZB66FJyNH7g0F38U05pXmA5P8cBh7z+1w== + dependencies: + "@babel/types" "^7.16.7" + +"@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.13.0", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.16.7.tgz" + integrity sha512-Qg3Nk7ZxpgMrsox6HreY1ZNKdBq7K72tDSliA6dCl5f007jR4ne8iD5UzuNnCJH2xBf2BEEVGr+/OL6Gdp7RxA== + +"@babel/helper-remap-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.16.8.tgz" + integrity sha512-fm0gH7Flb8H51LqJHy3HJ3wnE1+qtYR2A99K06ahwrawLdOFsCEWjZOrYricXJHoPSudNKxrMBUPEIPxiIIvBw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-wrap-function" "^7.16.8" + "@babel/types" "^7.16.8" + +"@babel/helper-replace-supers@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.16.7.tgz" + integrity sha512-y9vsWilTNaVnVh6xiJfABzsNpgDPKev9HnAgz6Gb1p6UUwf9NepdlsV7VXGCftJM+jqD5f7JIEubcpLjZj5dBw== + dependencies: + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-member-expression-to-functions" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/helper-simple-access@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.16.7.tgz" + integrity sha512-ZIzHVyoeLMvXMN/vok/a4LWRy8G2v205mNP0XOuf9XRLyX5/u9CnVulUtDgUTama3lT+bf/UqucuZjqiGuTS1g== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-skip-transparent-expression-wrappers@^7.16.0": + version "7.16.0" + resolved "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.16.0.tgz" + integrity sha512-+il1gTy0oHwUsBQZyJvukbB4vPMdcYBrFHa0Uc4AizLxbq6BOYC51Rv4tWocX9BLBDLZ4kc6qUFpQ6HRgL+3zw== + dependencies: + "@babel/types" "^7.16.0" + +"@babel/helper-split-export-declaration@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.16.7.tgz" + integrity sha512-xbWoy/PFoxSWazIToT9Sif+jJTlrMcndIsaOKvTA6u7QEo7ilkRZpjew18/W3c7nm8fXdUDXh02VXTbZ0pGDNw== + dependencies: + "@babel/types" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/helper-validator-option@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.16.7.tgz" + integrity sha512-TRtenOuRUVo9oIQGPC5G9DgK4743cdxvtOw0weQNpZXaS16SCBi5MNjZF8vba3ETURjZpTbVn7Vvcf2eAwFozQ== + +"@babel/helper-wrap-function@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.16.8.tgz" + integrity sha512-8RpyRVIAW1RcDDGTA+GpPAwV22wXCfKOoM9bet6TLkGIFTkRQSkH1nMQ5Yet4MpoXe1ZwHPVtNasc2w0uZMqnw== + dependencies: + "@babel/helper-function-name" "^7.16.7" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.8" + "@babel/types" "^7.16.8" + +"@babel/helpers@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.16.7.tgz" + integrity sha512-9ZDoqtfY7AuEOt3cxchfii6C7GDyyMBffktR5B2jvWv8u2+efwvpnVKXMWzNehqy68tKgAfSwfdw/lWpthS2bw== + dependencies: + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/highlight@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.7.tgz" + integrity sha512-aKpPMfLvGO3Q97V0qhw/V2SWNWlwfJknuwAunU7wZLSfrM4xTBvg7E5opUVi1kJTBKihE38CPg4nBiqX83PWYw== + dependencies: + "@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.7", "@babel/parser@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.16.8.tgz" + integrity sha512-i7jDUfrVBWc+7OKcBzEe5n7fbv3i2fWtxKzzCvOjnzSxMfWMigAhtfJ7qzZNGFNMsCCd67+uz553dYKWXPvCKw== + +"@babel/parser@^7.16.10", "@babel/parser@^7.16.12": + version "7.16.12" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.16.12.tgz" + integrity sha512-VfaV15po8RiZssrkPweyvbGVSe4x2y+aciFCgn0n0/SJMR22cwofRV1mtnJQYcSB1wUTaA/X1LnA3es66MCO5A== + +"@babel/parser@^7.16.4": + version "7.18.11" + resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.18.11.tgz" + integrity sha512-9JKn5vN+hDt0Hdqn1PiJ2guflwP+B6Ga8qbDuoF0PzzVhrzsKIJo8yGqVk6CmMHiMei9w1C1Bp9IMJSIK+HPIQ== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.16.7.tgz" + integrity sha512-anv/DObl7waiGEnC24O9zqL0pSuI9hljihqiDuFHC8d7/bjr/4RLGPWuc8rYOff/QPzbEPSkzG8wGG9aDuhHRg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.16.7.tgz" + integrity sha512-di8vUHRdf+4aJ7ltXhaDbPoszdkh59AQtJM5soLsuHpQJdFQZOA4uGj0V2u/CZ8bJ/u8ULDL5yq6FO/bCXnKHw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + +"@babel/plugin-proposal-async-generator-functions@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.16.8.tgz" + integrity sha512-71YHIvMuiuqWJQkebWJtdhQTfd4Q4mF76q2IX37uZPkG9+olBxsX+rH1vkhFto4UeJZ9dPY2s+mDvhDm1u2BGQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.16.0", "@babel/plugin-proposal-class-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.16.7.tgz" + integrity sha512-IobU0Xme31ewjYOShSIqd/ZGM/r/cuOz2z0MDbNrhF5FW+ZVgi0f2lyeoj9KFPDOAqsYxmLWZte1WOwlvY9aww== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-proposal-class-static-block@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.16.7.tgz" + integrity sha512-dgqJJrcZoG/4CkMopzhPJjGxsIe9A8RlkQLnL/Vhhx8AA9ZuaRwGSlscSh42hazc7WSrya/IK7mTeoF0DP9tEw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-decorators@^7.16.4": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.16.7.tgz" + integrity sha512-DoEpnuXK14XV9btI1k8tzNGCutMclpj4yru8aXKoHlVmbO1s+2A+g2+h4JhcjrxkFJqzbymnLG6j/niOf3iFXQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-decorators" "^7.16.7" + +"@babel/plugin-proposal-dynamic-import@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.16.7.tgz" + integrity sha512-I8SW9Ho3/8DRSdmDdH3gORdyUuYnk1m4cMxUAdu5oy4n3OfN8flDEH+d60iG7dUfi0KkYwSvoalHzzdRzpWHTg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.16.7.tgz" + integrity sha512-ZxdtqDXLRGBL64ocZcs7ovt71L3jhC1RGSyR996svrCi3PYqHNkb3SwPJCs8RIzD86s+WPpt2S73+EHCGO+NUA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.16.7.tgz" + integrity sha512-lNZ3EEggsGY78JavgbHsK9u5P3pQaW7k4axlgFLYkMd7UBsiNahCITShLjNQschPyjtO6dADrL24757IdhBrsQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.16.7.tgz" + integrity sha512-K3XzyZJGQCr00+EtYtrDjmwX7o7PLK6U9bi1nCwkQioRFVUv6dJoxbQjtWVtP+bCPy82bONBKG8NPyQ4+i6yjg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.16.0", "@babel/plugin-proposal-nullish-coalescing-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.16.7.tgz" + integrity sha512-aUOrYU3EVtjf62jQrCj63pYZ7k6vns2h/DQvHPWGmsJRYzWXZ6/AsfgpiRy6XiuIDADhJzP2Q9MwSMKauBQ+UQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.16.0", "@babel/plugin-proposal-numeric-separator@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.16.7.tgz" + integrity sha512-vQgPMknOIgiuVqbokToyXbkY/OmmjAzr/0lhSIbG/KmnzXPGwW/AdhdKpi+O4X/VkWiWjnkKOBiqJrTaC98VKw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.16.7.tgz" + integrity sha512-3O0Y4+dw94HA86qSg9IHfyPktgR7q3gpNVAeiKQd+8jBKFaU5NQS1Yatgo4wY+UFNuLjvxcSmzcsHqrhgTyBUA== + dependencies: + "@babel/compat-data" "^7.16.4" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.16.7" + +"@babel/plugin-proposal-optional-catch-binding@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.16.7.tgz" + integrity sha512-eMOH/L4OvWSZAE1VkHbr1vckLG1WUcHGJSLqqQwl2GaUqG6QjddvrOaTUMNYiv77H5IKPMZ9U9P7EaHwvAShfA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.16.0", "@babel/plugin-proposal-optional-chaining@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.16.7.tgz" + integrity sha512-eC3xy+ZrUcBtP7x+sq62Q/HYd674pPTb/77XZMb5wbDPGWIdUbSr4Agr052+zaUPSb+gGRnjxXfKFvx5iMJ+DA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.16.0", "@babel/plugin-proposal-private-methods@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.7.tgz" + integrity sha512-7twV3pzhrRxSwHeIvFE6coPgvo+exNDOiGUMg39o2LiLo1Y+4aKpfkcLGcg1UHonzorCt7SNXnoMyCnnIOA8Sw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-proposal-private-methods@^7.16.11": + version "7.16.11" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.16.11.tgz" + integrity sha512-F/2uAkPlXDr8+BHpZvo19w3hLFKge+k75XUprE6jaqKxjGkSYcK+4c+bup5PdW/7W/Rpjwql7FTVEDW+fRAQsw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.10" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-proposal-private-property-in-object@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.16.7.tgz" + integrity sha512-rMQkjcOFbm+ufe3bTZLyOfsOUOxyvLXZJCTARhJr+8UMSoZmqTe1K1BgkFcrW37rAchWg57yI69ORxiWvUINuQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.16.7", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.16.7.tgz" + integrity sha512-QRK0YI/40VLhNVGIjRNAAQkEHws0cswSdFFjpFyt943YmJIU1da9uW63Iu6NFV6CxTZW5eTDCrwZUstBWgp/Rg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-bigint@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz" + integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13", "@babel/plugin-syntax-class-properties@^7.8.3": + version "7.12.13" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-decorators@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.16.7.tgz" + integrity sha512-vQ+PxL+srA7g6Rx6I1e15m55gftknl2X8GCUW1JTlkTaXZLJOS0UcaY0eK9jYT7IYf4awn6qwyghVHLDz1WyMw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-flow@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.16.7.tgz" + integrity sha512-UDo3YGQO0jH6ytzVwgSLv9i/CzMcUjbKenL67dTrAZPPv6GFAtDhe6jqnvmoKzC/7htNTohhos+onPtDMqJwaQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-import-meta@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz" + integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.12.13", "@babel/plugin-syntax-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.16.7.tgz" + integrity sha512-Esxmk7YjA8QysKeT3VhTXvF6y77f/a91SIs4pWb4H2eWGQkCKFgQaG6hdoEVZtGsrAcb2K5BW66XsOErD4WU3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4", "@babel/plugin-syntax-logical-assignment-operators@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4", "@babel/plugin-syntax-numeric-separator@^7.8.3": + version "7.10.4" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5", "@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.14.5" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-typescript@^7.16.7", "@babel/plugin-syntax-typescript@^7.7.2": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.16.7.tgz" + integrity sha512-YhUIJHHGkqPgEcMYkPCKTyGUdoGKWtopIycQyjJH8OjvRgOYsXsaKehLVPScKJWAULPxMa4N1vCe6szREFlZ7A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-arrow-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.16.7.tgz" + integrity sha512-9ffkFFMbvzTvv+7dTp/66xvZAWASuPD5Tl9LK3Z9vhOmANo6j94rik+5YMBt4CwHVMWLWpMsriIc2zsa3WW3xQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-async-to-generator@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.16.8.tgz" + integrity sha512-MtmUmTJQHCnyJVrScNzNlofQJ3dLFuobYn3mwOTKHnSCMtbNsqvF71GQmJfFjdrXSsAA7iysFmYWw4bXZ20hOg== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-remap-async-to-generator" "^7.16.8" + +"@babel/plugin-transform-block-scoped-functions@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.16.7.tgz" + integrity sha512-JUuzlzmF40Z9cXyytcbZEZKckgrQzChbQJw/5PuEHYeqzCsvebDx0K0jWnIIVcmmDOAVctCgnYs0pMcrYj2zJg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-block-scoping@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.16.7.tgz" + integrity sha512-ObZev2nxVAYA4bhyusELdo9hb3H+A56bxH3FZMbEImZFiEDYVHXQSJ1hQKFlDnlt8G9bBrCZ5ZpURZUrV4G5qQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-classes@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.16.7.tgz" + integrity sha512-WY7og38SFAGYRe64BrjKf8OrE6ulEHtr5jEYaZMwox9KebgqPi67Zqz8K53EKk1fFEJgm96r32rkKZ3qA2nCWQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-optimise-call-expression" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.16.7.tgz" + integrity sha512-gN72G9bcmenVILj//sv1zLNaPyYcOzUho2lIJBMh/iakJ9ygCo/hEF9cpGb61SCMEDxbbyBoVQxrt+bWKu5KGw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-destructuring@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.16.7.tgz" + integrity sha512-VqAwhTHBnu5xBVDCvrvqJbtLUa++qZaWC0Fgr2mqokBlulZARGyIvZDoqbPlPaKImQ9dKAcCzbv+ul//uqu70A== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-dotall-regex@^7.16.7", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.16.7.tgz" + integrity sha512-Lyttaao2SjZF6Pf4vk1dVKv8YypMpomAbygW+mU5cYP3S5cWTfCJjG8xV6CFdzGFlfWK81IjL9viiTvpb6G7gQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-duplicate-keys@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.16.7.tgz" + integrity sha512-03DvpbRfvWIXyK0/6QiR1KMTWeT6OcQ7tbhjrXyFS02kjuX/mu5Bvnh5SDSWHxyawit2g5aWhKwI86EE7GUnTw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-exponentiation-operator@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.16.7.tgz" + integrity sha512-8UYLSlyLgRixQvlYH3J2ekXFHDFLQutdy7FfFAMm3CPZ6q9wHCwnUyiXpQCe3gVVnQlHc5nsuiEVziteRNTXEA== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-flow-strip-types@^7.16.0": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.16.7.tgz" + integrity sha512-mzmCq3cNsDpZZu9FADYYyfZJIOrSONmHcop2XEKPdBNMa4PDC4eEvcOvzZaCNcjKu72v0XQlA5y1g58aLRXdYg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-flow" "^7.16.7" + +"@babel/plugin-transform-for-of@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.16.7.tgz" + integrity sha512-/QZm9W92Ptpw7sjI9Nx1mbcsWz33+l8kuMIQnDwgQBG5s3fAfQvkRjQ7NqXhtNcKOnPkdICmUHyCaWW06HCsqg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-function-name@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.16.7.tgz" + integrity sha512-SU/C68YVwTRxqWj5kgsbKINakGag0KTgq9f2iZEXdStoAbOzLHEBRYzImmA6yFo8YZhJVflvXmIHUO7GWHmxxA== + dependencies: + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.16.7.tgz" + integrity sha512-6tH8RTpTWI0s2sV6uq3e/C9wPo4PTqqZps4uF0kzQ9/xPLFQtipynvmT1g/dOfEJ+0EQsHhkQ/zyRId8J2b8zQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-member-expression-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.16.7.tgz" + integrity sha512-mBruRMbktKQwbxaJof32LT9KLy2f3gH+27a5XSuXo6h7R3vqltl0PgZ80C8ZMKw98Bf8bqt6BEVi3svOh2PzMw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-modules-amd@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.16.7.tgz" + integrity sha512-KaaEtgBL7FKYwjJ/teH63oAmE3lP34N3kshz8mm4VMAw7U3PxjVwwUmxEFksbgsNUaO3wId9R2AVQYSEGRa2+g== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-commonjs@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.16.8.tgz" + integrity sha512-oflKPvsLT2+uKQopesJt3ApiaIS2HW+hzHFcwRNtyDGieAeC/dIHZX8buJQ2J2X1rxGPy4eRcUijm3qcSPjYcA== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-simple-access" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-systemjs@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.16.7.tgz" + integrity sha512-DuK5E3k+QQmnOqBR9UkusByy5WZWGRxfzV529s9nPra1GE7olmxfqO2FHobEOYSPIjPBTr4p66YDcjQnt8cBmw== + dependencies: + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-identifier" "^7.16.7" + babel-plugin-dynamic-import-node "^2.3.3" + +"@babel/plugin-transform-modules-umd@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.16.7.tgz" + integrity sha512-EMh7uolsC8O4xhudF2F6wedbSHm1HHZ0C6aJ7K67zcDNidMzVcxWdGr+htW9n21klm+bOn+Rx4CBsAntZd3rEQ== + dependencies: + "@babel/helper-module-transforms" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.16.8.tgz" + integrity sha512-j3Jw+n5PvpmhRR+mrgIh04puSANCk/T/UA3m3P1MjJkhlK906+ApHhDIqBQDdOgL/r1UYpz4GNclTXxyZrYGSw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + +"@babel/plugin-transform-new-target@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.16.7.tgz" + integrity sha512-xiLDzWNMfKoGOpc6t3U+etCE2yRnn3SM09BXqWPIZOBpL2gvVrBWUKnsJx0K/ADi5F5YC5f8APFfWrz25TdlGg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-object-super@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.16.7.tgz" + integrity sha512-14J1feiQVWaGvRxj2WjyMuXS2jsBkgB3MdSN5HuC2G5nRspa5RK9COcs82Pwy5BuGcjb+fYaUj94mYcOj7rCvw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-replace-supers" "^7.16.7" + +"@babel/plugin-transform-parameters@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.16.7.tgz" + integrity sha512-AT3MufQ7zZEhU2hwOA11axBnExW0Lszu4RL/tAlUJBuNoRak+wehQW8h6KcXOcgjY42fHtDxswuMhMjFEuv/aw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-property-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.16.7.tgz" + integrity sha512-z4FGr9NMGdoIl1RqavCqGG+ZuYjfZ/hkCIeuH6Do7tXmSm0ls11nYVSJqFEUOSJbDab5wC6lRE/w6YjVcr6Hqw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-constant-elements@^7.12.1": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.16.7.tgz" + integrity sha512-lF+cfsyTgwWkcw715J88JhMYJ5GpysYNLhLP1PkvkhTRN7B3e74R/1KsDxFxhRpSn0UUD3IWM4GvdBR2PEbbQQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-display-name@^7.16.0", "@babel/plugin-transform-react-display-name@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.16.7.tgz" + integrity sha512-qgIg8BcZgd0G/Cz916D5+9kqX0c7nPZyXaP8R2tLNN5tkyIZdG5fEwBrxwplzSnjC1jvQmyMNVwUCZPcbGY7Pg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-react-jsx-development@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.16.7.tgz" + integrity sha512-RMvQWvpla+xy6MlBpPlrKZCMRs2AGiHOGHY3xRwl0pEeim348dDyxeH4xBsMPbIMhujeq7ihE702eM2Ew0Wo+A== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.16.7" + +"@babel/plugin-transform-react-jsx@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.16.7.tgz" + integrity sha512-8D16ye66fxiE8m890w0BpPpngG9o9OVBBy0gH2E+2AR7qMR2ZpTYJEqLxAsoroenMId0p/wMW+Blc0meDgu0Ag== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-jsx" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/plugin-transform-react-pure-annotations@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.16.7.tgz" + integrity sha512-hs71ToC97k3QWxswh2ElzMFABXHvGiJ01IB1TbYQDGeWRKWz/MPUTh5jGExdHvosYKpnJW5Pm3S4+TA3FyX+GA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-regenerator@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.16.7.tgz" + integrity sha512-mF7jOgGYCkSJagJ6XCujSQg+6xC1M77/03K2oBmVJWoFGNUtnVJO4WHKJk3dnPC8HCcj4xBQP1Egm8DWh3Pb3Q== + dependencies: + regenerator-transform "^0.14.2" + +"@babel/plugin-transform-reserved-words@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.16.7.tgz" + integrity sha512-KQzzDnZ9hWQBjwi5lpY5v9shmm6IVG0U9pB18zvMu2i4H90xpT4gmqwPYsn8rObiadYe2M0gmgsiOIF5A/2rtg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-runtime@^7.16.4": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.16.8.tgz" + integrity sha512-6Kg2XHPFnIarNweZxmzbgYnnWsXxkx9WQUVk2sksBRL80lBC1RAQV3wQagWxdCHiYHqPN+oenwNIuttlYgIbQQ== + dependencies: + "@babel/helper-module-imports" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + semver "^6.3.0" + +"@babel/plugin-transform-shorthand-properties@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz" + integrity sha512-hah2+FEnoRoATdIb05IOXf+4GzXYTq75TVhIn1PewihbpyrNWUt2JbudKQOETWw6QpLe+AIUpJ5MVLYTQbeeUg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-spread@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.16.7.tgz" + integrity sha512-+pjJpgAngb53L0iaA5gU/1MLXJIfXcYepLgXB3esVRf4fqmj8f2cxM3/FKaHsZms08hFQJkFccEWuIpm429TXg== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-skip-transparent-expression-wrappers" "^7.16.0" + +"@babel/plugin-transform-sticky-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.16.7.tgz" + integrity sha512-NJa0Bd/87QV5NZZzTuZG5BPJjLYadeSZ9fO6oOUoL4iQx+9EEuw/eEM92SrsT19Yc2jgB1u1hsjqDtH02c3Drw== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-template-literals@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.16.7.tgz" + integrity sha512-VwbkDDUeenlIjmfNeDX/V0aWrQH2QiVyJtwymVQSzItFDTpxfyJh3EVaQiS0rIN/CqbLGr0VcGmuwyTdZtdIsA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-typeof-symbol@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.16.7.tgz" + integrity sha512-p2rOixCKRJzpg9JB4gjnG4gjWkWa89ZoYUnl9snJ1cWIcTH/hvxZqfO+WjG6T8DRBpctEol5jw1O5rA8gkCokQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-typescript@^7.16.7": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.16.8.tgz" + integrity sha512-bHdQ9k7YpBDO2d0NVfkj51DpQcvwIzIusJ7mEUaMlbZq3Kt/U47j24inXZHQ5MDiYpCs+oZiwnXyKedE8+q7AQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/plugin-syntax-typescript" "^7.16.7" + +"@babel/plugin-transform-unicode-escapes@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.16.7.tgz" + integrity sha512-TAV5IGahIz3yZ9/Hfv35TV2xEm+kaBDaZQCn2S/hG9/CZ0DktxJv9eKfPc7yYCvOYR4JGx1h8C+jcSOvgaaI/Q== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/plugin-transform-unicode-regex@^7.16.7": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.16.7.tgz" + integrity sha512-oC5tYYKw56HO75KZVLQ+R/Nl3Hro9kf8iG0hXoaHP7tjAyCpvqBiSNe6vGrZni1Z6MggmUOC6A7VP7AVmw225Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + +"@babel/preset-env@^7.11.0": + version "7.16.11" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.11.tgz" + integrity sha512-qcmWG8R7ZW6WBRPZK//y+E3Cli151B20W1Rv7ln27vuPaXU/8TKms6jFdiJtF7UDTxcrb7mZd88tAeK9LjdT8g== + dependencies: + "@babel/compat-data" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-async-generator-functions" "^7.16.8" + "@babel/plugin-proposal-class-properties" "^7.16.7" + "@babel/plugin-proposal-class-static-block" "^7.16.7" + "@babel/plugin-proposal-dynamic-import" "^7.16.7" + "@babel/plugin-proposal-export-namespace-from" "^7.16.7" + "@babel/plugin-proposal-json-strings" "^7.16.7" + "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" + "@babel/plugin-proposal-numeric-separator" "^7.16.7" + "@babel/plugin-proposal-object-rest-spread" "^7.16.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-private-methods" "^7.16.11" + "@babel/plugin-proposal-private-property-in-object" "^7.16.7" + "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.16.7" + "@babel/plugin-transform-async-to-generator" "^7.16.8" + "@babel/plugin-transform-block-scoped-functions" "^7.16.7" + "@babel/plugin-transform-block-scoping" "^7.16.7" + "@babel/plugin-transform-classes" "^7.16.7" + "@babel/plugin-transform-computed-properties" "^7.16.7" + "@babel/plugin-transform-destructuring" "^7.16.7" + "@babel/plugin-transform-dotall-regex" "^7.16.7" + "@babel/plugin-transform-duplicate-keys" "^7.16.7" + "@babel/plugin-transform-exponentiation-operator" "^7.16.7" + "@babel/plugin-transform-for-of" "^7.16.7" + "@babel/plugin-transform-function-name" "^7.16.7" + "@babel/plugin-transform-literals" "^7.16.7" + "@babel/plugin-transform-member-expression-literals" "^7.16.7" + "@babel/plugin-transform-modules-amd" "^7.16.7" + "@babel/plugin-transform-modules-commonjs" "^7.16.8" + "@babel/plugin-transform-modules-systemjs" "^7.16.7" + "@babel/plugin-transform-modules-umd" "^7.16.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" + "@babel/plugin-transform-new-target" "^7.16.7" + "@babel/plugin-transform-object-super" "^7.16.7" + "@babel/plugin-transform-parameters" "^7.16.7" + "@babel/plugin-transform-property-literals" "^7.16.7" + "@babel/plugin-transform-regenerator" "^7.16.7" + "@babel/plugin-transform-reserved-words" "^7.16.7" + "@babel/plugin-transform-shorthand-properties" "^7.16.7" + "@babel/plugin-transform-spread" "^7.16.7" + "@babel/plugin-transform-sticky-regex" "^7.16.7" + "@babel/plugin-transform-template-literals" "^7.16.7" + "@babel/plugin-transform-typeof-symbol" "^7.16.7" + "@babel/plugin-transform-unicode-escapes" "^7.16.7" + "@babel/plugin-transform-unicode-regex" "^7.16.7" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.16.8" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + core-js-compat "^3.20.2" + semver "^6.3.0" + +"@babel/preset-env@^7.12.1", "@babel/preset-env@^7.16.4": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.16.8.tgz" + integrity sha512-9rNKgVCdwHb3z1IlbMyft6yIXIeP3xz6vWvGaLHrJThuEIqWfHb0DNBH9VuTgnDfdbUDhkmkvMZS/YMCtP7Elg== + dependencies: + "@babel/compat-data" "^7.16.8" + "@babel/helper-compilation-targets" "^7.16.7" + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.16.7" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-async-generator-functions" "^7.16.8" + "@babel/plugin-proposal-class-properties" "^7.16.7" + "@babel/plugin-proposal-class-static-block" "^7.16.7" + "@babel/plugin-proposal-dynamic-import" "^7.16.7" + "@babel/plugin-proposal-export-namespace-from" "^7.16.7" + "@babel/plugin-proposal-json-strings" "^7.16.7" + "@babel/plugin-proposal-logical-assignment-operators" "^7.16.7" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.7" + "@babel/plugin-proposal-numeric-separator" "^7.16.7" + "@babel/plugin-proposal-object-rest-spread" "^7.16.7" + "@babel/plugin-proposal-optional-catch-binding" "^7.16.7" + "@babel/plugin-proposal-optional-chaining" "^7.16.7" + "@babel/plugin-proposal-private-methods" "^7.16.7" + "@babel/plugin-proposal-private-property-in-object" "^7.16.7" + "@babel/plugin-proposal-unicode-property-regex" "^7.16.7" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.16.7" + "@babel/plugin-transform-async-to-generator" "^7.16.8" + "@babel/plugin-transform-block-scoped-functions" "^7.16.7" + "@babel/plugin-transform-block-scoping" "^7.16.7" + "@babel/plugin-transform-classes" "^7.16.7" + "@babel/plugin-transform-computed-properties" "^7.16.7" + "@babel/plugin-transform-destructuring" "^7.16.7" + "@babel/plugin-transform-dotall-regex" "^7.16.7" + "@babel/plugin-transform-duplicate-keys" "^7.16.7" + "@babel/plugin-transform-exponentiation-operator" "^7.16.7" + "@babel/plugin-transform-for-of" "^7.16.7" + "@babel/plugin-transform-function-name" "^7.16.7" + "@babel/plugin-transform-literals" "^7.16.7" + "@babel/plugin-transform-member-expression-literals" "^7.16.7" + "@babel/plugin-transform-modules-amd" "^7.16.7" + "@babel/plugin-transform-modules-commonjs" "^7.16.8" + "@babel/plugin-transform-modules-systemjs" "^7.16.7" + "@babel/plugin-transform-modules-umd" "^7.16.7" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.16.8" + "@babel/plugin-transform-new-target" "^7.16.7" + "@babel/plugin-transform-object-super" "^7.16.7" + "@babel/plugin-transform-parameters" "^7.16.7" + "@babel/plugin-transform-property-literals" "^7.16.7" + "@babel/plugin-transform-regenerator" "^7.16.7" + "@babel/plugin-transform-reserved-words" "^7.16.7" + "@babel/plugin-transform-shorthand-properties" "^7.16.7" + "@babel/plugin-transform-spread" "^7.16.7" + "@babel/plugin-transform-sticky-regex" "^7.16.7" + "@babel/plugin-transform-template-literals" "^7.16.7" + "@babel/plugin-transform-typeof-symbol" "^7.16.7" + "@babel/plugin-transform-unicode-escapes" "^7.16.7" + "@babel/plugin-transform-unicode-regex" "^7.16.7" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.16.8" + babel-plugin-polyfill-corejs2 "^0.3.0" + babel-plugin-polyfill-corejs3 "^0.5.0" + babel-plugin-polyfill-regenerator "^0.3.0" + core-js-compat "^3.20.2" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.12.5", "@babel/preset-react@^7.16.0": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.16.7.tgz" + integrity sha512-fWpyI8UM/HE6DfPBzD8LnhQ/OcH8AgTaqcqP2nGOXEUV+VKBR5JRN9hCk9ai+zQQ57vtm9oWeXguBCPNUjytgA== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-react-display-name" "^7.16.7" + "@babel/plugin-transform-react-jsx" "^7.16.7" + "@babel/plugin-transform-react-jsx-development" "^7.16.7" + "@babel/plugin-transform-react-pure-annotations" "^7.16.7" + +"@babel/preset-typescript@^7.16.0": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.16.7.tgz" + integrity sha512-WbVEmgXdIyvzB77AQjGBEyYPZx+8tTsO50XtfozQrkW8QB2rLJpH2lgx0TRw5EJrBxOZQ+wCcyPVQvS8tjEHpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.16.7" + "@babel/helper-validator-option" "^7.16.7" + "@babel/plugin-transform-typescript" "^7.16.7" + +"@babel/runtime-corejs3@^7.10.2": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.16.8.tgz" + integrity sha512-3fKhuICS1lMz0plI5ktOE/yEtBRMVxplzRkdn6mJQ197XiY0JnrzYV0+Mxozq3JZ8SBV9Ecurmw1XsGbwOf+Sg== + dependencies: + core-js-pure "^3.20.2" + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.0.0": + version "7.21.5" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz" + integrity sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.16.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.16.7.tgz" + integrity sha512-9E9FJowqAsytyOY6LG+1KuueckRL+aQW+mKvXRXnuFGyRAyepJPmEo9vgMfXUA6O9u3IeEdv9MAkppFcaQwogQ== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.10.5": + version "7.17.2" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.2.tgz" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.17.8": + version "7.17.9" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.17.9.tgz" + integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.20.13": + version "7.21.0" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz" + integrity sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.16.7", "@babel/template@^7.3.3": + version "7.16.7" + resolved "https://registry.npmjs.org/@babel/template/-/template-7.16.7.tgz" + integrity sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/parser" "^7.16.7" + "@babel/types" "^7.16.7" + +"@babel/traverse@^7.13.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.16.8": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.8.tgz" + integrity sha512-xe+H7JlvKsDQwXRsBhSnq1/+9c+LlQcCK3Tn/l5sbx02HYns/cn7ibp9+RV1sIUqu7hKg91NWsgHurO9dowITQ== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.16.8" + "@babel/types" "^7.16.8" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/traverse@^7.16.10", "@babel/traverse@^7.7.2": + version "7.16.10" + resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.16.10.tgz" + integrity sha512-yzuaYXoRJBGMlBhsMJoUW7G1UmSb/eXr/JHYM/MsOJgavJibLwASijW7oXBdw3NQ6T0bW7Ty5P/VarOs9cHmqw== + dependencies: + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.16.8" + "@babel/helper-environment-visitor" "^7.16.7" + "@babel/helper-function-name" "^7.16.7" + "@babel/helper-hoist-variables" "^7.16.7" + "@babel/helper-split-export-declaration" "^7.16.7" + "@babel/parser" "^7.16.10" + "@babel/types" "^7.16.8" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.12.6", "@babel/types@^7.16.0", "@babel/types@^7.16.7", "@babel/types@^7.16.8", "@babel/types@^7.3.0", "@babel/types@^7.3.3", "@babel/types@^7.4.4": + version "7.16.8" + resolved "https://registry.npmjs.org/@babel/types/-/types-7.16.8.tgz" + integrity sha512-smN2DQc5s4M7fntyjGtyIPbRJv6wW4rU/94fmYJ7PKQuZkC0qGMHXJbg6sNGt12JmVr4k5YaptI/XtiLJBnmIg== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + to-fast-properties "^2.0.0" + +"@bcoe/v8-coverage@^0.2.3": + version "0.2.3" + resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz" + integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== + +"@bull-board/api@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@bull-board/api/-/api-3.10.1.tgz" + integrity sha512-ZYjNBdoBQu+UVbLAHQuEhJL96C+i7vYioc2n7FL/XoVea44XIw2WiKFcFxq0LnActPErja26QyZBQht23ph1lg== + dependencies: + redis-info "^3.0.8" + +"@bull-board/express@^3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@bull-board/express/-/express-3.10.1.tgz" + integrity sha512-jygcJBZhTZf34FXo//m6rFVcCwkXQBxSXdj2KUL5JZX8GBh80z4o95GqJtyB8A+vBN/zGr+bIG0ikkm2x8mTtQ== + dependencies: + "@bull-board/api" "3.10.1" + "@bull-board/ui" "3.10.1" + ejs "3.1.6" + express "4.17.3" + +"@bull-board/ui@3.10.1": + version "3.10.1" + resolved "https://registry.npmjs.org/@bull-board/ui/-/ui-3.10.1.tgz" + integrity sha512-K2qEAvTuyHZxUdK31HaBb9sdTFSOSKAZkxsl/LeiT4FGNF/h54iYGmWF9+HSFytggcnGdM0XnK3wLihCaIQAOQ== + dependencies: + "@bull-board/api" "3.10.1" + +"@casl/ability@^6.5.0": + version "6.5.0" + resolved "https://registry.npmjs.org/@casl/ability/-/ability-6.5.0.tgz" + integrity sha512-3guc94ugr5ylZQIpJTLz0CDfwNi0mxKVECj1vJUPAvs+Lwunh/dcuUjwzc4MHM9D8JOYX0XUZMEPedpB3vIbOw== + dependencies: + "@ucast/mongo2js" "^1.3.0" + +"@casl/react@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@casl/react/-/react-3.1.0.tgz" + integrity sha512-p4Xmex1Slxz/G0cBtZik+xyOkeOynBUe0UrMFTai6aYkYOb4NyUy3w+9rtnedjcuKijiow2HKJQjnSurLxdc/g== + +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@csstools/normalize.css@*": + version "12.0.0" + resolved "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.0.0.tgz" + integrity sha512-M0qqxAcwCsIVfpFQSlGN5XjXWu8l5JDZN+fPt1LeW5SZexQTgnaEvgXAY+CeygRw0EeppWHi12JxESWiWrB0Sg== + +"@ctrl/tinycolor@^3.6.0": + version "3.6.0" + resolved "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.0.tgz" + integrity sha512-/Z3l6pXthq0JvMYdUFyX9j0MaCltlIn6mfh9jLyQwg5aPKxkyNa0PTHtU1AlFXLNk55ZuAeJRcpvq+tmLfKmaQ== + +"@dabh/diagnostics@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz" + integrity sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + +"@dagrejs/dagre@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@dagrejs/dagre/-/dagre-1.1.2.tgz#5ec339979447091f48d2144deed8c70dfadae374" + integrity sha512-F09dphqvHsbe/6C2t2unbmpr5q41BNPEfJCdn8Z7aEBpVSy/zFQ/b4SWsweQjWNsYMDvE2ffNUN8X0CeFsEGNw== + dependencies: + "@dagrejs/graphlib" "2.2.2" + +"@dagrejs/graphlib@2.2.2": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@dagrejs/graphlib/-/graphlib-2.2.2.tgz#74154d5cb880a23b4fae71034a09b4b5aef06feb" + integrity sha512-CbyGpCDKsiTg/wuk79S7Muoj8mghDGAESWGxcSyhHX5jD35vYMBZochYVFzlHxynpE9unpu6O+4ZuhrLxASsOg== + +"@docsearch/css@3.2.1", "@docsearch/css@^3.2.1": + version "3.2.1" + resolved "https://registry.npmjs.org/@docsearch/css/-/css-3.2.1.tgz" + integrity sha512-gaP6TxxwQC+K8D6TRx5WULUWKrcbzECOPA2KCVMuI+6C7dNiGUk5yXXzVhc5sld79XKYLnO9DRTI4mjXDYkh+g== + +"@docsearch/js@^3.2.1": + version "3.2.1" + resolved "https://registry.npmjs.org/@docsearch/js/-/js-3.2.1.tgz" + integrity sha512-H1PekEtSeS0msetR2YGGey2w7jQ2wAKfGODJvQTygSwMgUZ+2DHpzUgeDyEBIXRIfaBcoQneqrzsljM62pm6Xg== + dependencies: + "@docsearch/react" "3.2.1" + preact "^10.0.0" + +"@docsearch/react@3.2.1": + version "3.2.1" + resolved "https://registry.npmjs.org/@docsearch/react/-/react-3.2.1.tgz" + integrity sha512-EzTQ/y82s14IQC5XVestiK/kFFMe2aagoYFuTAIfIb/e+4FU7kSMKonRtLwsCiLQHmjvNQq+HO+33giJ5YVtaQ== + dependencies: + "@algolia/autocomplete-core" "1.7.1" + "@algolia/autocomplete-preset-algolia" "1.7.1" + "@docsearch/css" "3.2.1" + algoliasearch "^4.0.0" + +"@emotion/babel-plugin@^11.3.0": + version "11.7.2" + resolved "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.7.2.tgz" + integrity sha512-6mGSCWi9UzXut/ZAN6lGFu33wGR3SJisNl3c0tvlmb8XChH1b2SUvxvnOh7hvLpqyRdHHU9AiazV3Cwbk5SXKQ== + dependencies: + "@babel/helper-module-imports" "^7.12.13" + "@babel/plugin-syntax-jsx" "^7.12.13" + "@babel/runtime" "^7.13.10" + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.5" + "@emotion/serialize" "^1.0.2" + babel-plugin-macros "^2.6.1" + convert-source-map "^1.5.0" + escape-string-regexp "^4.0.0" + find-root "^1.1.0" + source-map "^0.5.7" + stylis "4.0.13" + +"@emotion/cache@^11.10.5": + version "11.10.5" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz" + integrity sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA== + dependencies: + "@emotion/memoize" "^0.8.0" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + "@emotion/weak-memoize" "^0.3.0" + stylis "4.1.3" + +"@emotion/cache@^11.7.1": + version "11.7.1" + resolved "https://registry.npmjs.org/@emotion/cache/-/cache-11.7.1.tgz" + integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A== + dependencies: + "@emotion/memoize" "^0.7.4" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + stylis "4.0.13" + +"@emotion/hash@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz" + integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow== + +"@emotion/is-prop-valid@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.1.1.tgz" + integrity sha512-bW1Tos67CZkOURLc0OalnfxtSXQJMrAMV0jZTVGJUPSOd4qgjF3+tTD5CwJM13PHA8cltGW1WGbbvV9NpvUZPw== + dependencies: + "@emotion/memoize" "^0.7.4" + +"@emotion/is-prop-valid@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz" + integrity sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg== + dependencies: + "@emotion/memoize" "^0.8.0" + +"@emotion/memoize@^0.7.4", "@emotion/memoize@^0.7.5": + version "0.7.5" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.5.tgz" + integrity sha512-igX9a37DR2ZPGYtV6suZ6whr8pTFtyHL3K/oLUotxpSVO2ASaprmAe2Dkq7tBo7CRY7MMDrAa9nuQP9/YG8FxQ== + +"@emotion/memoize@^0.8.0": + version "0.8.0" + resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz" + integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== + +"@emotion/react@^11.4.1": + version "11.7.1" + resolved "https://registry.npmjs.org/@emotion/react/-/react-11.7.1.tgz" + integrity sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/cache" "^11.7.1" + "@emotion/serialize" "^1.0.2" + "@emotion/sheet" "^1.1.0" + "@emotion/utils" "^1.0.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + +"@emotion/serialize@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.0.2.tgz" + integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A== + dependencies: + "@emotion/hash" "^0.8.0" + "@emotion/memoize" "^0.7.4" + "@emotion/unitless" "^0.7.5" + "@emotion/utils" "^1.0.0" + csstype "^3.0.2" + +"@emotion/sheet@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.1.0.tgz" + integrity sha512-u0AX4aSo25sMAygCuQTzS+HsImZFuS8llY8O7b9MDRzbJM0kVJlAz6KNDqcG7pOuQZJmj/8X/rAW+66kMnMW+g== + +"@emotion/sheet@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz" + integrity sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA== + +"@emotion/styled@^11.3.0": + version "11.6.0" + resolved "https://registry.npmjs.org/@emotion/styled/-/styled-11.6.0.tgz" + integrity sha512-mxVtVyIOTmCAkFbwIp+nCjTXJNgcz4VWkOYQro87jE2QBTydnkiYusMrRGFtzuruiGK4dDaNORk4gH049iiQuw== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.3.0" + "@emotion/is-prop-valid" "^1.1.1" + "@emotion/serialize" "^1.0.2" + "@emotion/utils" "^1.0.0" + +"@emotion/unitless@^0.7.5": + version "0.7.5" + resolved "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz" + integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg== + +"@emotion/utils@^1.0.0": + version "1.0.0" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.0.0.tgz" + integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA== + +"@emotion/utils@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz" + integrity sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw== + +"@emotion/weak-memoize@^0.2.5": + version "0.2.5" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz" + integrity sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA== + +"@emotion/weak-memoize@^0.3.0": + version "0.3.0" + resolved "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz" + integrity sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg== + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.15.11": + version "0.15.11" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.11.tgz#bdd9c3e098183bdca97075aa4c3e0152ed3e10ee" + integrity sha512-PzMcQLazLBkwDEkrNPi9AbjFt6+3I7HKbiYF2XtWQ7wItrHvEOeO3T8Am434zAozWtVP7lrTue1bEfc2nYWeCA== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.15.11": + version "0.15.11" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.11.tgz#2f4f9a1083dcb4fc65233b6f59003c406abf32e5" + integrity sha512-geWp637tUhNmhL3Xgy4Bj703yXB9dqiLJe05lCUfjSFDrQf9C/8pArusyPUbUbPwlC/EAUjBw32sxuIl/11dZw== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + +"@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint/eslintrc@^1.0.5": + version "1.0.5" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.0.5.tgz" + integrity sha512-BLxsnmK3KyPunz5wmCCpqy0YelEoxxGmH73Is+Z74oOTMtExcjkr3dDR6quwrjh1YspA8DH9gnX1o069KiS9AQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.2.0" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@eslint/eslintrc@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.2.1.tgz" + integrity sha512-bxvbYnBPN1Gibwyp6NrpnFzA3YtRL3BBAyEAFVIpNTm2Rn4Vy87GA5M4aSn3InRrlsbX5N0GW7XIx+U4SAEKdQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.1" + globals "^13.9.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@faker-js/faker@^8.2.0": + version "8.2.0" + resolved "https://registry.yarnpkg.com/@faker-js/faker/-/faker-8.2.0.tgz#d4656d2cb485fe6ec4e7b340da9f16fac2c36c4a" + integrity sha512-VacmzZqVxdWdf9y64lDOMZNDMM/FQdtM9IsaOPKOm2suYwEatb8VkdHqOzXcDnZbk7YDE2BmsJmy/2Hmkn563g== + +"@formatjs/ecma402-abstract@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.1.tgz" + integrity sha512-tgtNODZUGuUI6PAcnvaLZpGrZLVkXnnAvgzOiueYMzFdOdcOw4iH1WKhCe3+r6VR8rHKToJ2HksUGNCB+zt/bg== + dependencies: + "@formatjs/intl-localematcher" "0.2.22" + tslib "^2.1.0" + +"@formatjs/fast-memoize@1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz" + integrity sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg== + dependencies: + tslib "^2.1.0" + +"@formatjs/icu-messageformat-parser@2.0.16": + version "2.0.16" + resolved "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.0.16.tgz" + integrity sha512-sYg0ImXsAqBbjU/LotoCD9yKC5nUpWVy3s4DwWerHXD4sm62FcjMF8mekwudRk3eZLHqSO+M21MpFUUjDQ+Q5Q== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/icu-skeleton-parser" "1.3.3" + tslib "^2.1.0" + +"@formatjs/icu-skeleton-parser@1.3.3": + version "1.3.3" + resolved "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.3.tgz" + integrity sha512-ifWnzjmHPHUF89UpCvClTP66sXYFc8W/qg7Qt+qtTUB9BqRWlFeUsevAzaMYDJsRiOy4S2WJFrJoZgRKUFfPGQ== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + tslib "^2.1.0" + +"@formatjs/intl-displaynames@5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-5.4.0.tgz" + integrity sha512-zWmTkq9eGOeJCmw22KPXW6rlnx3Z3CIV+rc/jh9ytEfm1Ps/OgOITe4h6ZTDrQC+nXVACvLO1Kpes4jMWcjWuQ== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/intl-localematcher" "0.2.22" + tslib "^2.1.0" + +"@formatjs/intl-listformat@6.5.0": + version "6.5.0" + resolved "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-6.5.0.tgz" + integrity sha512-gVyAV5QWWtq84MK4cAyJITW+Wb74c2+FT+wa8jhSPxXUky9B5z/k/Ff7or4Vb3KV0YYZuVBQ/vMIoD8Gr182ww== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/intl-localematcher" "0.2.22" + tslib "^2.1.0" + +"@formatjs/intl-localematcher@0.2.22": + version "0.2.22" + resolved "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.22.tgz" + integrity sha512-z+TvbHW8Q/g2l7/PnfUl0mV9gWxV4d0HT6GQyzkO5QI6QjCvCZGiztnmLX7zoyS16uSMvZ2PoMDfSK9xvZkRRA== + dependencies: + tslib "^2.1.0" + +"@formatjs/intl@1.18.3": + version "1.18.3" + resolved "https://registry.npmjs.org/@formatjs/intl/-/intl-1.18.3.tgz" + integrity sha512-eMdU2FBAvC2vMeQRjvBhJeRNsftZ2VXdB4jW1KKbP72O4JWB9lv2KqEdS2jo6DfhDvm0EAMZXMNEEK8ybTxfyA== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/fast-memoize" "1.2.1" + "@formatjs/icu-messageformat-parser" "2.0.16" + "@formatjs/intl-displaynames" "5.4.0" + "@formatjs/intl-listformat" "6.5.0" + intl-messageformat "9.11.2" + tslib "^2.1.0" + +"@gar/promisify@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz" + integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw== + +"@graphql-tools/batch-execute@^8.3.2": + version "8.3.2" + resolved "https://registry.npmjs.org/@graphql-tools/batch-execute/-/batch-execute-8.3.2.tgz" + integrity sha512-ICWqM+MvEkIPHm18Q0cmkvm134zeQMomBKmTRxyxMNhL/ouz6Nqld52/brSlaHnzA3fczupeRJzZ0YatruGBcQ== + dependencies: + "@graphql-tools/utils" "^8.6.2" + dataloader "2.0.0" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/delegate@^8.5.1": + version "8.5.1" + resolved "https://registry.npmjs.org/@graphql-tools/delegate/-/delegate-8.5.1.tgz" + integrity sha512-/YPmVxitt57F8sH50pnfXASzOOjEfaUDkX48eF5q6f16+JBncej2zeu+Zm2c68q8MbIxhPlEGfpd0QZeqTvAxw== + dependencies: + "@graphql-tools/batch-execute" "^8.3.2" + "@graphql-tools/schema" "^8.3.2" + "@graphql-tools/utils" "^8.6.2" + dataloader "2.0.0" + graphql-executor "0.0.18" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/graphql-file-loader@^7.3.4": + version "7.3.4" + resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.3.4.tgz" + integrity sha512-Q0/YtDq0APR6syRclsQMNguWKRlchd8nFTOpLhfc7Xeiy21VhEEi4Ik+quRySfb7ubDfJGhiUq4MQW43FhWJvg== + dependencies: + "@graphql-tools/import" "^6.6.6" + "@graphql-tools/utils" "^8.6.2" + globby "^11.0.3" + tslib "~2.3.0" + unixify "^1.0.0" + +"@graphql-tools/import@^6.6.6": + version "6.6.6" + resolved "https://registry.npmjs.org/@graphql-tools/import/-/import-6.6.6.tgz" + integrity sha512-a0aVajxqu1MsL8EwavA44Osw20lBOIhq8IM2ZIHFPP62cPAcOB26P+Sq57DHMsSyX5YQ0ab9XPM2o4e1dQhs0w== + dependencies: + "@graphql-tools/utils" "8.6.2" + resolve-from "5.0.0" + tslib "~2.3.0" + +"@graphql-tools/load@^7.5.2": + version "7.5.2" + resolved "https://registry.npmjs.org/@graphql-tools/load/-/load-7.5.2.tgz" + integrity sha512-URPqVP77mYxdZxT895DzrWf2C23S3yC/oAmXD4D4YlxR5eVVH/fxu0aZR78WcEKF331fWSiFwWy9j7BZWvkj7g== + dependencies: + "@graphql-tools/schema" "8.3.2" + "@graphql-tools/utils" "^8.6.2" + p-limit "3.1.0" + tslib "~2.3.0" + +"@graphql-tools/merge@^8.2.3": + version "8.2.3" + resolved "https://registry.npmjs.org/@graphql-tools/merge/-/merge-8.2.3.tgz" + integrity sha512-XCSmL6/Xg8259OTWNp69B57CPWiVL69kB7pposFrufG/zaAlI9BS68dgzrxmmSqZV5ZHU4r/6Tbf6fwnEJGiSw== + dependencies: + "@graphql-tools/utils" "^8.6.2" + tslib "~2.3.0" + +"@graphql-tools/schema@8.3.2", "@graphql-tools/schema@^8.2.0", "@graphql-tools/schema@^8.3.2": + version "8.3.2" + resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-8.3.2.tgz" + integrity sha512-77feSmIuHdoxMXRbRyxE8rEziKesd/AcqKV6fmxe7Zt+PgIQITxNDew2XJJg7qFTMNM43W77Ia6njUSBxNOkwg== + dependencies: + "@graphql-tools/merge" "^8.2.3" + "@graphql-tools/utils" "^8.6.2" + tslib "~2.3.0" + value-or-promise "1.0.11" + +"@graphql-tools/utils@8.6.2", "@graphql-tools/utils@^8.6.2": + version "8.6.2" + resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-8.6.2.tgz" + integrity sha512-x1DG0cJgpJtImUlNE780B/dfp8pxvVxOD6UeykFH5rHes26S4kGokbgU8F1IgrJ1vAPm/OVBHtd2kicTsPfwdA== + dependencies: + tslib "~2.3.0" + +"@graphql-typed-document-node/core@^3.0.0", "@graphql-typed-document-node/core@^3.1.1": + version "3.1.1" + resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.1.1.tgz" + integrity sha512-NQ17ii0rK1b34VZonlmT2QMJFI70m0TRwbknO/ihlbatXyaktDhN/98vBiUU6kNBPljqGqyIrl2T4nY2RpFANg== + +"@hookform/resolvers@^2.8.8": + version "2.8.8" + resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-2.8.8.tgz" + integrity sha512-meAEDur1IJBfKyTo9yPYAuzjIfrxA7m9Ov+1nxaW/YupsqMeseWifoUjWK03+hz/RJizsVQAaUjVxFEkyu0GWg== + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.2" + resolved "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.2.tgz" + integrity sha512-UXOuFCGcwciWckOpmfKDq/GyhlTf9pN/BzG//x8p8zTOFEcGuA68ANXheFS0AGvy3qgZqLBUkMs7hqzqCKOVwA== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@hutson/parse-repository-url@^3.0.0": + version "3.0.2" + resolved "https://registry.npmjs.org/@hutson/parse-repository-url/-/parse-repository-url-3.0.2.tgz" + integrity sha512-H9XAx3hc0BQHY6l+IFSWHDySypcXsvsuLhgYLUGywmJ5pswRVQJUHpOsobnLYp2ZUaUlKiKDrgWWhosOwAEM8Q== + +"@ioredis/commands@^1.1.1": + version "1.2.0" + resolved "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz" + integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@istanbuljs/load-nyc-config@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz" + integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== + dependencies: + camelcase "^5.3.1" + find-up "^4.1.0" + get-package-type "^0.1.0" + js-yaml "^3.13.1" + resolve-from "^5.0.0" + +"@istanbuljs/schema@^0.1.2": + version "0.1.3" + resolved "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz" + integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== + +"@jest/console@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/console/-/console-27.4.6.tgz" + integrity sha512-jauXyacQD33n47A44KrlOVeiXHEXDqapSdfb9kTekOchH/Pd18kBIO1+xxJQRLuG+LUuljFCwTG92ra4NW7SpA== + dependencies: + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + jest-message-util "^27.4.6" + jest-util "^27.4.2" + slash "^3.0.0" + +"@jest/core@^27.4.7": + version "27.4.7" + resolved "https://registry.npmjs.org/@jest/core/-/core-27.4.7.tgz" + integrity sha512-n181PurSJkVMS+kClIFSX/LLvw9ExSb+4IMtD6YnfxZVerw9ANYtW0bPrm0MJu2pfe9SY9FJ9FtQ+MdZkrZwjg== + dependencies: + "@jest/console" "^27.4.6" + "@jest/reporters" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" + "@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 "^27.4.2" + jest-config "^27.4.7" + jest-haste-map "^27.4.6" + jest-message-util "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-resolve-dependencies "^27.4.6" + jest-runner "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + jest-validate "^27.4.6" + jest-watcher "^27.4.6" + micromatch "^4.0.4" + rimraf "^3.0.0" + slash "^3.0.0" + strip-ansi "^6.0.0" + +"@jest/environment@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/environment/-/environment-27.4.6.tgz" + integrity sha512-E6t+RXPfATEEGVidr84WngLNWZ8ffCPky8RqqRK6u1Bn0LK92INe0MDttyPl/JOzaq92BmDzOeuqk09TvM22Sg== + dependencies: + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + jest-mock "^27.4.6" + +"@jest/fake-timers@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.4.6.tgz" + integrity sha512-mfaethuYF8scV8ntPpiVGIHQgS0XIALbpY2jt2l7wb/bvq4Q5pDLk4EP4D7SAvYT1QrPOPVZAtbdGAOOyIgs7A== + dependencies: + "@jest/types" "^27.4.2" + "@sinonjs/fake-timers" "^8.0.1" + "@types/node" "*" + jest-message-util "^27.4.6" + jest-mock "^27.4.6" + jest-util "^27.4.2" + +"@jest/globals@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/globals/-/globals-27.4.6.tgz" + integrity sha512-kAiwMGZ7UxrgPzu8Yv9uvWmXXxsy0GciNejlHvfPIfWkSxChzv6bgTS3YqBkGuHcis+ouMFI2696n2t+XYIeFw== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/types" "^27.4.2" + expect "^27.4.6" + +"@jest/reporters@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/reporters/-/reporters-27.4.6.tgz" + integrity sha512-+Zo9gV81R14+PSq4wzee4GC2mhAN9i9a7qgJWL90Gpx7fHYkWpTBvwWNZUXvJByYR9tAVBdc8VxDWqfJyIUrIQ== + dependencies: + "@bcoe/v8-coverage" "^0.2.3" + "@jest/console" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" + "@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" + istanbul-lib-coverage "^3.0.0" + istanbul-lib-instrument "^5.1.0" + istanbul-lib-report "^3.0.0" + istanbul-lib-source-maps "^4.0.0" + istanbul-reports "^3.1.3" + jest-haste-map "^27.4.6" + jest-resolve "^27.4.6" + jest-util "^27.4.2" + jest-worker "^27.4.6" + slash "^3.0.0" + source-map "^0.6.0" + string-length "^4.0.1" + terminal-link "^2.0.0" + v8-to-istanbul "^8.1.0" + +"@jest/schemas@^29.6.3": + version "29.6.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" + integrity sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA== + dependencies: + "@sinclair/typebox" "^0.27.8" + +"@jest/source-map@^27.4.0": + version "27.4.0" + resolved "https://registry.npmjs.org/@jest/source-map/-/source-map-27.4.0.tgz" + integrity sha512-Ntjx9jzP26Bvhbm93z/AKcPRj/9wrkI88/gK60glXDx1q+IeI0rf7Lw2c89Ch6ofonB0On/iRDreQuQ6te9pgQ== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.2.4" + source-map "^0.6.0" + +"@jest/test-result@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/test-result/-/test-result-27.4.6.tgz" + integrity sha512-fi9IGj3fkOrlMmhQqa/t9xum8jaJOOAi/lZlm6JXSc55rJMXKHxNDN1oCP39B0/DhNOa2OMupF9BcKZnNtXMOQ== + dependencies: + "@jest/console" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/istanbul-lib-coverage" "^2.0.0" + collect-v8-coverage "^1.0.0" + +"@jest/test-sequencer@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-27.4.6.tgz" + integrity sha512-3GL+nsf6E1PsyNsJuvPyIz+DwFuCtBdtvPpm/LMXVkBJbdFvQYCDpccYT56qq5BGniXWlE81n2qk1sdXfZebnw== + dependencies: + "@jest/test-result" "^27.4.6" + graceful-fs "^4.2.4" + jest-haste-map "^27.4.6" + jest-runtime "^27.4.6" + +"@jest/transform@^27.4.6": + version "27.4.6" + resolved "https://registry.npmjs.org/@jest/transform/-/transform-27.4.6.tgz" + integrity sha512-9MsufmJC8t5JTpWEQJ0OcOOAXaH5ioaIX6uHVBLBMoCZPfKKQF+EqP8kACAvCZ0Y1h2Zr3uOccg8re+Dr5jxyw== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^27.4.2" + 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 "^27.4.6" + jest-regex-util "^27.4.0" + jest-util "^27.4.2" + 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.npmjs.org/@jest/types/-/types-26.6.2.tgz" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@jest/types@^27.4.2": + version "27.4.2" + resolved "https://registry.npmjs.org/@jest/types/-/types-27.4.2.tgz" + integrity sha512-j35yw0PMTPpZsUoOBiuHzr1zTYoad1cVIE0ajEjcrJONxxrko/IRGKkXx3os0Nsi4Hu3+5VmDbVfq5WhG/pWAg== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^3.0.0" + "@types/node" "*" + "@types/yargs" "^16.0.0" + chalk "^4.0.0" + +"@jridgewell/gen-mapping@^0.3.0": + version "0.3.2" + resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz" + 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.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.14" + resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.9": + version "0.3.19" + resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz" + integrity sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@juggle/resize-observer@^3.4.0": + version "3.4.0" + resolved "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.4.0.tgz" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + +"@lerna/add@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz" + integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== + dependencies: + "@lerna/bootstrap" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + npm-package-arg "^8.1.0" + p-map "^4.0.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/bootstrap@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz" + integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/has-npm-version" "4.0.0" + "@lerna/npm-install" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + read-package-tree "^5.3.1" + semver "^7.3.4" + +"@lerna/changed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz" + integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/check-working-tree@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz" + integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== + dependencies: + "@lerna/collect-uncommitted" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/validation-error" "4.0.0" + +"@lerna/child-process@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz" + integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== + dependencies: + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" + +"@lerna/clean@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz" + integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + +"@lerna/cli@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz" + integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== + dependencies: + "@lerna/global-options" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + yargs "^16.2.0" + +"@lerna/collect-uncommitted@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz" + integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== + dependencies: + "@lerna/child-process" "4.0.0" + chalk "^4.1.0" + npmlog "^4.1.2" + +"@lerna/collect-updates@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz" + integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/describe-ref" "4.0.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + slash "^3.0.0" + +"@lerna/command@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz" + integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/project" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/write-log-file" "4.0.0" + clone-deep "^4.0.1" + dedent "^0.7.0" + execa "^5.0.0" + is-ci "^2.0.0" + npmlog "^4.1.2" + +"@lerna/conventional-commits@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz" + integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== + dependencies: + "@lerna/validation-error" "4.0.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.2" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" + lodash.template "^4.5.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + semver "^7.3.4" + +"@lerna/create-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz" + integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== + dependencies: + cmd-shim "^4.1.0" + fs-extra "^9.1.0" + npmlog "^4.1.2" + +"@lerna/create@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz" + integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + globby "^11.0.2" + init-package-json "^2.0.2" + npm-package-arg "^8.1.0" + p-reduce "^2.1.0" + pacote "^11.2.6" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + whatwg-url "^8.4.0" + yargs-parser "20.2.4" + +"@lerna/describe-ref@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz" + integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + +"@lerna/diff@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz" + integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/validation-error" "4.0.0" + npmlog "^4.1.2" + +"@lerna/exec@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz" + integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/filter-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz" + integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== + dependencies: + "@lerna/collect-updates" "4.0.0" + "@lerna/filter-packages" "4.0.0" + dedent "^0.7.0" + npmlog "^4.1.2" + +"@lerna/filter-packages@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz" + integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== + dependencies: + "@lerna/validation-error" "4.0.0" + multimatch "^5.0.0" + npmlog "^4.1.2" + +"@lerna/get-npm-exec-opts@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz" + integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== + dependencies: + npmlog "^4.1.2" + +"@lerna/get-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz" + integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== + dependencies: + fs-extra "^9.1.0" + ssri "^8.0.1" + tar "^6.1.0" + +"@lerna/github-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz" + integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== + dependencies: + "@lerna/child-process" "4.0.0" + "@octokit/plugin-enterprise-rest" "^6.0.1" + "@octokit/rest" "^18.1.0" + git-url-parse "^11.4.4" + npmlog "^4.1.2" + +"@lerna/gitlab-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz" + integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== + dependencies: + node-fetch "^2.6.1" + npmlog "^4.1.2" + whatwg-url "^8.4.0" + +"@lerna/global-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz" + integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== + +"@lerna/has-npm-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz" + integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== + dependencies: + "@lerna/child-process" "4.0.0" + semver "^7.3.4" + +"@lerna/import@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz" + integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/validation-error" "4.0.0" + dedent "^0.7.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" + +"@lerna/info@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz" + integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/output" "4.0.0" + envinfo "^7.7.4" + +"@lerna/init@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz" + integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/link@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz" + integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + p-map "^4.0.0" + slash "^3.0.0" + +"@lerna/list@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz" + integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" + +"@lerna/listable@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz" + integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== + dependencies: + "@lerna/query-graph" "4.0.0" + chalk "^4.1.0" + columnify "^1.5.4" + +"@lerna/log-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz" + integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== + dependencies: + byte-size "^7.0.0" + columnify "^1.5.4" + has-unicode "^2.0.1" + npmlog "^4.1.2" + +"@lerna/npm-conf@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz" + integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== + dependencies: + config-chain "^1.1.12" + pify "^5.0.0" + +"@lerna/npm-dist-tag@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz" + integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== + dependencies: + "@lerna/otplease" "4.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + +"@lerna/npm-install@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz" + integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + fs-extra "^9.1.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + signal-exit "^3.0.3" + write-pkg "^4.0.0" + +"@lerna/npm-publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz" + integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== + dependencies: + "@lerna/otplease" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + fs-extra "^9.1.0" + libnpmpublish "^4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + pify "^5.0.0" + read-package-json "^3.0.0" + +"@lerna/npm-run-script@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz" + integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== + dependencies: + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + npmlog "^4.1.2" + +"@lerna/otplease@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz" + integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== + dependencies: + "@lerna/prompt" "4.0.0" + +"@lerna/output@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz" + integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== + dependencies: + npmlog "^4.1.2" + +"@lerna/pack-directory@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz" + integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== + dependencies: + "@lerna/get-packed" "4.0.0" + "@lerna/package" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + npm-packlist "^2.1.4" + npmlog "^4.1.2" + tar "^6.1.0" + temp-write "^4.0.0" + +"@lerna/package-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz" + integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== + dependencies: + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/validation-error" "4.0.0" + npm-package-arg "^8.1.0" + npmlog "^4.1.2" + semver "^7.3.4" + +"@lerna/package@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz" + integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== + dependencies: + load-json-file "^6.2.0" + npm-package-arg "^8.1.0" + write-pkg "^4.0.0" + +"@lerna/prerelease-id-from-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz" + integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== + dependencies: + semver "^7.3.4" + +"@lerna/profiler@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz" + integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + upath "^2.0.1" + +"@lerna/project@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz" + integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== + dependencies: + "@lerna/package" "4.0.0" + "@lerna/validation-error" "4.0.0" + cosmiconfig "^7.0.0" + dedent "^0.7.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + load-json-file "^6.2.0" + npmlog "^4.1.2" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" + +"@lerna/prompt@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz" + integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== + dependencies: + inquirer "^7.3.3" + npmlog "^4.1.2" + +"@lerna/publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz" + integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/log-packed" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/npm-dist-tag" "4.0.0" + "@lerna/npm-publish" "4.0.0" + "@lerna/otplease" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/pack-directory" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/version" "4.0.0" + fs-extra "^9.1.0" + libnpmaccess "^4.0.1" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^11.2.6" + semver "^7.3.4" + +"@lerna/pulse-till-done@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz" + integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== + dependencies: + npmlog "^4.1.2" + +"@lerna/query-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz" + integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== + dependencies: + "@lerna/package-graph" "4.0.0" + +"@lerna/resolve-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz" + integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== + dependencies: + fs-extra "^9.1.0" + npmlog "^4.1.2" + read-cmd-shim "^2.0.0" + +"@lerna/rimraf-dir@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz" + integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== + dependencies: + "@lerna/child-process" "4.0.0" + npmlog "^4.1.2" + path-exists "^4.0.0" + rimraf "^3.0.2" + +"@lerna/run-lifecycle@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz" + integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== + dependencies: + "@lerna/npm-conf" "4.0.0" + npm-lifecycle "^3.1.5" + npmlog "^4.1.2" + +"@lerna/run-topologically@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz" + integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== + dependencies: + "@lerna/query-graph" "4.0.0" + p-queue "^6.6.2" + +"@lerna/run@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz" + integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== + dependencies: + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-run-script" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/timer" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" + +"@lerna/symlink-binary@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz" + integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/package" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + +"@lerna/symlink-dependencies@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz" + integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== + dependencies: + "@lerna/create-symlink" "4.0.0" + "@lerna/resolve-symlink" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + +"@lerna/timer@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz" + integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== + +"@lerna/validation-error@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz" + integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== + dependencies: + npmlog "^4.1.2" + +"@lerna/version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz" + integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== + dependencies: + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/conventional-commits" "4.0.0" + "@lerna/github-client" "4.0.0" + "@lerna/gitlab-client" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + chalk "^4.1.0" + dedent "^0.7.0" + load-json-file "^6.2.0" + minimatch "^3.0.4" + npmlog "^4.1.2" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + temp-write "^4.0.0" + write-json-file "^4.3.0" + +"@lerna/write-log-file@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz" + integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== + dependencies: + npmlog "^4.1.2" + write-file-atomic "^3.0.3" + +"@mapbox/node-pre-gyp@^1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa" + integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ== + dependencies: + detect-libc "^2.0.0" + https-proxy-agent "^5.0.0" + make-dir "^3.1.0" + node-fetch "^2.6.7" + nopt "^5.0.0" + npmlog "^5.0.1" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.11" + +"@msgpackr-extract/msgpackr-extract-darwin-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-2.1.2.tgz" + integrity sha512-TyVLn3S/+ikMDsh0gbKv2YydKClN8HaJDDpONlaZR+LVJmsxLFUgA+O7zu59h9+f9gX1aj/ahw9wqa6rosmrYQ== + +"@msgpackr-extract/msgpackr-extract-darwin-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-2.1.2.tgz#bfbc6936ede2955218f5621a675679a5fe8e6f4c" + integrity sha512-YPXtcVkhmVNoMGlqp81ZHW4dMxK09msWgnxtsDpSiZwTzUBG2N+No2bsr7WMtBKCVJMSD6mbAl7YhKUqkp/Few== + +"@msgpackr-extract/msgpackr-extract-linux-arm64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-2.1.2.tgz#22555e28382af2922e7450634c8a2f240bb9eb82" + integrity sha512-vHZ2JiOWF2+DN9lzltGbhtQNzDo8fKFGrf37UJrgqxU0yvtERrzUugnfnX1wmVfFhSsF8OxrfqiNOUc5hko1Zg== + +"@msgpackr-extract/msgpackr-extract-linux-arm@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-2.1.2.tgz#ffb6ae1beea7ac572b6be6bf2a8e8162ebdd8be7" + integrity sha512-42R4MAFeIeNn+L98qwxAt360bwzX2Kf0ZQkBBucJ2Ircza3asoY4CDbgiu9VWklq8gWJVSJSJBwDI+c/THiWkA== + +"@msgpackr-extract/msgpackr-extract-linux-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-2.1.2.tgz#7caf62eebbfb1345de40f75e89666b3d4194755f" + integrity sha512-RjRoRxg7Q3kPAdUSC5EUUPlwfMkIVhmaRTIe+cqHbKrGZ4M6TyCA/b5qMaukQ/1CHWrqYY2FbKOAU8Hg0pQFzg== + +"@msgpackr-extract/msgpackr-extract-win32-x64@2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-2.1.2.tgz#f2d8b9ddd8d191205ed26ce54aba3dfc5ae3e7c9" + integrity sha512-rIZVR48zA8hGkHIK7ED6+ZiXsjRCcAVBJbm8o89OKAMTmEAQ2QvoOxoiu3w2isAaWwzgtQIOFIqHwvZDyLKCvw== + +"@mui/base@5.0.0-alpha.118": + version "5.0.0-alpha.118" + resolved "https://registry.npmjs.org/@mui/base/-/base-5.0.0-alpha.118.tgz" + integrity sha512-GAEpqhnuHjRaAZLdxFNuOf2GDTp9sUawM46oHZV4VnYPFjXJDkIYFWfIQLONb0nga92OiqS5DD/scGzVKCL0Mw== + dependencies: + "@babel/runtime" "^7.20.13" + "@emotion/is-prop-valid" "^1.2.0" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.9" + "@popperjs/core" "^2.11.6" + clsx "^1.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/core-downloads-tracker@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.11.9.tgz" + integrity sha512-YGEtucQ/Nl91VZkzYaLad47Cdui51n/hW+OQm4210g4N3/nZzBxmGeKfubEalf+ShKH4aYDS86XTO6q/TpZnjQ== + +"@mui/icons-material@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.9.tgz" + integrity sha512-SPANMk6K757Q1x48nCwPGdSNb8B71d+2hPMJ0V12VWerpSsbjZtvAPi5FAn13l2O5mwWkvI0Kne+0tCgnNxMNw== + dependencies: + "@babel/runtime" "^7.20.13" + +"@mui/lab@^5.0.0-alpha.120": + version "5.0.0-alpha.120" + resolved "https://registry.npmjs.org/@mui/lab/-/lab-5.0.0-alpha.120.tgz" + integrity sha512-vjlF2jTKSZnNxtUO0xxHEDfpL5cG0LLNRsfKv8TYOiPs0Q1bbqO3YfqJsqxv8yh+wx7EFZc8lwJ4NSAQdenW3A== + dependencies: + "@babel/runtime" "^7.20.13" + "@mui/base" "5.0.0-alpha.118" + "@mui/system" "^5.11.9" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.9" + clsx "^1.2.1" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@mui/material@^5.11.10": + version "5.11.10" + resolved "https://registry.npmjs.org/@mui/material/-/material-5.11.10.tgz" + integrity sha512-hs1WErbiedqlJIZsljgoil908x4NMp8Lfk8di+5c7o809roqKcFTg2+k3z5ucKvs29AXcsdXrDB/kn2K6dGYIw== + dependencies: + "@babel/runtime" "^7.20.13" + "@mui/base" "5.0.0-alpha.118" + "@mui/core-downloads-tracker" "^5.11.9" + "@mui/system" "^5.11.9" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.9" + "@types/react-transition-group" "^4.4.5" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + react-is "^18.2.0" + react-transition-group "^4.4.5" + +"@mui/private-theming@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.11.9.tgz" + integrity sha512-XMyVIFGomVCmCm92EvYlgq3zrC9K+J6r7IKl/rBJT2/xVYoRY6uM7jeB+Wxh7kXxnW9Dbqsr2yL3cx6wSD1sAg== + dependencies: + "@babel/runtime" "^7.20.13" + "@mui/utils" "^5.11.9" + prop-types "^15.8.1" + +"@mui/styled-engine@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.11.9.tgz" + integrity sha512-bkh2CjHKOMy98HyOc8wQXEZvhOmDa/bhxMUekFX5IG0/w4f5HJ8R6+K6nakUUYNEgjOWPYzNPrvGB8EcGbhahQ== + dependencies: + "@babel/runtime" "^7.20.13" + "@emotion/cache" "^11.10.5" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/system@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/system/-/system-5.11.9.tgz" + integrity sha512-h6uarf+l3FO6l75Nf7yO+qDGrIoa1DM9nAMCUFZQsNCDKOInRzcptnm8M1w/Z3gVetfeeGoIGAYuYKbft6KZZA== + dependencies: + "@babel/runtime" "^7.20.13" + "@mui/private-theming" "^5.11.9" + "@mui/styled-engine" "^5.11.9" + "@mui/types" "^7.2.3" + "@mui/utils" "^5.11.9" + clsx "^1.2.1" + csstype "^3.1.1" + prop-types "^15.8.1" + +"@mui/types@^7.2.3": + version "7.2.3" + resolved "https://registry.npmjs.org/@mui/types/-/types-7.2.3.tgz" + integrity sha512-tZ+CQggbe9Ol7e/Fs5RcKwg/woU+o8DCtOnccX6KmbBc7YrfqMYEYuaIcXHuhpT880QwNkZZ3wQwvtlDFA2yOw== + +"@mui/utils@^5.11.9": + version "5.11.9" + resolved "https://registry.npmjs.org/@mui/utils/-/utils-5.11.9.tgz" + integrity sha512-eOJaqzcEs4qEwolcvFAmXGpln+uvouvOS9FUX6Wkrte+4I8rZbjODOBDVNlK+V6/ziTfD4iNKC0G+KfOTApbqg== + dependencies: + "@babel/runtime" "^7.20.13" + "@types/prop-types" "^15.7.5" + "@types/react-is" "^16.7.1 || ^17.0.0" + prop-types "^15.8.1" + react-is "^18.2.0" + +"@node-saml/node-saml@^4.0.4": + version "4.0.5" + resolved "https://registry.npmjs.org/@node-saml/node-saml/-/node-saml-4.0.5.tgz" + integrity sha512-J5DglElbY1tjOuaR1NPtjOXkXY5bpUhDoKVoeucYN98A3w4fwgjIOPqIGcb6cQsqFq2zZ6vTCeKn5C/hvefSaw== + dependencies: + "@types/debug" "^4.1.7" + "@types/passport" "^1.0.11" + "@types/xml-crypto" "^1.4.2" + "@types/xml-encryption" "^1.2.1" + "@types/xml2js" "^0.4.11" + "@xmldom/xmldom" "^0.8.6" + debug "^4.3.4" + xml-crypto "^3.0.1" + xml-encryption "^3.0.2" + xml2js "^0.5.0" + xmlbuilder "^15.1.1" + +"@node-saml/passport-saml@^4.0.4": + version "4.0.4" + resolved "https://registry.npmjs.org/@node-saml/passport-saml/-/passport-saml-4.0.4.tgz" + integrity sha512-xFw3gw0yo+K1mzlkW15NeBF7cVpRHN/4vpjmBKzov5YFImCWh/G0LcTZ8krH3yk2/eRPc3Or8LRPudVJBjmYaw== + dependencies: + "@node-saml/node-saml" "^4.0.4" + "@types/express" "^4.17.14" + "@types/passport" "^1.0.11" + "@types/passport-strategy" "^0.2.35" + passport "^0.6.0" + passport-strategy "^1.0.0" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@npmcli/agent@^2.0.0": + version "2.2.2" + resolved "https://registry.yarnpkg.com/@npmcli/agent/-/agent-2.2.2.tgz#967604918e62f620a648c7975461c9c9e74fc5d5" + integrity sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og== + dependencies: + agent-base "^7.1.0" + http-proxy-agent "^7.0.0" + https-proxy-agent "^7.0.1" + lru-cache "^10.0.1" + socks-proxy-agent "^8.0.3" + +"@npmcli/ci-detect@^1.0.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.4.0.tgz" + integrity sha512-3BGrt6FLjqM6br5AhWRKTr3u5GIVkjRYeAFrMp3HjnfICrg4xOrVRwFavKT6tsp++bq5dluL5t8ME/Nha/6c1Q== + +"@npmcli/fs@^1.0.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.0.tgz" + integrity sha512-VhP1qZLXcrXRIaPoqb4YA55JQxLNF3jNR4T55IdOJa3+IFJKNYHtPvtXx8slmeMavj37vCzCfrqQM1vWLsYKLA== + dependencies: + "@gar/promisify" "^1.0.1" + semver "^7.3.5" + +"@npmcli/fs@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-3.1.0.tgz#233d43a25a91d68c3a863ba0da6a3f00924a173e" + integrity sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w== + dependencies: + semver "^7.3.5" + +"@npmcli/git@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.1.0.tgz" + integrity sha512-/hBFX/QG1b+N7PZBFs0bi+evgRZcK9nWBxQKZkGoXUT5hJSwl5c4d7y8/hm+NQZRPhQ67RzFaj5UM9YeyKoryw== + dependencies: + "@npmcli/promise-spawn" "^1.3.2" + lru-cache "^6.0.0" + mkdirp "^1.0.4" + npm-pick-manifest "^6.1.1" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.5" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + version "1.0.7" + resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +"@npmcli/move-file@^1.0.1": + version "1.1.2" + resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz" + integrity sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + +"@npmcli/node-gyp@^1.0.2": + version "1.0.3" + resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.3.tgz" + integrity sha512-fnkhw+fmX65kiLqk6E3BFLXNC26rUhK90zVwe2yncPliVT/Qos3xjhTLE59Df8KnPlcwIERXKVlU1bXoUQ+liA== + +"@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.6" + resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.6.tgz" + integrity sha512-e42bVZnC6VluBZBAFEr3YrdqSspG3bgilyg4nSLBJ7TRGNCzxHa92XAHxQBLYg0BmgwO4b2mf3h/l5EkEWRn3g== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + node-gyp "^7.1.0" + read-package-json-fast "^2.0.1" + +"@octokit/auth-token@^2.4.4": + version "2.5.0" + resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz" + integrity sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g== + dependencies: + "@octokit/types" "^6.0.3" + +"@octokit/core@^3.5.1": + version "3.5.1" + resolved "https://registry.npmjs.org/@octokit/core/-/core-3.5.1.tgz" + integrity sha512-omncwpLVxMP+GLpLPgeGJBF6IWJFjXDS5flY5VbppePYX9XehevbDykRH9PdCdvqt9TS5AOTiDide7h0qrkHjw== + dependencies: + "@octokit/auth-token" "^2.4.4" + "@octokit/graphql" "^4.5.8" + "@octokit/request" "^5.6.0" + "@octokit/request-error" "^2.0.5" + "@octokit/types" "^6.0.3" + before-after-hook "^2.2.0" + universal-user-agent "^6.0.0" + +"@octokit/endpoint@^6.0.1": + version "6.0.12" + resolved "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz" + integrity sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA== + dependencies: + "@octokit/types" "^6.0.3" + is-plain-object "^5.0.0" + universal-user-agent "^6.0.0" + +"@octokit/graphql@^4.5.8": + version "4.8.0" + resolved "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz" + integrity sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg== + dependencies: + "@octokit/request" "^5.6.0" + "@octokit/types" "^6.0.3" + universal-user-agent "^6.0.0" + +"@octokit/openapi-types@^11.2.0": + version "11.2.0" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-11.2.0.tgz" + integrity sha512-PBsVO+15KSlGmiI8QAzaqvsNlZlrDlyAJYcrXBCvVUxCp7VnXjkwPoFHgjEJXx3WF9BAwkA6nfCUA7i9sODzKA== + +"@octokit/plugin-enterprise-rest@^6.0.1": + version "6.0.1" + resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz" + integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== + +"@octokit/plugin-paginate-rest@^2.16.8": + version "2.17.0" + resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.17.0.tgz" + integrity sha512-tzMbrbnam2Mt4AhuyCHvpRkS0oZ5MvwwcQPYGtMv4tUa5kkzG58SVB0fcsLulOZQeRnOgdkZWkRUiyBlh0Bkyw== + dependencies: + "@octokit/types" "^6.34.0" + +"@octokit/plugin-request-log@^1.0.4": + version "1.0.4" + resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz" + integrity sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA== + +"@octokit/plugin-rest-endpoint-methods@^5.12.0": + version "5.13.0" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.13.0.tgz" + integrity sha512-uJjMTkN1KaOIgNtUPMtIXDOjx6dGYysdIFhgA52x4xSadQCz3b/zJexvITDVpANnfKPW/+E0xkOvLntqMYpviA== + dependencies: + "@octokit/types" "^6.34.0" + deprecation "^2.3.1" + +"@octokit/request-error@^2.0.5", "@octokit/request-error@^2.1.0": + version "2.1.0" + resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz" + integrity sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg== + dependencies: + "@octokit/types" "^6.0.3" + deprecation "^2.0.0" + once "^1.4.0" + +"@octokit/request@^5.6.0": + version "5.6.2" + resolved "https://registry.npmjs.org/@octokit/request/-/request-5.6.2.tgz" + integrity sha512-je66CvSEVf0jCpRISxkUcCa0UkxmFs6eGDRSbfJtAVwbLH5ceqF+YEyC8lj8ystKyZTy8adWr0qmkY52EfOeLA== + dependencies: + "@octokit/endpoint" "^6.0.1" + "@octokit/request-error" "^2.1.0" + "@octokit/types" "^6.16.1" + is-plain-object "^5.0.0" + node-fetch "^2.6.1" + universal-user-agent "^6.0.0" + +"@octokit/rest@^18.1.0": + version "18.12.0" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz" + integrity sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q== + dependencies: + "@octokit/core" "^3.5.1" + "@octokit/plugin-paginate-rest" "^2.16.8" + "@octokit/plugin-request-log" "^1.0.4" + "@octokit/plugin-rest-endpoint-methods" "^5.12.0" + +"@octokit/types@^6.0.3", "@octokit/types@^6.16.1", "@octokit/types@^6.34.0": + version "6.34.0" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.34.0.tgz" + integrity sha512-s1zLBjWhdEI2zwaoSgyOFoKSl109CUcVBCc7biPJ3aAf6LGLU6szDvi31JPU7bxfla2lqfhjbbg/5DdFNxOwHw== + dependencies: + "@octokit/openapi-types" "^11.2.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@playwright/test@^1.36.2": + version "1.36.2" + resolved "https://registry.npmjs.org/@playwright/test/-/test-1.36.2.tgz" + integrity sha512-2rVZeyPRjxfPH6J0oGJqE8YxiM1IBRyM8hyrXYK7eSiAqmbNhxwcLa7dZ7fy9Kj26V7FYia5fh9XJRq4Dqme+g== + dependencies: + "@types/node" "*" + playwright-core "1.36.2" + optionalDependencies: + fsevents "2.3.2" + +"@pmmmwh/react-refresh-webpack-plugin@^0.5.3": + version "0.5.4" + resolved "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.4.tgz" + integrity sha512-zZbZeHQDnoTlt2AF+diQT0wsSXpvWiaIOZwBRdltNFhG1+I3ozyaw7U/nBiUwyJ0D+zwdXp0E3bWOl38Ag2BMw== + dependencies: + ansi-html-community "^0.0.8" + common-path-prefix "^3.0.0" + core-js-pure "^3.8.1" + error-stack-parser "^2.0.6" + find-up "^5.0.0" + html-entities "^2.1.0" + loader-utils "^2.0.0" + schema-utils "^3.0.0" + source-map "^0.7.3" + +"@popperjs/core@^2.11.6": + version "2.11.6" + resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz" + integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== + +"@reactflow/background@11.3.12": + version "11.3.12" + resolved "https://registry.yarnpkg.com/@reactflow/background/-/background-11.3.12.tgz#9c9491cce4659bae13074fcdb48ac25664879d3f" + integrity sha512-jBuWVb43JQy5h4WOS7G0PU8voGTEJNA+qDmx8/jyBtrjbasTesLNfQvboTGjnQYYiJco6mw5vrtQItAJDNoIqw== + dependencies: + "@reactflow/core" "11.11.2" + classcat "^5.0.3" + zustand "^4.4.1" + +"@reactflow/controls@11.2.12": + version "11.2.12" + resolved "https://registry.yarnpkg.com/@reactflow/controls/-/controls-11.2.12.tgz#85e2aa5de17e2af28a5ecf6a75bb9c828a20640b" + integrity sha512-L9F3+avFRShoprdT+5oOijm5gVsz2rqWCXBzOAgD923L1XFGIspdiHLLf8IlPGsT+mfl0GxbptZhaEeEzl1e3g== + dependencies: + "@reactflow/core" "11.11.2" + classcat "^5.0.3" + zustand "^4.4.1" + +"@reactflow/core@11.11.2": + version "11.11.2" + resolved "https://registry.yarnpkg.com/@reactflow/core/-/core-11.11.2.tgz#c62f78297bda9d2e86a12228617ec3f91fbd4b22" + integrity sha512-+GfgyskweL1PsgRSguUwfrT2eDotlFgaKfDLm7x0brdzzPJY2qbCzVetaxedaiJmIli3817iYbILvE9qLKwbRA== + dependencies: + "@types/d3" "^7.4.0" + "@types/d3-drag" "^3.0.1" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.4.1" + +"@reactflow/minimap@11.7.12": + version "11.7.12" + resolved "https://registry.yarnpkg.com/@reactflow/minimap/-/minimap-11.7.12.tgz#6b2fc671ee17e37ccd3bc038ae8d2121d0ce6291" + integrity sha512-SRDU77c2PCF54PV/MQfkz7VOW46q7V1LZNOQlXAp7dkNyAOI6R+tb9qBUtUJOvILB+TCN6pRfD9fQ+2T99bW3Q== + dependencies: + "@reactflow/core" "11.11.2" + "@types/d3-selection" "^3.0.3" + "@types/d3-zoom" "^3.0.1" + classcat "^5.0.3" + d3-selection "^3.0.0" + d3-zoom "^3.0.0" + zustand "^4.4.1" + +"@reactflow/node-resizer@2.2.12": + version "2.2.12" + resolved "https://registry.yarnpkg.com/@reactflow/node-resizer/-/node-resizer-2.2.12.tgz#df82a7dfba883afea6a01a9c8210008a1ddba01f" + integrity sha512-6LHJGuI1zHyRrZHw5gGlVLIWnvVxid9WIqw8FMFSg+oF2DuS3pAPwSoZwypy7W22/gDNl9eD1Dcl/OtFtDFQ+w== + dependencies: + "@reactflow/core" "11.11.2" + classcat "^5.0.4" + d3-drag "^3.0.0" + d3-selection "^3.0.0" + zustand "^4.4.1" + +"@reactflow/node-toolbar@1.3.12": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@reactflow/node-toolbar/-/node-toolbar-1.3.12.tgz#89e7aa9d34b6213bb5e64c344d4e2e3cb7af3163" + integrity sha512-4kJRvNna/E3y2MZW9/80wTKwkhw4pLJiz3D5eQrD13XcmojSb1rArO9CiwyrI+rMvs5gn6NlCFB4iN1F+Q+lxQ== + dependencies: + "@reactflow/core" "11.11.2" + classcat "^5.0.3" + zustand "^4.4.1" + +"@rollup/plugin-babel@^5.2.0": + version "5.3.0" + resolved "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.0.tgz" + integrity sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^11.2.1": + version "11.2.1" + resolved "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-11.2.1.tgz" + integrity sha512-yc2n43jcqVyGE2sqV5/YCmocy9ArjVAP/BeXyTtADTBBX6V0e5UMqwO8CdQ0kzjb6zu5P1qMzsScCMRvE9OlVg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + "@types/resolve" "1.17.1" + builtin-modules "^3.1.0" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.19.0" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-3.1.0.tgz" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rollup/rollup-android-arm-eabi@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.0.tgz#57936f50d0335e2e7bfac496d209606fa516add4" + integrity sha512-jwXtxYbRt1V+CdQSy6Z+uZti7JF5irRKF8hlKfEnF/xJpcNGuuiZMBvuoYM+x9sr9iWGnzrlM0+9hvQ1kgkf1w== + +"@rollup/rollup-android-arm64@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.0.tgz#81bba83b37382a2d0e30ceced06c8d3d85138054" + integrity sha512-fI9nduZhCccjzlsA/OuAwtFGWocxA4gqXGTLvOyiF8d+8o0fZUeSztixkYjcGq1fGZY3Tkq4yRvHPFxU+jdZ9Q== + +"@rollup/rollup-darwin-arm64@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.0.tgz#a371bd723a5c4c4a33376da72abfc3938066842b" + integrity sha512-BcnSPRM76/cD2gQC+rQNGBN6GStBs2pl/FpweW8JYuz5J/IEa0Fr4AtrPv766DB/6b2MZ/AfSIOSGw3nEIP8SA== + +"@rollup/rollup-darwin-x64@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.0.tgz#8baf2fda277c9729125017c65651296282412886" + integrity sha512-LDyFB9GRolGN7XI6955aFeI3wCdCUszFWumWU0deHA8VpR3nWRrjG6GtGjBrQxQKFevnUTHKCfPR4IvrW3kCgQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.0.tgz#822830a8f7388d5b81d04c69415408d3bab1079b" + integrity sha512-ygrGVhQP47mRh0AAD0zl6QqCbNsf0eTo+vgwkY6LunBcg0f2Jv365GXlDUECIyoXp1kKwL5WW6rsO429DBY/bA== + +"@rollup/rollup-linux-arm64-gnu@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.0.tgz#e20fbe1bd4414c7119f9e0bba8ad17a6666c8365" + integrity sha512-x+uJ6MAYRlHGe9wi4HQjxpaKHPM3d3JjqqCkeC5gpnnI6OWovLdXTpfa8trjxPLnWKyBsSi5kne+146GAxFt4A== + +"@rollup/rollup-linux-arm64-musl@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.0.tgz#13f475596a62e1924f13fe1c8cf2c40e09a99b47" + integrity sha512-nrRw8ZTQKg6+Lttwqo6a2VxR9tOroa2m91XbdQ2sUUzHoedXlsyvY1fN4xWdqz8PKmf4orDwejxXHjh7YBGUCA== + +"@rollup/rollup-linux-powerpc64le-gnu@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.0.tgz#6a431c441420d1c510a205e08c6673355a0a2ea9" + integrity sha512-xV0d5jDb4aFu84XKr+lcUJ9y3qpIWhttO3Qev97z8DKLXR62LC3cXT/bMZXrjLF9X+P5oSmJTzAhqwUbY96PnA== + +"@rollup/rollup-linux-riscv64-gnu@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.0.tgz#53d9448962c3f9ed7a1672269655476ea2d67567" + integrity sha512-SDDhBQwZX6LPRoPYjAZWyL27LbcBo7WdBFWJi5PI9RPCzU8ijzkQn7tt8NXiXRiFMJCVpkuMkBf4OxSxVMizAw== + +"@rollup/rollup-linux-s390x-gnu@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.0.tgz#95f0c133b324da3e7e5c7d12855e0eb71d21a946" + integrity sha512-RxB/qez8zIDshNJDufYlTT0ZTVut5eCpAZ3bdXDU9yTxBzui3KhbGjROK2OYTTor7alM7XBhssgoO3CZ0XD3qA== + +"@rollup/rollup-linux-x64-gnu@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.0.tgz#820ada75c68ead1acc486e41238ca0d8f8531478" + integrity sha512-C6y6z2eCNCfhZxT9u+jAM2Fup89ZjiG5pIzZIDycs1IwESviLxwkQcFRGLjnDrP+PT+v5i4YFvlcfAs+LnreXg== + +"@rollup/rollup-linux-x64-musl@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.0.tgz#ca74f22e125efbe94c1148d989ef93329b464443" + integrity sha512-i0QwbHYfnOMYsBEyjxcwGu5SMIi9sImDVjDg087hpzXqhBSosxkE7gyIYFHgfFl4mr7RrXksIBZ4DoLoP4FhJg== + +"@rollup/rollup-win32-arm64-msvc@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.0.tgz#269023332297051d037a9593dcba92c10fef726b" + integrity sha512-Fq52EYb0riNHLBTAcL0cun+rRwyZ10S9vKzhGKKgeD+XbwunszSY0rVMco5KbOsTlwovP2rTOkiII/fQ4ih/zQ== + +"@rollup/rollup-win32-ia32-msvc@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.0.tgz#d7701438daf964011fd7ca33e3f13f3ff5129e7b" + integrity sha512-e/PBHxPdJ00O9p5Ui43+vixSgVf4NlLsmV6QneGERJ3lnjIua/kim6PRFe3iDueT1rQcgSkYP8ZBBXa/h4iPvw== + +"@rollup/rollup-win32-x64-msvc@4.14.0": + version "4.14.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.0.tgz#0bb7ac3cd1c3292db1f39afdabfd03ccea3a3d34" + integrity sha512-aGg7iToJjdklmxlUlJh/PaPNa4PmqHfyRMLunbL3eaMO0gp656+q1zOKkpJ/CVe9CryJv6tAN1HDoR8cNGzkag== + +"@rudderstack/rudder-sdk-node@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@rudderstack/rudder-sdk-node/-/rudder-sdk-node-1.1.2.tgz" + integrity sha512-cvcJ6mI6nSYnO73sTVh9kVEVwP1RMlKO3Q+eSbw3UNYorqOHxyhrNViqiOSc9AN1UNHsAKC0Q6DM2tizyb3yhg== + dependencies: + "@segment/loosely-validate-event" "^2.0.0" + axios "0.26.0" + axios-retry "^3.2.4" + bull "^4.7.0" + lodash.clonedeep "^4.5.0" + lodash.isstring "^4.0.1" + md5 "^2.3.0" + ms "^2.1.3" + remove-trailing-slash "^0.1.1" + serialize-javascript "^6.0.0" + uuid "^8.3.2" + winston "^3.6.0" + +"@rushstack/eslint-patch@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.1.0.tgz" + integrity sha512-JLo+Y592QzIE+q7Dl2pMUtt4q8SKYI5jDrZxrozEQxnGVOyYE+GWK9eLkwTaeN9DDctlaRAQ3TBmzZ1qdLE30A== + +"@segment/loosely-validate-event@^2.0.0": + version "2.0.0" + resolved "https://registry.npmjs.org/@segment/loosely-validate-event/-/loosely-validate-event-2.0.0.tgz" + integrity sha512-ZMCSfztDBqwotkl848ODgVcAmN4OItEWDCkshcKz0/W6gGSQayuuCtWV/MlodFivAZD793d6UgANd6wCXUfrIw== + dependencies: + component-type "^1.2.1" + join-component "^1.1.0" + +"@sentry/core@7.42.0": + version "7.42.0" + resolved "https://registry.npmjs.org/@sentry/core/-/core-7.42.0.tgz" + integrity sha512-vNcTyoQz5kUXo5vMGDyc5BJMO0UugPvMfYMQVxqt/BuDNR30LVhY+DL2tW1DFZDvRvyn5At+H7kSTj6GFrANXQ== + dependencies: + "@sentry/types" "7.42.0" + "@sentry/utils" "7.42.0" + tslib "^1.9.3" + +"@sentry/node@^7.42.0": + version "7.42.0" + resolved "https://registry.npmjs.org/@sentry/node/-/node-7.42.0.tgz" + integrity sha512-mmpVSDeoM5aEbKOMq3Wt54wAvH53bkivhRh3Ip+R7Uj3aOKkcVJST2XlbghHgoYQXTWz+pl475EVyODWgY9QYg== + dependencies: + "@sentry/core" "7.42.0" + "@sentry/types" "7.42.0" + "@sentry/utils" "7.42.0" + cookie "^0.4.1" + https-proxy-agent "^5.0.0" + lru_map "^0.3.3" + tslib "^1.9.3" + +"@sentry/tracing@^7.42.0": + version "7.42.0" + resolved "https://registry.npmjs.org/@sentry/tracing/-/tracing-7.42.0.tgz" + integrity sha512-0veGu3Ntweuj1pwWrJIFHmVdow4yufCreGIhsNDyrclwOjaTY3uI8iA6N62+hhtxOvqv+xueB98K1DvT5liPCQ== + dependencies: + "@sentry/core" "7.42.0" + "@sentry/types" "7.42.0" + "@sentry/utils" "7.42.0" + tslib "^1.9.3" + +"@sentry/types@7.42.0": + version "7.42.0" + resolved "https://registry.npmjs.org/@sentry/types/-/types-7.42.0.tgz" + integrity sha512-Ga0xaBIR/peuXQ88hI9a5TNY3GLNoH8jpsgPaAjAtRHkLsTx0y3AR+PrD7pUysza9QjvG+Qux01DRvLgaNKOHA== + +"@sentry/utils@7.42.0": + version "7.42.0" + resolved "https://registry.npmjs.org/@sentry/utils/-/utils-7.42.0.tgz" + integrity sha512-cBiDZVipC+is+IVgsTQLJyZWUZQxlLZ9GarNT+XZOZ5BFh0acFtz88hO6+S7vGmhcx2aCvsdC9yb2Yf+BphK6Q== + dependencies: + "@sentry/types" "7.42.0" + tslib "^1.9.3" + +"@sinclair/typebox@^0.27.8": + version "0.27.8" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" + integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== + +"@sindresorhus/is@^0.14.0": + version "0.14.0" + resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz" + integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== + +"@sinonjs/commons@^1.7.0": + version "1.8.3" + resolved "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz" + integrity sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ== + dependencies: + type-detect "4.0.8" + +"@sinonjs/fake-timers@^8.0.1": + version "8.1.0" + resolved "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz" + integrity sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg== + dependencies: + "@sinonjs/commons" "^1.7.0" + +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@svgr/babel-plugin-add-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz" + integrity sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg== + +"@svgr/babel-plugin-remove-jsx-attribute@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz" + integrity sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg== + +"@svgr/babel-plugin-remove-jsx-empty-expression@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz" + integrity sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA== + +"@svgr/babel-plugin-replace-jsx-attribute-value@^5.0.1": + version "5.0.1" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz" + integrity sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ== + +"@svgr/babel-plugin-svg-dynamic-title@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz" + integrity sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg== + +"@svgr/babel-plugin-svg-em-dimensions@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz" + integrity sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw== + +"@svgr/babel-plugin-transform-react-native-svg@^5.4.0": + version "5.4.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz" + integrity sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q== + +"@svgr/babel-plugin-transform-svg-component@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz" + integrity sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ== + +"@svgr/babel-preset@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz" + integrity sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig== + dependencies: + "@svgr/babel-plugin-add-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-attribute" "^5.4.0" + "@svgr/babel-plugin-remove-jsx-empty-expression" "^5.0.1" + "@svgr/babel-plugin-replace-jsx-attribute-value" "^5.0.1" + "@svgr/babel-plugin-svg-dynamic-title" "^5.4.0" + "@svgr/babel-plugin-svg-em-dimensions" "^5.4.0" + "@svgr/babel-plugin-transform-react-native-svg" "^5.4.0" + "@svgr/babel-plugin-transform-svg-component" "^5.5.0" + +"@svgr/core@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz" + integrity sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ== + dependencies: + "@svgr/plugin-jsx" "^5.5.0" + camelcase "^6.2.0" + cosmiconfig "^7.0.0" + +"@svgr/hast-util-to-babel-ast@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz" + integrity sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ== + dependencies: + "@babel/types" "^7.12.6" + +"@svgr/plugin-jsx@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz" + integrity sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA== + dependencies: + "@babel/core" "^7.12.3" + "@svgr/babel-preset" "^5.5.0" + "@svgr/hast-util-to-babel-ast" "^5.5.0" + svg-parser "^2.0.2" + +"@svgr/plugin-svgo@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz" + integrity sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ== + dependencies: + cosmiconfig "^7.0.0" + deepmerge "^4.2.2" + svgo "^1.2.2" + +"@svgr/webpack@^5.5.0": + version "5.5.0" + resolved "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz" + integrity sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g== + dependencies: + "@babel/core" "^7.12.3" + "@babel/plugin-transform-react-constant-elements" "^7.12.1" + "@babel/preset-env" "^7.12.1" + "@babel/preset-react" "^7.12.5" + "@svgr/core" "^5.5.0" + "@svgr/plugin-jsx" "^5.5.0" + "@svgr/plugin-svgo" "^5.5.0" + loader-utils "^2.0.0" + +"@szmarczak/http-timer@^1.1.2": + version "1.1.2" + resolved "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz" + integrity sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA== + dependencies: + defer-to-connect "^1.0.1" + +"@tanstack/eslint-plugin-query@^5.20.1": + version "5.20.1" + resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.20.1.tgz#861afedd7cde6b98c88cf86a5923bb659122e7af" + integrity sha512-oIp7Wh90KHOm1FKCvcv87fiD2H96xo/crFrlhbvqBzR2f0tMEGOK/ANKMGNFQprd6BT6lyZhQPlOEkFdezsjIg== + dependencies: + "@typescript-eslint/utils" "^6.20.0" + +"@tanstack/query-core@5.24.1": + version "5.24.1" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.24.1.tgz#d40928dec22b47df97fb2648e8c499772e8d7eb2" + integrity sha512-DZ6Nx9p7BhjkG50ayJ+MKPgff+lMeol7QYXkvuU5jr2ryW/4ok5eanaS9W5eooA4xN0A/GPHdLGOZGzArgf5Cg== + +"@tanstack/query-devtools@5.24.0": + version "5.24.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.24.0.tgz#b9b7828d42d5034415b1973ff4a154e880b17d59" + integrity sha512-pThim455t69zrZaQKa7IRkEIK8UBTS+gHVAdNfhO72Xh4rWpMc63ovRje5/n6iw63+d6QiJzVadsJVdPoodSeQ== + +"@tanstack/react-query-devtools@^5.24.1": + version "5.24.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.24.1.tgz#e3a8ea71115fb899119126e1507fc340ee9d9496" + integrity sha512-qa4SEugN+EF8JJXcpsM9Lu05HfUv5cvHvLuB0uw/81eJZyNHFdtHFBi5RLCgpBrOyVMDfH8UQ3VBMqXzFKV68A== + dependencies: + "@tanstack/query-devtools" "5.24.0" + +"@tanstack/react-query@^5.24.1": + version "5.24.1" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.24.1.tgz#bcb913febe0d813cec1fda7783298d07aa998b20" + integrity sha512-4+09JEdO4d6+Gc8Y/g2M/MuxDK5IY0QV8+2wL2304wPKJgJ54cBbULd3nciJ5uvh/as8rrxx6s0mtIwpRuGd1g== + dependencies: + "@tanstack/query-core" "5.24.1" + +"@testing-library/dom@^7.28.1": + version "7.31.2" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.31.2.tgz" + integrity sha512-3UqjCpey6HiTZT92vODYLPxTBWlM8ZOOjr3LX5F37/VRipW2M1kX6I/Cm4VXzteZqfGfagg8yXywpcOgQBlNsQ== + dependencies: + "@babel/code-frame" "^7.10.4" + "@babel/runtime" "^7.12.5" + "@types/aria-query" "^4.2.0" + aria-query "^4.2.2" + chalk "^4.1.0" + dom-accessibility-api "^0.5.6" + lz-string "^1.4.4" + pretty-format "^26.6.2" + +"@testing-library/jest-dom@^5.11.4": + version "5.16.1" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.1.tgz" + integrity sha512-ajUJdfDIuTCadB79ukO+0l8O+QwN0LiSxDaYUTI4LndbbUsGi6rWU1SCexXzBA2NSjlVB9/vbkasQIL3tmPBjw== + dependencies: + "@babel/runtime" "^7.9.2" + "@types/testing-library__jest-dom" "^5.9.1" + aria-query "^5.0.0" + chalk "^3.0.0" + css "^3.0.0" + css.escape "^1.5.1" + dom-accessibility-api "^0.5.6" + lodash "^4.17.15" + redent "^3.0.0" + +"@testing-library/react@^11.1.0": + version "11.2.7" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.7.tgz" + integrity sha512-tzRNp7pzd5QmbtXNG/mhdcl7Awfu/Iz1RaVHY75zTdOkmHCuzMhRL83gWHSgOAcjS3CCbyfwUHMZgRJb4kAfpA== + dependencies: + "@babel/runtime" "^7.12.5" + "@testing-library/dom" "^7.28.1" + +"@testing-library/user-event@^12.1.10": + version "12.8.3" + resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.8.3.tgz" + integrity sha512-IR0iWbFkgd56Bu5ZI/ej8yQwrkCv8Qydx6RzwbKz9faXazR/+5tvYKsZQgyXJiwgpcva127YO6JcWy7YlCfofQ== + dependencies: + "@babel/runtime" "^7.12.5" + +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + +"@trysound/sax@0.2.0": + version "0.2.0" + resolved "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz" + integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== + +"@types/aria-query@^4.2.0": + version "4.2.2" + resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz" + integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig== + +"@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": + version "7.1.18" + resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.18.tgz" + integrity sha512-S7unDjm/C7z2A2R9NzfKCK1I+BAALDtxEmsJBwlB3EzNfb929ykjL++1CK9LO++EIp2fQrC8O+BwjKvz6UeDyQ== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.4" + resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz" + integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.1" + resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz" + integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== + dependencies: + "@babel/parser" "^7.1.0" + "@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.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz" + integrity sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA== + dependencies: + "@babel/types" "^7.3.0" + +"@types/base16@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@types/base16/-/base16-1.0.2.tgz" + integrity sha512-oYO/U4VD1DavwrKuCSQWdLG+5K22SLPem2OQaHmFcQuwHoVeGC+JGVRji2MUqZUAIQZHEonOeVfAX09hYiLsdg== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.10" + resolved "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz" + integrity sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.3.5" + resolved "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz" + integrity sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/d3-array@*": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-3.2.1.tgz#1f6658e3d2006c4fceac53fde464166859f8b8c5" + integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg== + +"@types/d3-axis@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-axis/-/d3-axis-3.0.6.tgz#e760e5765b8188b1defa32bc8bb6062f81e4c795" + integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-brush@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-brush/-/d3-brush-3.0.6.tgz#c2f4362b045d472e1b186cdbec329ba52bdaee6c" + integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-chord@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-chord/-/d3-chord-3.0.6.tgz#1706ca40cf7ea59a0add8f4456efff8f8775793d" + integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg== + +"@types/d3-color@*": + version "3.1.3" + resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.3.tgz#368c961a18de721da8200e80bf3943fb53136af2" + integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A== + +"@types/d3-contour@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-contour/-/d3-contour-3.0.6.tgz#9ada3fa9c4d00e3a5093fed0356c7ab929604231" + integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg== + dependencies: + "@types/d3-array" "*" + "@types/geojson" "*" + +"@types/d3-delaunay@*": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz#185c1a80cc807fdda2a3fe960f7c11c4a27952e1" + integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw== + +"@types/d3-dispatch@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz#096efdf55eb97480e3f5621ff9a8da552f0961e7" + integrity sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ== + +"@types/d3-drag@*", "@types/d3-drag@^3.0.1": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-drag/-/d3-drag-3.0.7.tgz#b13aba8b2442b4068c9a9e6d1d82f8bcea77fc02" + integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-dsv@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz#0a351f996dc99b37f4fa58b492c2d1c04e3dac17" + integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g== + +"@types/d3-ease@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-ease/-/d3-ease-3.0.2.tgz#e28db1bfbfa617076f7770dd1d9a48eaa3b6c51b" + integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA== + +"@types/d3-fetch@*": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz#c04a2b4f23181aa376f30af0283dbc7b3b569980" + integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA== + dependencies: + "@types/d3-dsv" "*" + +"@types/d3-force@*": + version "3.0.9" + resolved "https://registry.yarnpkg.com/@types/d3-force/-/d3-force-3.0.9.tgz#dd96ccefba4386fe4ff36b8e4ee4e120c21fcf29" + integrity sha512-IKtvyFdb4Q0LWna6ymywQsEYjK/94SGhPrMfEr1TIc5OBeziTi+1jcCvttts8e0UWZIxpasjnQk9MNk/3iS+kA== + +"@types/d3-format@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-format/-/d3-format-3.0.4.tgz#b1e4465644ddb3fdf3a263febb240a6cd616de90" + integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g== + +"@types/d3-geo@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-geo/-/d3-geo-3.1.0.tgz#b9e56a079449174f0a2c8684a9a4df3f60522440" + integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ== + dependencies: + "@types/geojson" "*" + +"@types/d3-hierarchy@*": + version "3.1.7" + resolved "https://registry.yarnpkg.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz#6023fb3b2d463229f2d680f9ac4b47466f71f17b" + integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg== + +"@types/d3-interpolate@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz#412b90e84870285f2ff8a846c6eb60344f12a41c" + integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA== + dependencies: + "@types/d3-color" "*" + +"@types/d3-path@*": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@types/d3-path/-/d3-path-3.1.0.tgz#2b907adce762a78e98828f0b438eaca339ae410a" + integrity sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ== + +"@types/d3-polygon@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-polygon/-/d3-polygon-3.0.2.tgz#dfae54a6d35d19e76ac9565bcb32a8e54693189c" + integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA== + +"@types/d3-quadtree@*": + version "3.0.6" + resolved "https://registry.yarnpkg.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz#d4740b0fe35b1c58b66e1488f4e7ed02952f570f" + integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg== + +"@types/d3-random@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-random/-/d3-random-3.0.3.tgz#ed995c71ecb15e0cd31e22d9d5d23942e3300cfb" + integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ== + +"@types/d3-scale-chromatic@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.3.tgz#fc0db9c10e789c351f4c42d96f31f2e4df8f5644" + integrity sha512-laXM4+1o5ImZv3RpFAsTRn3TEkzqkytiOY0Dz0sq5cnd1dtNlk6sHLon4OvqaiJb28T0S/TdsBI3Sjsy+keJrw== + +"@types/d3-scale@*": + version "4.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-scale/-/d3-scale-4.0.8.tgz#d409b5f9dcf63074464bf8ddfb8ee5a1f95945bb" + integrity sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ== + dependencies: + "@types/d3-time" "*" + +"@types/d3-selection@*", "@types/d3-selection@^3.0.3": + version "3.0.10" + resolved "https://registry.yarnpkg.com/@types/d3-selection/-/d3-selection-3.0.10.tgz#98cdcf986d0986de6912b5892e7c015a95ca27fe" + integrity sha512-cuHoUgS/V3hLdjJOLTT691+G2QoqAjCVLmr4kJXR4ha56w1Zdu8UUQ5TxLRqudgNjwXeQxKMq4j+lyf9sWuslg== + +"@types/d3-shape@*": + version "3.1.6" + resolved "https://registry.yarnpkg.com/@types/d3-shape/-/d3-shape-3.1.6.tgz#65d40d5a548f0a023821773e39012805e6e31a72" + integrity sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA== + dependencies: + "@types/d3-path" "*" + +"@types/d3-time-format@*": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time-format/-/d3-time-format-4.0.3.tgz#d6bc1e6b6a7db69cccfbbdd4c34b70632d9e9db2" + integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg== + +"@types/d3-time@*": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/d3-time/-/d3-time-3.0.3.tgz#3c186bbd9d12b9d84253b6be6487ca56b54f88be" + integrity sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw== + +"@types/d3-timer@*": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/d3-timer/-/d3-timer-3.0.2.tgz#70bbda77dc23aa727413e22e214afa3f0e852f70" + integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw== + +"@types/d3-transition@*": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-transition/-/d3-transition-3.0.8.tgz#677707f5eed5b24c66a1918cde05963021351a8f" + integrity sha512-ew63aJfQ/ms7QQ4X7pk5NxQ9fZH/z+i24ZfJ6tJSfqxJMrYLiK01EAs2/Rtw/JreGUsS3pLPNV644qXFGnoZNQ== + dependencies: + "@types/d3-selection" "*" + +"@types/d3-zoom@*", "@types/d3-zoom@^3.0.1": + version "3.0.8" + resolved "https://registry.yarnpkg.com/@types/d3-zoom/-/d3-zoom-3.0.8.tgz#dccb32d1c56b1e1c6e0f1180d994896f038bc40b" + integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw== + dependencies: + "@types/d3-interpolate" "*" + "@types/d3-selection" "*" + +"@types/d3@^7.4.0": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@types/d3/-/d3-7.4.3.tgz#d4550a85d08f4978faf0a4c36b848c61eaac07e2" + integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww== + dependencies: + "@types/d3-array" "*" + "@types/d3-axis" "*" + "@types/d3-brush" "*" + "@types/d3-chord" "*" + "@types/d3-color" "*" + "@types/d3-contour" "*" + "@types/d3-delaunay" "*" + "@types/d3-dispatch" "*" + "@types/d3-drag" "*" + "@types/d3-dsv" "*" + "@types/d3-ease" "*" + "@types/d3-fetch" "*" + "@types/d3-force" "*" + "@types/d3-format" "*" + "@types/d3-geo" "*" + "@types/d3-hierarchy" "*" + "@types/d3-interpolate" "*" + "@types/d3-path" "*" + "@types/d3-polygon" "*" + "@types/d3-quadtree" "*" + "@types/d3-random" "*" + "@types/d3-scale" "*" + "@types/d3-scale-chromatic" "*" + "@types/d3-selection" "*" + "@types/d3-shape" "*" + "@types/d3-time" "*" + "@types/d3-time-format" "*" + "@types/d3-timer" "*" + "@types/d3-transition" "*" + "@types/d3-zoom" "*" + +"@types/debug@^4.1.7": + version "4.1.8" + resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.8.tgz" + integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== + dependencies: + "@types/ms" "*" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.2.2" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-8.2.2.tgz" + integrity sha512-nQxgB8/Sg+QKhnV8e0WzPpxjIGT3tuJDDzybkDi8ItE/IgTlHo07U0shaIjzhcvQxlq9SDRE42lsJ23uvEgJ2A== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/eslint@^7.28.2": + version "7.29.0" + resolved "https://registry.npmjs.org/@types/eslint/-/eslint-7.29.0.tgz" + integrity sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "0.0.50" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.50.tgz" + integrity sha512-C6N5s2ZFtuZRj54k2/zyRhNDjJwwcViAM3Nbm8zjBpbqAdZ00mr0CFxvSKeO8Y/e03WVFLpQMdHYVfUd6SB+Hw== + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/estree@1.0.5", "@types/estree@^1.0.0": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.18": + version "4.17.28" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz" + integrity sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.35" + resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz" + integrity sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*": + version "4.17.13" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz" + 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/express@^4.17.14": + version "4.17.17" + resolved "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/geojson@*": + version "7946.0.14" + resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" + integrity sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg== + +"@types/graceful-fs@^4.1.2": + version "4.1.5" + resolved "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz" + integrity sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw== + dependencies: + "@types/node" "*" + +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + +"@types/html-minifier-terser@^6.0.0": + version "6.1.0" + resolved "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" + integrity sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg== + +"@types/http-proxy@^1.17.5": + version "1.17.8" + resolved "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.8.tgz" + integrity sha512-5kPLG5BKpWYkw/LVOGWpiq3nEVqxiN32rTgI53Sk12/xHFQ2rG3ehI9IO+O3W2QoKeyB92dJkoka8SUm6BX1pA== + dependencies: + "@types/node" "*" + +"@types/is-hotkey@^0.1.1": + version "0.1.7" + resolved "https://registry.npmjs.org/@types/is-hotkey/-/is-hotkey-0.1.7.tgz" + integrity sha512-yB5C7zcOM7idwYZZ1wKQ3pTfjA9BbvFqRWvKB46GFddxnJtHwi/b9y84ykQtxQPg5qhdpg4Q/kWU3EGoCTmLzQ== + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": + version "2.0.4" + resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz" + integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^3.0.0": + version "3.0.1" + resolved "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz" + integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== + dependencies: + "@types/istanbul-lib-report" "*" + +"@types/jest@*": + version "27.4.0" + resolved "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz" + integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== + dependencies: + jest-diff "^27.0.0" + pretty-format "^27.0.0" + +"@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.9" + resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + +"@types/json-schema@^7.0.12": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/lodash@^4.14.149": + version "4.14.178" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz" + integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw== + +"@types/lodash@^4.14.175", "@types/lodash@^4.14.178": + version "4.14.181" + resolved "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.181.tgz" + integrity sha512-n3tyKthHJbkiWhDZs3DkhkCzt2MexYHXlX0td5iMplyfwketaOeKboEVBqzceH7juqvEg3q5oUoBFxSLu7zFag== + +"@types/mime@^1": + version "1.3.2" + resolved "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz" + integrity sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw== + +"@types/minimatch@^3.0.3": + version "3.0.5" + resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz" + integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ== + +"@types/minimist@^1.2.0": + version "1.2.2" + resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" + integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== + +"@types/ms@*": + version "0.7.31" + resolved "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz" + integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + +"@types/node@*": + version "17.0.10" + resolved "https://registry.npmjs.org/@types/node/-/node-17.0.10.tgz" + integrity sha512-S/3xB4KzyFxYGCppyDt68yzBU9ysL88lSdIah4D6cptdcltc4NCPCAMc0+PCpg/lLIyC7IPvj2Z52OJWeIUkog== + +"@types/node@^17.0.5": + version "17.0.45" + resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz" + integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw== + +"@types/normalize-package-data@^2.4.0": + version "2.4.1" + resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz" + integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + +"@types/parse-json@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz" + integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== + +"@types/passport-strategy@^0.2.35": + version "0.2.35" + resolved "https://registry.npmjs.org/@types/passport-strategy/-/passport-strategy-0.2.35.tgz" + integrity sha512-o5D19Jy2XPFoX2rKApykY15et3Apgax00RRLf0RUotPDUsYrQa7x4howLYr9El2mlUApHmCMv5CZ1IXqKFQ2+g== + dependencies: + "@types/express" "*" + "@types/passport" "*" + +"@types/passport@*", "@types/passport@^1.0.11": + version "1.0.12" + resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.12.tgz" + integrity sha512-QFdJ2TiAEoXfEQSNDISJR1Tm51I78CymqcBa8imbjo6dNNu+l2huDxxbDEIoFIwOSKMkOfHEikyDuZ38WwWsmw== + dependencies: + "@types/express" "*" + +"@types/prettier@^2.1.5": + version "2.4.3" + resolved "https://registry.npmjs.org/@types/prettier/-/prettier-2.4.3.tgz" + integrity sha512-QzSuZMBuG5u8HqYz01qtMdg/Jfctlnvj1z/lYnIDXs/golxw0fxtRAHd9KrzjR7Yxz1qVeI00o0kiO3PmVdJ9w== + +"@types/prop-types@*", "@types/prop-types@^15.7.4": + version "15.7.4" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + +"@types/prop-types@^15.7.5": + version "15.7.5" + resolved "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz" + integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== + +"@types/q@^1.5.1": + version "1.5.5" + resolved "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz" + integrity sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + +"@types/react-is@^16.7.1 || ^17.0.0": + version "17.0.3" + resolved "https://registry.npmjs.org/@types/react-is/-/react-is-17.0.3.tgz" + integrity sha512-aBTIWg1emtu95bLTLx0cpkxwGW3ueZv71nE2YFBpL8k/z5czEW8yYpOo8Dp+UUAFAtKwNaOsh/ioSeQnWlZcfw== + dependencies: + "@types/react" "*" + +"@types/react-transition-group@^4.4.5": + version "4.4.5" + resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-juKD/eiSM3/xZYzjuzH6ZwpP+/lejltmiS3QEzV/vmb/Q8+HfDmxu+Baga8UEMGBqV88Nbg4l2hY/K2DkyaLLA== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@16 || 17": + version "17.0.38" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.38.tgz" + integrity sha512-SI92X1IA+FMnP3qM5m4QReluXzhcmovhZnLNm3pyeQlooi02qI7sLiepEYqT678uNiyc25XfCqxREFpy3W7YhQ== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + +"@types/resolve@1.17.1": + version "1.17.1" + resolved "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz" + integrity sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw== + dependencies: + "@types/node" "*" + +"@types/retry@^0.12.0": + version "0.12.1" + resolved "https://registry.npmjs.org/@types/retry/-/retry-0.12.1.tgz" + integrity sha512-xoDlM2S4ortawSWORYqsdU+2rxdh4LRW9ytc3zmT37RIKQh6IHyKwwtKhKis9ah8ol07DCkZxPt8BBvPjC6v4g== + +"@types/sax@^1.2.1": + version "1.2.4" + resolved "https://registry.npmjs.org/@types/sax/-/sax-1.2.4.tgz" + integrity sha512-pSAff4IAxJjfAXUG6tFkO7dsSbTmf8CtUpfhhZ5VhkRpC4628tJhh3+V6H1E+/Gs9piSzYKT5yzHO5M4GG9jkw== + dependencies: + "@types/node" "*" + +"@types/scheduler@*": + version "0.16.2" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz" + integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== + +"@types/semver@^7.5.0": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== + +"@types/send@*": + version "0.17.1" + resolved "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz" + integrity sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.1" + resolved "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz" + integrity sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg== + dependencies: + "@types/express" "*" + +"@types/serve-static@*": + version "1.13.10" + resolved "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz" + integrity sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/sockjs@^0.3.33": + version "0.3.33" + resolved "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz" + integrity sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw== + dependencies: + "@types/node" "*" + +"@types/stack-utils@^2.0.0": + version "2.0.1" + resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz" + integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== + +"@types/testing-library__jest-dom@^5.9.1": + version "5.14.2" + resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.2.tgz" + integrity sha512-vehbtyHUShPxIa9SioxDwCvgxukDMH//icJG90sXQBUm5lJOHLT5kNeU9tnivhnA/TkOFMzGIXN2cTc4hY8/kg== + dependencies: + "@types/jest" "*" + +"@types/trusted-types@^2.0.2": + version "2.0.2" + resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + +"@types/web-bluetooth@^0.0.15": + version "0.0.15" + resolved "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.15.tgz" + integrity sha512-w7hEHXnPMEZ+4nGKl/KDRVpxkwYxYExuHOYXyzIzCDzEZ9ZCGMAewulr9IqJu2LR4N37fcnb1XVeuZ09qgOxhA== + +"@types/ws@^8.2.2": + version "8.2.2" + resolved "https://registry.npmjs.org/@types/ws/-/ws-8.2.2.tgz" + integrity sha512-NOn5eIcgWLOo6qW8AcuLZ7G8PycXu0xTxxkS6Q18VWFxgPUSOwV0pBj2a/4viNZVu25i7RIB7GttdkAIUUXOOg== + dependencies: + "@types/node" "*" + +"@types/xml-crypto@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@types/xml-crypto/-/xml-crypto-1.4.2.tgz" + integrity sha512-1kT+3gVkeBDg7Ih8NefxGYfCApwZViMIs5IEs5AXF6Fpsrnf9CLAEIRh0DYb1mIcRcvysVbe27cHsJD6rJi36w== + dependencies: + "@types/node" "*" + xpath "0.0.27" + +"@types/xml-encryption@^1.2.1": + version "1.2.1" + resolved "https://registry.npmjs.org/@types/xml-encryption/-/xml-encryption-1.2.1.tgz" + integrity sha512-UeyZkfZFZSa9XCGU5uGgUmsSLwQESDJvF076bJGyDf2gkXJjKvK8fW/x4ckvEHB2M/5RHJEkMc5xI+JrdmCTKA== + dependencies: + "@types/node" "*" + +"@types/xml2js@^0.4.11": + version "0.4.11" + resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.11.tgz" + integrity sha512-JdigeAKmCyoJUiQljjr7tQG3if9NkqGUgwEUqBvV0N7LM4HyQk7UXCnusRa1lnvXAEYJ8mw8GtZWioagNztOwA== + dependencies: + "@types/node" "*" + +"@types/yargs-parser@*": + version "20.2.1" + resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz" + integrity sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw== + +"@types/yargs@^15.0.0": + version "15.0.14" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz" + integrity sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ== + dependencies: + "@types/yargs-parser" "*" + +"@types/yargs@^16.0.0": + version "16.0.4" + resolved "https://registry.npmjs.org/@types/yargs/-/yargs-16.0.4.tgz" + integrity sha512-T8Yc9wt/5LbJyCaLiHPReJa0kApcIgJ7Bn735GjItUfh08Z1pJvu8QZqb9s+mMvKV6WUQRV7K2R46YbjMXTTJw== + dependencies: + "@types/yargs-parser" "*" + +"@types/yup@0.29.11": + version "0.29.11" + resolved "https://registry.npmjs.org/@types/yup/-/yup-0.29.11.tgz" + integrity sha512-9cwk3c87qQKZrT251EDoibiYRILjCmxBvvcb4meofCmx1vdnNcR9gyildy5vOHASpOKMsn42CugxUvcwK5eu1g== + +"@types/zen-observable@0.8.3": + version "0.8.3" + resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.3.tgz" + integrity sha512-fbF6oTd4sGGy0xjHPKAt+eS2CrxJ3+6gQ3FGcBoIJR2TLAyCkCyI8JqZNy+FeON0AhVgNJoUumVoZQjBFUqHkw== + +"@typescript-eslint/eslint-plugin@^5.5.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.10.0.tgz" + integrity sha512-XXVKnMsq2fuu9K2KsIxPUGqb6xAImz8MEChClbXmE3VbveFtBUU5bzM6IPVWqzyADIgdkS2Ws/6Xo7W2TeZWjQ== + dependencies: + "@typescript-eslint/scope-manager" "5.10.0" + "@typescript-eslint/type-utils" "5.10.0" + "@typescript-eslint/utils" "5.10.0" + debug "^4.3.2" + functional-red-black-tree "^1.0.1" + ignore "^5.1.8" + regexpp "^3.2.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/experimental-utils@^5.0.0", "@typescript-eslint/experimental-utils@^5.9.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-5.10.0.tgz" + integrity sha512-GeQAPqQMI5DVMGOUwGbSR+NdsirryyKOgUFRTWInhlsKUArns/MVnXmPpzxfrzB1nU36cT5WJAwmfCsjoaVBWg== + dependencies: + "@typescript-eslint/utils" "5.10.0" + +"@typescript-eslint/parser@^5.5.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.10.0.tgz" + integrity sha512-pJB2CCeHWtwOAeIxv8CHVGJhI5FNyJAIpx5Pt72YkK3QfEzt6qAlXZuyaBmyfOdM62qU0rbxJzNToPTVeJGrQw== + dependencies: + "@typescript-eslint/scope-manager" "5.10.0" + "@typescript-eslint/types" "5.10.0" + "@typescript-eslint/typescript-estree" "5.10.0" + debug "^4.3.2" + +"@typescript-eslint/scope-manager@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.10.0.tgz" + integrity sha512-tgNgUgb4MhqK6DoKn3RBhyZ9aJga7EQrw+2/OiDk5hKf3pTVZWyqBi7ukP+Z0iEEDMF5FDa64LqODzlfE4O/Dg== + dependencies: + "@typescript-eslint/types" "5.10.0" + "@typescript-eslint/visitor-keys" "5.10.0" + +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/type-utils@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.10.0.tgz" + integrity sha512-TzlyTmufJO5V886N+hTJBGIfnjQDQ32rJYxPaeiyWKdjsv2Ld5l8cbS7pxim4DeNs62fKzRSt8Q14Evs4JnZyQ== + dependencies: + "@typescript-eslint/utils" "5.10.0" + debug "^4.3.2" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.10.0.tgz" + integrity sha512-wUljCgkqHsMZbw60IbOqT/puLfyqqD5PquGiBo1u1IS3PLxdi3RDGlyf032IJyh+eQoGhz9kzhtZa+VC4eWTlQ== + +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/typescript-estree@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.10.0.tgz" + integrity sha512-x+7e5IqfwLwsxTdliHRtlIYkgdtYXzE0CkFeV6ytAqq431ZyxCFzNMNR5sr3WOlIG/ihVZr9K/y71VHTF/DUQA== + dependencies: + "@typescript-eslint/types" "5.10.0" + "@typescript-eslint/visitor-keys" "5.10.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/utils@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.10.0.tgz" + integrity sha512-IGYwlt1CVcFoE2ueW4/ioEwybR60RAdGeiJX/iDAw0t5w0wK3S7QncDwpmsM70nKgGTuVchEWB8lwZwHqPAWRg== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.10.0" + "@typescript-eslint/types" "5.10.0" + "@typescript-eslint/typescript-estree" "5.10.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/utils@^6.20.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134" + integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@types/json-schema" "^7.0.12" + "@types/semver" "^7.5.0" + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + semver "^7.5.4" + +"@typescript-eslint/visitor-keys@5.10.0": + version "5.10.0" + resolved "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.10.0.tgz" + integrity sha512-GMxj0K1uyrFLPKASLmZzCuSddmjZVbVj3Ouy5QVuIGKZopxvOr24JsS7gruz6C3GExE01mublZ3mIBOaon9zuQ== + dependencies: + "@typescript-eslint/types" "5.10.0" + eslint-visitor-keys "^3.0.0" + +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@ucast/core@^1.0.0", "@ucast/core@^1.4.1", "@ucast/core@^1.6.1": + version "1.10.2" + resolved "https://registry.npmjs.org/@ucast/core/-/core-1.10.2.tgz" + integrity sha512-ons5CwXZ/51wrUPfoduC+cO7AS1/wRb0ybpQJ9RrssossDxVy4t49QxWoWgfBDvVKsz9VXzBk9z0wqTdZ+Cq8g== + +"@ucast/js@^3.0.0": + version "3.0.3" + resolved "https://registry.npmjs.org/@ucast/js/-/js-3.0.3.tgz" + integrity sha512-jBBqt57T5WagkAjqfCIIE5UYVdaXYgGkOFYv2+kjq2AVpZ2RIbwCo/TujJpDlwTVluUI+WpnRpoGU2tSGlEvFQ== + dependencies: + "@ucast/core" "^1.0.0" + +"@ucast/mongo2js@^1.3.0": + version "1.3.4" + resolved "https://registry.npmjs.org/@ucast/mongo2js/-/mongo2js-1.3.4.tgz" + integrity sha512-ahazOr1HtelA5AC1KZ9x0UwPMqqimvfmtSm/PRRSeKKeE5G2SCqTgwiNzO7i9jS8zA3dzXpKVPpXMkcYLnyItA== + dependencies: + "@ucast/core" "^1.6.1" + "@ucast/js" "^3.0.0" + "@ucast/mongo" "^2.4.0" + +"@ucast/mongo@^2.4.0": + version "2.4.3" + resolved "https://registry.npmjs.org/@ucast/mongo/-/mongo-2.4.3.tgz" + integrity sha512-XcI8LclrHWP83H+7H2anGCEeDq0n+12FU2mXCTz6/Tva9/9ddK/iacvvhCyW6cijAAOILmt0tWplRyRhVyZLsA== + dependencies: + "@ucast/core" "^1.4.1" + +"@vitejs/plugin-vue@^3.1.2": + version "3.1.2" + resolved "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-3.1.2.tgz" + integrity sha512-3zxKNlvA3oNaKDYX0NBclgxTQ1xaFdL7PzwF6zj9tGFziKwmBa3Q/6XcJQxudlT81WxDjEhHmevvIC4Orc1LhQ== + +"@vitest/expect@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-1.1.3.tgz#9667719dffa82e7350dcca7b95f9ec30426d037e" + integrity sha512-MnJqsKc1Ko04lksF9XoRJza0bGGwTtqfbyrsYv5on4rcEkdo+QgUdITenBQBUltKzdxW7K3rWh+nXRULwsdaVg== + dependencies: + "@vitest/spy" "1.1.3" + "@vitest/utils" "1.1.3" + chai "^4.3.10" + +"@vitest/runner@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-1.1.3.tgz#c71e0ab6aad0a6a75c804e060c295852dc052beb" + integrity sha512-Va2XbWMnhSdDEh/OFxyUltgQuuDRxnarK1hW5QNN4URpQrqq6jtt8cfww/pQQ4i0LjoYxh/3bYWvDFlR9tU73g== + dependencies: + "@vitest/utils" "1.1.3" + p-limit "^5.0.0" + pathe "^1.1.1" + +"@vitest/snapshot@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-1.1.3.tgz#94f321f80c9fb9e10b83dabb83a0d09f034a74b0" + integrity sha512-U0r8pRXsLAdxSVAyGNcqOU2H3Z4Y2dAAGGelL50O0QRMdi1WWeYHdrH/QWpN1e8juWfVKsb8B+pyJwTC+4Gy9w== + dependencies: + magic-string "^0.30.5" + pathe "^1.1.1" + pretty-format "^29.7.0" + +"@vitest/spy@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-1.1.3.tgz#244e4e049cd0a5b126a475af327df8b7ffa6b3b5" + integrity sha512-Ec0qWyGS5LhATFQtldvChPTAHv08yHIOZfiNcjwRQbFPHpkih0md9KAbs7TfeIfL7OFKoe7B/6ukBTqByubXkQ== + dependencies: + tinyspy "^2.2.0" + +"@vitest/utils@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-1.1.3.tgz#1f82122f916b0b6feb5e85fc854cfa1fbd522b55" + integrity sha512-Dyt3UMcdElTll2H75vhxfpZu03uFpXRCHxWnzcrFjZxT1kTbq8ALUYIeBgGolo1gldVdI0YSlQRacsqxTwNqwg== + dependencies: + diff-sequences "^29.6.3" + estree-walker "^3.0.3" + loupe "^2.3.7" + pretty-format "^29.7.0" + +"@vue/compiler-core@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.37.tgz" + integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + source-map "^0.6.1" + +"@vue/compiler-core@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.2.41.tgz" + integrity sha512-oA4mH6SA78DT+96/nsi4p9DX97PHcNROxs51lYk7gb9Z4BPKQ3Mh+BLn6CQZBw857Iuhu28BfMSRHAlPvD4vlw== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/shared" "3.2.41" + estree-walker "^2.0.2" + source-map "^0.6.1" + +"@vue/compiler-dom@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz" + integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ== + dependencies: + "@vue/compiler-core" "3.2.37" + "@vue/shared" "3.2.37" + +"@vue/compiler-dom@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.2.41.tgz" + integrity sha512-xe5TbbIsonjENxJsYRbDJvthzqxLNk+tb3d/c47zgREDa/PCp6/Y4gC/skM4H6PIuX5DAxm7fFJdbjjUH2QTMw== + dependencies: + "@vue/compiler-core" "3.2.41" + "@vue/shared" "3.2.41" + +"@vue/compiler-sfc@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz" + integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.37" + "@vue/compiler-dom" "3.2.37" + "@vue/compiler-ssr" "3.2.37" + "@vue/reactivity-transform" "3.2.37" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + +"@vue/compiler-sfc@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.2.41.tgz" + integrity sha512-+1P2m5kxOeaxVmJNXnBskAn3BenbTmbxBxWOtBq3mQTCokIreuMULFantBUclP0+KnzNCMOvcnKinqQZmiOF8w== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.41" + "@vue/compiler-dom" "3.2.41" + "@vue/compiler-ssr" "3.2.41" + "@vue/reactivity-transform" "3.2.41" + "@vue/shared" "3.2.41" + estree-walker "^2.0.2" + magic-string "^0.25.7" + postcss "^8.1.10" + source-map "^0.6.1" + +"@vue/compiler-ssr@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz" + integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw== + dependencies: + "@vue/compiler-dom" "3.2.37" + "@vue/shared" "3.2.37" + +"@vue/compiler-ssr@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.2.41.tgz" + integrity sha512-Y5wPiNIiaMz/sps8+DmhaKfDm1xgj6GrH99z4gq2LQenfVQcYXmHIOBcs5qPwl7jaW3SUQWjkAPKMfQemEQZwQ== + dependencies: + "@vue/compiler-dom" "3.2.41" + "@vue/shared" "3.2.41" + +"@vue/devtools-api@^6.4.4": + version "6.4.4" + resolved "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.4.4.tgz" + integrity sha512-Ku31WzpOV/8cruFaXaEZKF81WkNnvCSlBY4eOGtz5WMSdJvX1v1WWlSMGZeqUwPtQ27ZZz7B62erEMq8JDjcXw== + +"@vue/reactivity-transform@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz" + integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.37" + "@vue/shared" "3.2.37" + estree-walker "^2.0.2" + magic-string "^0.25.7" + +"@vue/reactivity-transform@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/reactivity-transform/-/reactivity-transform-3.2.41.tgz" + integrity sha512-mK5+BNMsL4hHi+IR3Ft/ho6Za+L3FA5j8WvreJ7XzHrqkPq8jtF/SMo7tuc9gHjLDwKZX1nP1JQOKo9IEAn54A== + dependencies: + "@babel/parser" "^7.16.4" + "@vue/compiler-core" "3.2.41" + "@vue/shared" "3.2.41" + estree-walker "^2.0.2" + magic-string "^0.25.7" + +"@vue/reactivity@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.37.tgz" + integrity sha512-/7WRafBOshOc6m3F7plwzPeCu/RCVv9uMpOwa/5PiY1Zz+WLVRWiy0MYKwmg19KBdGtFWsmZ4cD+LOdVPcs52A== + dependencies: + "@vue/shared" "3.2.37" + +"@vue/reactivity@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.2.41.tgz" + integrity sha512-9JvCnlj8uc5xRiQGZ28MKGjuCoPhhTwcoAdv3o31+cfGgonwdPNuvqAXLhlzu4zwqavFEG5tvaoINQEfxz+l6g== + dependencies: + "@vue/shared" "3.2.41" + +"@vue/runtime-core@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.37.tgz" + integrity sha512-JPcd9kFyEdXLl/i0ClS7lwgcs0QpUAWj+SKX2ZC3ANKi1U4DOtiEr6cRqFXsPwY5u1L9fAjkinIdB8Rz3FoYNQ== + dependencies: + "@vue/reactivity" "3.2.37" + "@vue/shared" "3.2.37" + +"@vue/runtime-core@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.2.41.tgz" + integrity sha512-0LBBRwqnI0p4FgIkO9q2aJBBTKDSjzhnxrxHYengkAF6dMOjeAIZFDADAlcf2h3GDALWnblbeprYYpItiulSVQ== + dependencies: + "@vue/reactivity" "3.2.41" + "@vue/shared" "3.2.41" + +"@vue/runtime-dom@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.37.tgz" + integrity sha512-HimKdh9BepShW6YozwRKAYjYQWg9mQn63RGEiSswMbW+ssIht1MILYlVGkAGGQbkhSh31PCdoUcfiu4apXJoPw== + dependencies: + "@vue/runtime-core" "3.2.37" + "@vue/shared" "3.2.37" + csstype "^2.6.8" + +"@vue/runtime-dom@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.2.41.tgz" + integrity sha512-U7zYuR1NVIP8BL6jmOqmapRAHovEFp7CSw4pR2FacqewXNGqZaRfHoNLQsqQvVQ8yuZNZtxSZy0FFyC70YXPpA== + dependencies: + "@vue/runtime-core" "3.2.41" + "@vue/shared" "3.2.41" + csstype "^2.6.8" + +"@vue/server-renderer@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.37.tgz" + integrity sha512-kLITEJvaYgZQ2h47hIzPh2K3jG8c1zCVbp/o/bzQOyvzaKiCquKS7AaioPI28GNxIsE/zSx+EwWYsNxDCX95MA== + dependencies: + "@vue/compiler-ssr" "3.2.37" + "@vue/shared" "3.2.37" + +"@vue/server-renderer@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.2.41.tgz" + integrity sha512-7YHLkfJdTlsZTV0ae5sPwl9Gn/EGr2hrlbcS/8naXm2CDpnKUwC68i1wGlrYAfIgYWL7vUZwk2GkYLQH5CvFig== + dependencies: + "@vue/compiler-ssr" "3.2.41" + "@vue/shared" "3.2.41" + +"@vue/shared@3.2.37": + version "3.2.37" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.37.tgz" + integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw== + +"@vue/shared@3.2.41": + version "3.2.41" + resolved "https://registry.npmjs.org/@vue/shared/-/shared-3.2.41.tgz" + integrity sha512-W9mfWLHmJhkfAmV+7gDjcHeAWALQtgGT3JErxULl0oz6R6+3ug91I7IErs93eCFhPCZPHBs4QJS7YWEV7A3sxw== + +"@vueuse/core@^9.3.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@vueuse/core/-/core-9.3.0.tgz" + integrity sha512-64Rna8IQDWpdrJxgitDg7yv1yTp41ZmvV8zlLEylK4QQLWAhz1OFGZDPZ8bU4lwcGgbEJ2sGi2jrdNh4LttUSQ== + dependencies: + "@types/web-bluetooth" "^0.0.15" + "@vueuse/metadata" "9.3.0" + "@vueuse/shared" "9.3.0" + vue-demi "*" + +"@vueuse/metadata@9.3.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.3.0.tgz" + integrity sha512-GnnfjbzIPJIh9ngL9s9oGU1+Hx/h5/KFqTfJykzh/1xjaHkedV9g0MASpdmPZIP+ynNhKAcEfA6g5i8KXwtoMA== + +"@vueuse/shared@9.3.0": + version "9.3.0" + resolved "https://registry.npmjs.org/@vueuse/shared/-/shared-9.3.0.tgz" + integrity sha512-caGUWLY0DpPC6l31KxeUy6vPVNA0yKxx81jFYLoMpyP6cF84FG5Dkf69DfSUqL57wX8JcUkJDMnQaQIZPWFEQQ== + dependencies: + vue-demi "*" + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@wry/context@^0.6.0": + version "0.6.1" + resolved "https://registry.npmjs.org/@wry/context/-/context-0.6.1.tgz" + integrity sha512-LOmVnY1iTU2D8tv4Xf6MVMZZ+juIJ87Kt/plMijjN20NMAXGmH4u8bS1t0uT74cZ5gwpocYueV58YwyI8y+GKw== + dependencies: + tslib "^2.3.0" + +"@wry/equality@^0.5.0": + version "0.5.2" + resolved "https://registry.npmjs.org/@wry/equality/-/equality-0.5.2.tgz" + integrity sha512-oVMxbUXL48EV/C0/M7gLVsoK6qRHPS85x8zECofEZOVvxGmIPLA9o5Z27cc2PoAyZz1S2VoM2A7FLAnpfGlneA== + dependencies: + tslib "^2.3.0" + +"@wry/trie@^0.3.0": + version "0.3.1" + resolved "https://registry.npmjs.org/@wry/trie/-/trie-0.3.1.tgz" + integrity sha512-WwB53ikYudh9pIorgxrkHKrQZcCqNM/Q/bDzZBffEaGUKGuHrRb3zZUT9Sh2qw9yogC7SsdRmQ1ER0pqvd3bfw== + dependencies: + tslib "^2.3.0" + +"@xmldom/xmldom@0.8.7": + version "0.8.7" + resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.7.tgz" + integrity sha512-sI1Ly2cODlWStkINzqGrZ8K6n+MTSbAeQnAipGyL+KZCXuHaRlj2gyyy8B/9MvsFFqN7XHryQnB2QwhzvJXovg== + +"@xmldom/xmldom@^0.8.5", "@xmldom/xmldom@^0.8.6": + version "0.8.8" + resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.8.tgz" + integrity sha512-0LNz4EY8B/8xXY86wMrQ4tz6zEHZv9ehFMJPm8u2gq5lQ71cfRKdaKyxfJAx5aUoyzx0qzgURblTisPGgz3d+Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +JSONStream@^1.0.4: + version "1.3.5" + resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz" + integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== + dependencies: + jsonparse "^1.2.0" + through ">=2.2.7 <3" + +abab@^2.0.3, abab@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz" + integrity sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q== + +abbrev@1: + version "1.1.1" + resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +abbrev@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-2.0.0.tgz#cf59829b8b4f03f89dda2771cb7f3653828c89bf" + integrity sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ== + +accepts@^1.3.7, accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +accounting@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/accounting/-/accounting-0.4.1.tgz" + integrity sha512-RU6KY9Y5wllyaCNBo1W11ZOTnTHMMgOZkIwdOOs6W5ibMTp72i4xIbEA48djxVGqMNTUNbvrP/1nWg5Af5m2gQ== + +acorn-globals@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz" + integrity sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg== + dependencies: + acorn "^7.1.1" + acorn-walk "^7.1.1" + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn-node@^1.6.1: + version "1.8.2" + resolved "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz" + integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== + dependencies: + acorn "^7.0.0" + acorn-walk "^7.0.0" + xtend "^4.0.2" + +acorn-walk@^7.0.0, acorn-walk@^7.1.1: + version "7.2.0" + resolved "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz" + integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== + +acorn-walk@^8.3.1: + version "8.3.2" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.2.tgz#7703af9415f1b6db9315d6895503862e231d34aa" + integrity sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A== + +acorn@^7.0.0, acorn@^7.1.1: + version "7.4.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz" + integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== + +acorn@^8.10.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +acorn@^8.2.4, acorn@^8.5.0, acorn@^8.7.0: + version "8.7.1" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz" + integrity sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A== + +acorn@^8.7.1: + version "8.8.2" + resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" + integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + +address@^1.0.1, address@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz" + integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== + +adjust-sourcemap-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz" + integrity sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A== + dependencies: + loader-utils "^2.0.0" + regex-parser "^2.2.11" + +agent-base@6, agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + +agent-base@^7.0.2, agent-base@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz" + integrity sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg== + dependencies: + debug "^4.3.4" + +agent-base@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-7.1.1.tgz#bdbded7dfb096b751a2a087eeeb9664725b2e317" + integrity sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA== + dependencies: + debug "^4.3.4" + +agentkeepalive@^4.1.3: + version "4.2.0" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.0.tgz" + integrity sha512-0PhAp58jZNw13UJv7NVdTGb0ZcghHUb3DrZ046JiiJY/BOaTTpbwdHq2VObPCBV8M2GPh7sgrJ3AQ8Ey468LJw== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.4.1, ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.10.0, ajv@^6.12.2, ajv@^6.12.3, ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.6.0, ajv@^8.8.0: + version "8.9.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.9.0.tgz" + integrity sha512-qOKJyNj/h+OWx7s5DePL6Zu1KeM9jPZhwBqs+7DzP6bGOvqzVCSf0xueYmVuaC/oQ/VtS2zLMLHdQFbkka+XDQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +ajv@^8.6.2: + version "8.10.0" + resolved "https://registry.npmjs.org/ajv/-/ajv-8.10.0.tgz" + integrity sha512-bzqAEZOjkrUMl2afH8dknrq5KEk2SrwdBROR+vH1EKVQTqaUbJVPdc/gEdggTMM0Se+s+Ja4ju4TlNcStKl2Hw== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + +algoliasearch@^4.0.0: + version "4.12.0" + resolved "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.12.0.tgz" + integrity sha512-fZOMMm+F3Bi5M/MoFIz7hiuyCitJza0Hu+r8Wzz4LIQClC6YGMRq7kT6NNU1fSSoFDSeJIwMfedbbi5G9dJoVQ== + dependencies: + "@algolia/cache-browser-local-storage" "4.12.0" + "@algolia/cache-common" "4.12.0" + "@algolia/cache-in-memory" "4.12.0" + "@algolia/client-account" "4.12.0" + "@algolia/client-analytics" "4.12.0" + "@algolia/client-common" "4.12.0" + "@algolia/client-personalization" "4.12.0" + "@algolia/client-search" "4.12.0" + "@algolia/logger-common" "4.12.0" + "@algolia/logger-console" "4.12.0" + "@algolia/requester-browser-xhr" "4.12.0" + "@algolia/requester-common" "4.12.0" + "@algolia/requester-node-http" "4.12.0" + "@algolia/transporter" "4.12.0" + +alphanum-sort@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz" + integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= + +ansi-align@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz" + integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w== + dependencies: + string-width "^4.1.0" + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.1: + version "4.3.2" + resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^5.0.0, ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz" + integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@^3.0.3, anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +append-field@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz" + integrity sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw== + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +"aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz" + integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== + dependencies: + delegates "^1.0.0" + readable-stream "^3.6.0" + +are-we-there-yet@~1.1.2: + version "1.1.7" + resolved "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz" + integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arg@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg== + +arg@^5.0.0: + version "5.0.2" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +arg@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/arg/-/arg-5.0.1.tgz" + integrity sha512-e0hDa9H2Z9AwFkk2qDlwhoMYE4eToKarchkQHovNdLTCYMHZHeRjI71crOh+dio4K6u1IcwubQqo79Ga4CyAQA== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== + dependencies: + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" + +aria-query@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz" + integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg== + +array-differ@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz" + integrity sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg== + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +array-ify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz" + integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= + +array-includes@^3.1.3, array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.npmjs.org/array-includes/-/array-includes-3.1.4.tgz" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +array.prototype.flatmap@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.5.tgz" + integrity sha512-08u6rVyi1Lj7oqWbS9nUxliETrtIROT4XGTA4D/LWGten6E3ocm7cy9SIrmNHOL5XVbVuckUp3X6Xyg8/zpvHA== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +arrify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= + +arrify@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== + +asap@^2.0.0, asap@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +asn1@~0.2.3: + version "0.2.6" + resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz" + 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.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +ast-types-flow@^0.0.7: + version "0.0.7" + resolved "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz" + integrity sha1-9wtzXGvKGlycItmCw+Oef+ujva0= + +async@0.9.x: + version "0.9.2" + resolved "https://registry.npmjs.org/async/-/async-0.9.2.tgz" + integrity sha1-rqdNXmHB+JlhO/ZL2mbUx48v0X0= + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.npmjs.org/async/-/async-2.6.3.tgz" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +async@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/async/-/async-3.2.3.tgz" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +autoprefixer@^10.4.2: + version "10.4.2" + resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.2.tgz" + integrity sha512-9fOPpHKuDW1w/0EKfRmVnxTDt8166MAnLI3mgZ1JCnhNtYWxcJ6Ud5CO/AVOZi/AvFa8DY9RTy3h3+tFBlrrdQ== + dependencies: + browserslist "^4.19.1" + caniuse-lite "^1.0.30001297" + fraction.js "^4.1.2" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.11.0" + resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" + integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== + +axe-core@^4.3.5: + version "4.3.5" + resolved "https://registry.npmjs.org/axe-core/-/axe-core-4.3.5.tgz" + integrity sha512-WKTW1+xAzhMS5dJsxWkliixlO/PqC4VhmO9T4juNYcaTg9jzWiJsou6m5pxWYGfigWbwzJWeFY6z47a+4neRXA== + +axios-retry@^3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/axios-retry/-/axios-retry-3.2.4.tgz" + integrity sha512-Co3UXiv4npi6lM963mfnuH90/YFLKWWDmoBYfxkHT5xtkSSWNqK9zdG3fw5/CP/dsoKB5aMMJCsgab+tp1OxLQ== + dependencies: + "@babel/runtime" "^7.15.4" + is-retry-allowed "^2.2.0" + +axios@0.26.0: + version "0.26.0" + resolved "https://registry.npmjs.org/axios/-/axios-0.26.0.tgz" + integrity sha512-lKoGLMYtHvFrPVt3r+RBMp9nh34N0M8zEfCWqdWZx6phynIEhQqAdydpyBAAG211zlhX9Rgu08cOamy6XjE5Og== + dependencies: + follow-redirects "^1.14.8" + +axios@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== + dependencies: + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + +axobject-query@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz" + integrity sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA== + +babel-jest@^27.4.2, babel-jest@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/babel-jest/-/babel-jest-27.4.6.tgz" + integrity sha512-qZL0JT0HS1L+lOuH+xC2DVASR3nunZi/ozGhpgauJHgmI7f8rudxf6hUjEHympdQ/J64CdKmPkgfJ+A3U6QCrg== + dependencies: + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/babel__core" "^7.1.14" + babel-plugin-istanbul "^6.1.1" + babel-preset-jest "^27.4.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + slash "^3.0.0" + +babel-loader@^8.2.3: + version "8.2.3" + resolved "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz" + integrity sha512-n4Zeta8NC3QAsuyiizu0GkmRcQ6clkV9WFUnUf1iXP//IeSKbWjofW3UHyZVwlOB4y039YQKefawyTn64Zwbuw== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^1.4.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-dynamic-import-node@^2.3.3: + version "2.3.3" + resolved "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz" + integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== + dependencies: + object.assign "^4.1.0" + +babel-plugin-istanbul@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz" + integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@istanbuljs/load-nyc-config" "^1.0.0" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-instrument "^5.0.4" + test-exclude "^6.0.0" + +babel-plugin-jest-hoist@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.4.0.tgz" + integrity sha512-Jcu7qS4OX5kTWBc45Hz7BMmgXuJqRnhatqpUhnzGC3OBYpOmf2tv6jFNwZpwM7wU7MUuv2r9IPS/ZlYOuburVw== + dependencies: + "@babel/template" "^7.3.3" + "@babel/types" "^7.3.3" + "@types/babel__core" "^7.0.0" + "@types/babel__traverse" "^7.0.6" + +babel-plugin-macros@^2.6.1: + version "2.8.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz" + integrity sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg== + dependencies: + "@babel/runtime" "^7.7.2" + cosmiconfig "^6.0.0" + resolve "^1.12.0" + +babel-plugin-macros@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz" + integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg== + dependencies: + "@babel/runtime" "^7.12.5" + cosmiconfig "^7.0.0" + resolve "^1.19.0" + +babel-plugin-named-asset-import@^0.3.8: + version "0.3.8" + resolved "https://registry.npmjs.org/babel-plugin-named-asset-import/-/babel-plugin-named-asset-import-0.3.8.tgz" + integrity sha512-WXiAc++qo7XcJ1ZnTYGtLxmBCVbddAml3CEXgWaBzNzLNoxtQ8AiGEFDMOhot9XjTCQbvP5E77Fj9Gk924f00Q== + +babel-plugin-polyfill-corejs2@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.1.tgz" + integrity sha512-v7/T6EQcNfVLfcN2X8Lulb7DjprieyLWJK/zOWH5DUYcAgex9sP3h25Q+DLsX9TloXe3y1O8l2q2Jv9q8UVB9w== + dependencies: + "@babel/compat-data" "^7.13.11" + "@babel/helper-define-polyfill-provider" "^0.3.1" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.5.0: + version "0.5.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.1.tgz" + integrity sha512-TihqEe4sQcb/QcPJvxe94/9RZuLQuF1+To4WqQcRvc+3J3gLCPIPgDKzGLG6zmQLfH3nn25heRuDNkS2KR4I8A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + core-js-compat "^3.20.0" + +babel-plugin-polyfill-regenerator@^0.3.0: + version "0.3.1" + resolved "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.3.1.tgz" + integrity sha512-Y2B06tvgHYt1x0yz17jGkGeeMr5FeKUu+ASJ+N6nB5lQ8Dapfg42i0OVrf8PNGJ3zKL4A23snMi1IRwrqqND7A== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.1" + +babel-plugin-transform-react-remove-prop-types@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + +babel-preset-current-node-syntax@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz" + integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== + dependencies: + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-bigint" "^7.8.3" + "@babel/plugin-syntax-class-properties" "^7.8.3" + "@babel/plugin-syntax-import-meta" "^7.8.3" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.8.3" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" + +babel-preset-jest@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.4.0.tgz" + integrity sha512-NK4jGYpnBvNxcGo7/ZpZJr51jCGT+3bwwpVIDY2oNfTxJJldRtB4VAcYdgp1loDE50ODuTu+yBjpMAswv5tlpg== + dependencies: + babel-plugin-jest-hoist "^27.4.0" + babel-preset-current-node-syntax "^1.0.0" + +babel-preset-react-app@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/babel-preset-react-app/-/babel-preset-react-app-10.0.1.tgz" + integrity sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg== + dependencies: + "@babel/core" "^7.16.0" + "@babel/plugin-proposal-class-properties" "^7.16.0" + "@babel/plugin-proposal-decorators" "^7.16.4" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.16.0" + "@babel/plugin-proposal-numeric-separator" "^7.16.0" + "@babel/plugin-proposal-optional-chaining" "^7.16.0" + "@babel/plugin-proposal-private-methods" "^7.16.0" + "@babel/plugin-transform-flow-strip-types" "^7.16.0" + "@babel/plugin-transform-react-display-name" "^7.16.0" + "@babel/plugin-transform-runtime" "^7.16.4" + "@babel/preset-env" "^7.16.4" + "@babel/preset-react" "^7.16.0" + "@babel/preset-typescript" "^7.16.0" + "@babel/runtime" "^7.16.3" + babel-plugin-macros "^3.1.0" + babel-plugin-transform-react-remove-prop-types "^0.4.24" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base16@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz" + integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA= + +basic-auth@^2.0.1, basic-auth@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz" + integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg== + dependencies: + safe-buffer "5.1.2" + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz" + integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bcrypt@^5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2" + integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww== + dependencies: + "@mapbox/node-pre-gyp" "^1.0.11" + node-addon-api "^5.0.0" + +before-after-hook@^2.2.0: + version "2.2.2" + resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz" + integrity sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ== + +bfj@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/bfj/-/bfj-7.0.2.tgz" + integrity sha512-+e/UqUzwmzJamNF50tBV6tZPTORow7gQ96iFow+8b562OdMpEK0BcJEq2OSPEDmAbSMBQ7PKZ87ubFkgxpYWgw== + dependencies: + bluebird "^3.5.5" + check-types "^11.1.1" + hoopy "^0.1.4" + tryer "^1.0.1" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bluebird@^3.5.5: + version "3.7.2" + resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.2: + version "1.19.2" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.19.2.tgz" + integrity sha512-SAAwOxgoCKMGs9uUAUFHygfLAyaniaoun6I8mFY9pRAJL9+Kec34aU+oIjDhTycub1jozEfEwx1W1IuOYxVSFw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.9.7" + raw-body "2.4.3" + type-is "~1.6.18" + +body-parser@1.20.1: + version "1.20.1" + resolved "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz" + integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw== + dependencies: + bytes "3.1.2" + content-type "~1.0.4" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.1" + type-is "~1.6.18" + unpipe "1.0.0" + +body-scroll-lock@4.0.0-beta.0: + version "4.0.0-beta.0" + resolved "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz" + integrity sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ== + +bonjour@^3.5.0: + version "3.5.0" + resolved "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz" + integrity sha1-jokKGD2O6aI5OzhExpGkK897yfU= + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^6.0.1" + multicast-dns-service-types "^1.1.0" + +boolbase@^1.0.0, boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + +boxen@^5.0.0: + version "5.1.2" + resolved "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz" + integrity sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ== + dependencies: + 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" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browserslist@^4.0.0, browserslist@^4.14.5, browserslist@^4.16.0, browserslist@^4.16.6, browserslist@^4.17.5, browserslist@^4.18.1, browserslist@^4.19.1: + version "4.19.1" + resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.19.1.tgz" + integrity sha512-u2tbbG5PdKRTUoctO3NBD8FQ5HdPh1ZXPHzp1rwaa5jTc+RV9/+RlWiAIKmjRPQF+xbGM9Kklj5bZQFa2s/38A== + dependencies: + caniuse-lite "^1.0.30001286" + electron-to-chromium "^1.4.17" + escalade "^3.1.1" + node-releases "^2.0.1" + picocolors "^1.0.0" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +buffer-writer@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz" + integrity sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw== + +builtin-modules@^3.1.0: + version "3.2.0" + resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz" + integrity sha512-lGzLKcioL90C7wMczpkY0n/oART3MbBa8R9OFGE1rJxoVI86u4WAGfEk8Wjv10eKSyTHVGkSo3bvBylCEtk7LA== + +builtins@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz" + integrity sha1-y5T662HIaWRR2zZTThQi+U8K7og= + +bull@^4.7.0: + version "4.8.1" + resolved "https://registry.npmjs.org/bull/-/bull-4.8.1.tgz" + integrity sha512-ojH5AfOchKQsQwwE+thViS1pMpvREGC+Ov1+3HXsQqn5Q27ZSGkgMriMqc6c9J9rvQ/+D732pZE+TN1+2LRWVg== + dependencies: + cron-parser "^4.2.1" + debuglog "^1.0.0" + get-port "^5.1.1" + ioredis "^4.28.5" + lodash "^4.17.21" + msgpackr "^1.5.2" + p-timeout "^3.2.0" + semver "^7.3.2" + uuid "^8.3.0" + +bullmq@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bullmq/-/bullmq-3.0.0.tgz" + integrity sha512-amw+YZhEo1B47iMpaLbtKwlzZjQi5NYjLCYl8n9qkQpkDDVAVJ9d++zdOgyXX6kG7i/pMP9tr2vyj3J6IcjbTA== + dependencies: + cron-parser "^4.6.0" + glob "^8.0.3" + ioredis "^5.2.2" + lodash "^4.17.21" + msgpackr "^1.6.2" + semver "^7.3.7" + tslib "^2.0.0" + uuid "^9.0.0" + +busboy@^1.0.0: + version "1.6.0" + resolved "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +byline@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz" + integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= + +byte-size@^7.0.0: + version "7.0.1" + resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.1.tgz" + integrity sha512-crQdqyCwhokxwV1UyDzLZanhkugAgft7vt0qbbdt60C6Zf3CAiGmtUCylbtYwrU6loOUw3euGrNtW1J651ot1A== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz" + integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +bytes@3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.1.tgz" + integrity sha512-dWe4nWO/ruEOY7HkUJ5gFt1DCFV9zPRoJr8pV0/ASQermOZjtq8jMjOprC0Kd10GLN+l7xaUPvxzJFWtxGu8Fg== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +cac@^6.7.14: + version "6.7.14" + resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" + integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== + +cacache@^15.0.5, cacache@^15.2.0: + version "15.3.0" + resolved "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz" + integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ== + dependencies: + "@npmcli/fs" "^1.0.0" + "@npmcli/move-file" "^1.0.1" + chownr "^2.0.0" + fs-minipass "^2.0.0" + glob "^7.1.4" + infer-owner "^1.0.4" + lru-cache "^6.0.0" + minipass "^3.1.1" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.2" + mkdirp "^1.0.3" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.0.2" + unique-filename "^1.1.1" + +cacache@^18.0.0: + version "18.0.2" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-18.0.2.tgz#fd527ea0f03a603be5c0da5805635f8eef00c60c" + integrity sha512-r3NU8h/P+4lVUHfeRw1dtgQYar3DZMm4/cm2bZgOvrFC/su7budSOeqh52VJIC4U4iG1WWwV6vRW0znqBvxNuw== + dependencies: + "@npmcli/fs" "^3.1.0" + fs-minipass "^3.0.0" + glob "^10.2.2" + lru-cache "^10.0.1" + minipass "^7.0.3" + minipass-collect "^2.0.1" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + p-map "^4.0.0" + ssri "^10.0.0" + tar "^6.1.11" + unique-filename "^3.0.0" + +cacheable-request@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz" + 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" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camel-case@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz" + integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== + dependencies: + pascal-case "^3.1.2" + tslib "^2.0.3" + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +camelcase@^6.2.0, camelcase@^6.2.1: + version "6.3.0" + resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" + integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== + +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001297: + version "1.0.30001300" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001300.tgz" + integrity sha512-cVjiJHWGcNlJi8TZVKNMnvMid3Z3TTdDHmLDzlOdIiZq138Exvo0G+G0wTdVYolxKb4AYwC+38pxodiInVtJSA== + +caniuse-lite@^1.0.30001299: + version "1.0.30001301" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001301.tgz" + integrity sha512-csfD/GpHMqgEL3V3uIgosvh+SVIQvCh43SNu9HRbP1lnxkKm1kjDG4f32PP571JplkLjfS+mg2p1gxR7MYrrIA== + +case-sensitive-paths-webpack-plugin@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/case-sensitive-paths-webpack-plugin/-/case-sensitive-paths-webpack-plugin-2.4.0.tgz" + integrity sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chai@^4.3.10: + version "4.4.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.4.0.tgz#f9ac79f26726a867ac9d90a9b382120479d5f55b" + integrity sha512-x9cHNq1uvkCdU+5xTkNh5WtgD4e4yDFCsp9jVc7N7qVeKeftv3gO/ZrviX5d+3ZfxdYnZXZYujjRInu1RogU6A== + dependencies: + assertion-error "^1.1.0" + check-error "^1.0.3" + deep-eql "^4.1.3" + get-func-name "^2.0.2" + loupe "^2.3.6" + pathval "^1.1.1" + type-detect "^4.0.8" + +chalk@^2.0.0, chalk@^2.4.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +char-regex@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz" + integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== + +char-regex@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/char-regex/-/char-regex-2.0.0.tgz" + integrity sha512-oGu2QekBMXgyQNWPDRQ001bjvDnZe4/zBTz37TMbiKz1NbNiyiH5hRkobe7npRN6GfbGbxMYFck/vQ1r9c1VMA== + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + +check-error@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" + integrity sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg== + dependencies: + get-func-name "^2.0.2" + +check-types@^11.1.1: + version "11.1.2" + resolved "https://registry.npmjs.org/check-types/-/check-types-11.1.2.tgz" + integrity sha512-tzWzvgePgLORb9/3a0YenggReLKAIb2owL03H2Xdoe5pKcUyWRSEQ8xfCar8t2SIAuEDwtmx2da1YB52YuHQMQ== + +chokidar@^3.4.2, chokidar@^3.5.2: + version "3.5.3" + resolved "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz" + 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.4: + version "1.1.4" + resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz" + integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== + +chownr@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz" + integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +ci-info@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/ci-info/-/ci-info-3.3.0.tgz" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== + +cjs-module-lexer@^1.0.0: + version "1.2.2" + resolved "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz" + integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== + +classcat@^5.0.3, classcat@^5.0.4: + version "5.0.5" + resolved "https://registry.yarnpkg.com/classcat/-/classcat-5.0.5.tgz#8c209f359a93ac302404a10161b501eba9c09c77" + integrity sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w== + +clean-css@^5.2.2: + version "5.2.2" + resolved "https://registry.npmjs.org/clean-css/-/clean-css-5.2.2.tgz" + integrity sha512-/eR8ru5zyxKzpBLv9YZvMXgTSSQn7AdkMItMYynsFgGwTveCRVam9IUPFloE85B4vAIj05IuKmmEoV7/AQjT0w== + dependencies: + source-map "~0.6.0" + +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-boxes@^2.2.1: + version "2.2.1" + resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz" + integrity sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +clipboard-copy@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clipboard-copy/-/clipboard-copy-4.0.1.tgz" + integrity sha512-wOlqdqziE/NNTUJsfSgXmBMIrYmfd5V0HCGsR8uAKHcg+h9NENWINcfRjtWGU77wDHC8B8ijV4hMTGYbrKovng== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +clone-response@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz" + integrity sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws= + dependencies: + mimic-response "^1.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +clsx@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz" + integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== + +clsx@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz" + integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== + +cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + +cmd-shim@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +coa@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz" + integrity sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA== + dependencies: + "@types/q" "^1.5.1" + chalk "^2.4.1" + q "^1.1.2" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= + +collect-v8-coverage@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz" + integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== + +color-convert@^1.9.0, color-convert@^1.9.3: + version "1.9.3" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color-support@^1.1.2: + version "1.1.3" + resolved "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + +color@^3.1.3, color@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/color/-/color-3.2.1.tgz" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colord@^2.9.1: + version "2.9.2" + resolved "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz" + integrity sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ== + +colorette@2.0.19, colorette@^2.0.10: + version "2.0.19" + resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.19.tgz" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + +columnify@^1.5.4: + version "1.5.4" + resolved "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz" + integrity sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs= + dependencies: + strip-ansi "^3.0.0" + wcwidth "^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.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.2.0: + version "7.2.0" + resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commander@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +commander@^9.0.0, commander@^9.1.0: + version "9.5.0" + resolved "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz" + integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== + +common-path-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz" + integrity sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== + dependencies: + array-ify "^1.0.0" + dot-prop "^5.1.0" + +compare-versions@^4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/compare-versions/-/compare-versions-4.1.3.tgz" + integrity sha512-WQfnbDcrYnGr55UwbxKiQKASnTtNnaAWVi8jZyy8NTpVAXWACSne8lMD1iaIo9AiU6mnuLvSVshCzewVuWxHUg== + +component-emitter@^1.3.0: + 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.npmjs.org/component-type/-/component-type-1.2.1.tgz" + integrity sha1-ikeQFwAjjk/DIml3EjAibyS0Fak= + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +compute-scroll-into-view@^1.0.17: + version "1.0.17" + resolved "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz" + integrity sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concat-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz" + integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^2.2.2" + typedarray "^0.0.6" + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +config-chain@^1.1.12: + version "1.1.13" + resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz" + integrity sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ== + dependencies: + ini "^1.3.4" + proto-list "~1.2.1" + +configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz" + 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" + +confusing-browser-globals@^1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz" + integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== + +connect-history-api-fallback@^1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz" + integrity sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg== + +console-control-strings@^1.0.0, console-control-strings@^1.1.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz" + integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4= + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@1.0.4, content-type@^1.0.4, content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +conventional-changelog-angular@^5.0.12: + version "5.0.13" + resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.13.tgz" + integrity sha512-i/gipMxs7s8L/QeuavPF2hLnJgH6pEZAttySB6aiQLWcX3puWDL3ACVmvBhJGxnAy52Qc15ua26BufY6KpmrVA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + +conventional-changelog-core@^4.2.2: + version "4.2.4" + resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.4.tgz" + integrity sha512-gDVS+zVJHE2v4SLc6B0sLsPiloR0ygU7HaDW14aNJE1v4SlqJPILPl/aJC7YdtRE4CybBf8gDwObBvKha8Xlyg== + dependencies: + add-stream "^1.0.0" + conventional-changelog-writer "^5.0.0" + conventional-commits-parser "^3.2.0" + dateformat "^3.0.0" + get-pkg-repo "^4.0.0" + git-raw-commits "^2.0.8" + git-remote-origin-url "^2.0.0" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + through2 "^4.0.0" + +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== + +conventional-changelog-writer@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz" + integrity sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ== + dependencies: + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.7" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + +conventional-commits-parser@^3.2.0: + version "3.2.4" + resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.4.tgz" + integrity sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q== + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== + dependencies: + concat-stream "^2.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" + q "^1.5.1" + +convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: + version "1.8.0" + resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz" + integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.2, cookie@^0.4.1: + version "0.4.2" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz" + integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== + +cookie@0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" + integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== + +cookiejar@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.4.tgz#ee669c1fea2cf42dc31585469d193fef0d65771b" + integrity sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw== + +core-js-compat@^3.20.0, core-js-compat@^3.20.2: + version "3.20.3" + resolved "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.20.3.tgz" + integrity sha512-c8M5h0IkNZ+I92QhIpuSijOxGAcj3lgpsWdkCqmUTZNwidujF4r3pi6x1DCN+Vcs5qTS2XWWMfWSuCqyupX8gw== + dependencies: + browserslist "^4.19.1" + semver "7.0.0" + +core-js-pure@^3.20.2, core-js-pure@^3.8.1: + version "3.20.3" + resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.20.3.tgz" + integrity sha512-Q2H6tQ5MtPtcC7f3HxJ48i4Q7T9ybPKgvWyuH7JXIoNa2pm0KuBnycsET/qw1SLLZYfbsbrZQNMeIOClb+6WIA== + +core-js@^3.19.2: + version "3.20.3" + resolved "https://registry.npmjs.org/core-js/-/core-js-3.20.3.tgz" + integrity sha512-vVl8j8ph6tRS3B8qir40H7yw7voy17xL0piAjlbBUsH7WIfzoedL/ZOr1OV9FyZQLWXsayOJyV4tnRyXR85/ag== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + +cosmiconfig@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz" + integrity sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.7.2" + +cosmiconfig@^7.0.0, cosmiconfig@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz" + integrity sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ== + dependencies: + "@types/parse-json" "^4.0.0" + import-fresh "^3.2.1" + parse-json "^5.0.0" + path-type "^4.0.0" + yaml "^1.10.0" + +cron-parser@^4.2.1: + version "4.3.0" + resolved "https://registry.npmjs.org/cron-parser/-/cron-parser-4.3.0.tgz" + integrity sha512-mK6qJ6k9Kn0/U7Cv6LKQnReUW3GqAW4exgwmHJGb3tPgcy0LrS+PeqxPPiwL8uW/4IJsMsCZrCc4vf1nnXMjzA== + dependencies: + luxon "^1.28.0" + +cron-parser@^4.6.0: + version "4.6.0" + resolved "https://registry.npmjs.org/cron-parser/-/cron-parser-4.6.0.tgz" + integrity sha512-guZNLMGUgg6z4+eGhmHGw7ft+v6OQeuHzd1gcLxCo9Yg/qoxmG3nindp2/uwGCLizEisf2H0ptqeVXeoCpP6FA== + dependencies: + luxon "^3.0.1" + +cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + +crypto-js@^4.1.1: + version "4.2.0" + resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" + integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +css-blank-pseudo@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.2.tgz" + integrity sha512-hOb1LFjRR+8ocA071xUSmg5VslJ8NGo/I2qpUpdeAYyBVCgupS5O8SEVo4SxEMYyFBNodBkzG3T1iqW9HCXxew== + dependencies: + postcss-selector-parser "^6.0.8" + +css-declaration-sorter@^6.0.3: + version "6.1.4" + resolved "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.1.4.tgz" + integrity sha512-lpfkqS0fctcmZotJGhnxkIyJWvBXgpyi2wsFd4J8VB7wzyrT6Ch/3Q+FMNJpjK4gu1+GN5khOnpU2ZVKrLbhCw== + dependencies: + timsort "^0.3.0" + +css-has-pseudo@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-3.0.3.tgz" + integrity sha512-0gDYWEKaGacwxCqvQ3Ypg6wGdD1AztbMm5h1JsactG2hP2eiflj808QITmuWBpE7sjSEVrAlZhPTVd/nNMj/hQ== + dependencies: + postcss-selector-parser "^6.0.8" + +css-loader@^6.5.1: + version "6.5.1" + resolved "https://registry.npmjs.org/css-loader/-/css-loader-6.5.1.tgz" + integrity sha512-gEy2w9AnJNnD9Kuo4XAP9VflW/ujKoS9c/syO+uWMlm5igc7LysKzPXaDoR2vroROkSwsTS2tGr1yGGEbZOYZQ== + dependencies: + icss-utils "^5.1.0" + postcss "^8.2.15" + postcss-modules-extract-imports "^3.0.0" + postcss-modules-local-by-default "^4.0.0" + postcss-modules-scope "^3.0.0" + postcss-modules-values "^4.0.0" + postcss-value-parser "^4.1.0" + semver "^7.3.5" + +css-minimizer-webpack-plugin@^3.2.0: + version "3.4.1" + resolved "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.4.1.tgz" + integrity sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q== + dependencies: + cssnano "^5.0.6" + jest-worker "^27.0.2" + postcss "^8.3.5" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + +css-prefers-color-scheme@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-6.0.2.tgz" + integrity sha512-gv0KQBEM+q/XdoKyznovq3KW7ocO7k+FhPP+hQR1MenJdu0uPGS6IZa9PzlbqBeS6XcZJNAoqoFxlAUW461CrA== + +css-select-base-adapter@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz" + integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== + +css-select@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz" + integrity sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ== + dependencies: + boolbase "^1.0.0" + css-what "^3.2.1" + domutils "^1.7.0" + nth-check "^1.0.2" + +css-select@^4.1.3: + version "4.2.1" + resolved "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz" + integrity sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ== + dependencies: + boolbase "^1.0.0" + css-what "^5.1.0" + domhandler "^4.3.0" + domutils "^2.8.0" + nth-check "^2.0.1" + +css-select@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz" + integrity sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg== + dependencies: + boolbase "^1.0.0" + css-what "^6.1.0" + domhandler "^5.0.2" + domutils "^3.0.1" + nth-check "^2.0.1" + +css-tree@1.0.0-alpha.37: + version "1.0.0-alpha.37" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz" + integrity sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg== + dependencies: + mdn-data "2.0.4" + source-map "^0.6.1" + +css-tree@^1.1.2, css-tree@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz" + integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== + dependencies: + mdn-data "2.0.14" + source-map "^0.6.1" + +css-what@^3.2.1: + version "3.4.2" + resolved "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz" + integrity sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ== + +css-what@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz" + integrity sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw== + +css-what@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz" + integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== + +css.escape@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz" + integrity sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s= + +css@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/css/-/css-3.0.0.tgz" + integrity sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ== + dependencies: + inherits "^2.0.4" + source-map "^0.6.1" + source-map-resolve "^0.6.0" + +cssdb@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/cssdb/-/cssdb-5.1.0.tgz" + integrity sha512-/vqjXhv1x9eGkE/zO6o8ZOI7dgdZbLVLUGyVRbPgk6YipXbW87YzUCcO+Jrmi5bwJlAH6oD+MNeZyRgXea1GZw== + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +cssnano-preset-default@^5.1.10: + version "5.1.10" + resolved "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-5.1.10.tgz" + integrity sha512-BcpSzUVygHMOnp9uG5rfPzTOCb0GAHQkqtUQx8j1oMNF9A1Q8hziOOhiM4bdICpmrBIU85BE64RD5XGYsVQZNA== + dependencies: + css-declaration-sorter "^6.0.3" + cssnano-utils "^3.0.0" + postcss-calc "^8.2.0" + postcss-colormin "^5.2.3" + postcss-convert-values "^5.0.2" + postcss-discard-comments "^5.0.1" + postcss-discard-duplicates "^5.0.1" + postcss-discard-empty "^5.0.1" + postcss-discard-overridden "^5.0.2" + postcss-merge-longhand "^5.0.4" + postcss-merge-rules "^5.0.4" + postcss-minify-font-values "^5.0.2" + postcss-minify-gradients "^5.0.4" + postcss-minify-params "^5.0.3" + postcss-minify-selectors "^5.1.1" + postcss-normalize-charset "^5.0.1" + postcss-normalize-display-values "^5.0.2" + postcss-normalize-positions "^5.0.2" + postcss-normalize-repeat-style "^5.0.2" + postcss-normalize-string "^5.0.2" + postcss-normalize-timing-functions "^5.0.2" + postcss-normalize-unicode "^5.0.2" + postcss-normalize-url "^5.0.4" + postcss-normalize-whitespace "^5.0.2" + postcss-ordered-values "^5.0.3" + postcss-reduce-initial "^5.0.2" + postcss-reduce-transforms "^5.0.2" + postcss-svgo "^5.0.3" + postcss-unique-selectors "^5.0.2" + +cssnano-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.0.0.tgz" + integrity sha512-Pzs7/BZ6OgT+tXXuF12DKR8SmSbzUeVYCtMBbS8lI0uAm3mrYmkyqCXXPsQESI6kmLfEVBppbdVY/el3hg3nAA== + +cssnano@^5.0.6: + version "5.0.15" + resolved "https://registry.npmjs.org/cssnano/-/cssnano-5.0.15.tgz" + integrity sha512-ppZsS7oPpi2sfiyV5+i+NbB/3GtQ+ab2Vs1azrZaXWujUSN4o+WdTxlCZIMcT9yLW3VO/5yX3vpyDaQ1nIn8CQ== + dependencies: + cssnano-preset-default "^5.1.10" + lilconfig "^2.0.3" + yaml "^1.10.2" + +csso@^4.0.2, csso@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz" + integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== + dependencies: + css-tree "^1.1.2" + +cssom@^0.4.4: + version "0.4.4" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz" + integrity sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw== + +cssom@~0.3.6: + version "0.3.8" + resolved "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz" + integrity sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A== + dependencies: + cssom "~0.3.6" + +csstype@^2.6.8: + version "2.6.20" + resolved "https://registry.npmjs.org/csstype/-/csstype-2.6.20.tgz" + integrity sha512-/WwNkdXfckNgw6S5R125rrW8ez139lBHWouiBvX8dfMFtcn6V81REDqnH7+CRpRipfYlyU1CmOnOxrmGcFOjeA== + +csstype@^3.0.10, csstype@^3.0.2: + version "3.0.10" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz" + integrity sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA== + +csstype@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz" + integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== + +"d3-color@1 - 3": + version "3.1.0" + resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" + integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== + +"d3-dispatch@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" + integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== + +"d3-drag@2 - 3", d3-drag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" + integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== + dependencies: + d3-dispatch "1 - 3" + d3-selection "3" + +"d3-ease@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" + integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== + +"d3-interpolate@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" + integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== + dependencies: + d3-color "1 - 3" + +"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" + integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== + +"d3-timer@1 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" + integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== + +"d3-transition@2 - 3": + version "3.0.1" + resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" + integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== + dependencies: + d3-color "1 - 3" + d3-dispatch "1 - 3" + d3-ease "1 - 3" + d3-interpolate "1 - 3" + d3-timer "1 - 3" + +d3-zoom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" + integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== + dependencies: + d3-dispatch "1 - 3" + d3-drag "2 - 3" + d3-interpolate "1 - 3" + d3-selection "2 - 3" + d3-transition "2 - 3" + +damerau-levenshtein@^1.0.7: + version "1.0.8" + resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz" + integrity sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ== + dependencies: + abab "^2.0.3" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.0.0" + +dataloader@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz" + integrity sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ== + +dateformat@^3.0.0: + version "3.0.3" + resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz" + integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== + +db-errors@^0.2.3: + version "0.2.3" + resolved "https://registry.npmjs.org/db-errors/-/db-errors-0.2.3.tgz" + integrity sha512-OOgqgDuCavHXjYSJoV2yGhv6SeG8nk42aoCSoyXLZUH7VwFG27rxbavU1z+VrZbZjphw5UkDQwUlD21MwZpUng== + +debug@2.6.9, debug@^2.6.0, debug@^2.6.9, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +debug@^3.1.1, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debuglog@^1.0.0, debuglog@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz" + integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= + +decamelize-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz" + integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= + dependencies: + decamelize "^1.1.0" + map-obj "^1.0.0" + +decamelize@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decimal.js@^10.2.1: + version "10.3.1" + resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz" + integrity sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ== + +decode-uri-component@^0.2.0: + version "0.2.2" + resolved "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== + +decompress-response@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz" + integrity sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M= + dependencies: + mimic-response "^1.0.0" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-eql@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" + integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== + dependencies: + type-detect "^4.0.0" + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== + +deep-is@^0.1.3, deep-is@~0.1.3: + version "0.1.4" + resolved "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +deepmerge@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz" + integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +defer-to-connect@^1.0.1: + version "1.1.3" + resolved "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +defined@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz" + integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= + +del@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/del/-/del-6.0.0.tgz" + integrity sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ== + dependencies: + globby "^11.0.1" + graceful-fs "^4.2.4" + is-glob "^4.0.1" + is-path-cwd "^2.2.0" + is-path-inside "^3.0.2" + p-map "^4.0.0" + rimraf "^3.0.2" + slash "^3.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz" + integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= + +denque@^1.1.0: + version "1.5.1" + resolved "https://registry.npmjs.org/denque/-/denque-1.5.1.tgz" + integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== + +denque@^2.0.1: + version "2.1.0" + resolved "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + +depd@2.0.0, depd@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@^1.1.2, depd@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-indent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz" + integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +detect-libc@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + +detect-newline@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" + integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +detect-port-alt@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz" + integrity sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q== + dependencies: + address "^1.0.1" + debug "^2.6.0" + +detective@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz" + integrity sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg== + dependencies: + acorn-node "^1.6.1" + defined "^1.0.0" + minimist "^1.1.1" + +dezalgo@^1.0.0: + version "1.0.3" + resolved "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz" + integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY= + dependencies: + asap "^2.0.0" + wrappy "1" + +dezalgo@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.4.tgz#751235260469084c132157dfa857f386d4c33d81" + integrity sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig== + dependencies: + asap "^2.0.0" + wrappy "1" + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +diff-sequences@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.4.0.tgz" + integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== + +diff-sequences@^29.6.3: + version "29.6.3" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" + integrity sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +direction@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz" + integrity sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ== + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^1.3.1: + version "1.3.4" + resolved "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz" + integrity sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA== + dependencies: + ip "^1.1.0" + safe-buffer "^5.0.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +dom-accessibility-api@^0.5.6: + version "0.5.10" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.10.tgz" + integrity sha512-Xu9mD0UjrJisTmv7lmVSDMagQcU9R5hwAbxsaAE/35XPnPLJobbuREfV/rraiSaEj/UOvgrzQs66zyTWTlyd+g== + +dom-converter@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz" + integrity sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA== + dependencies: + utila "~0.4" + +dom-helpers@^5.0.1: + version "5.2.1" + resolved "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz" + integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA== + dependencies: + "@babel/runtime" "^7.8.7" + csstype "^3.0.2" + +dom-serializer@0: + version "0.2.2" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + +dom-serializer@^1.0.1: + version "1.3.2" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz" + integrity sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.2.0" + entities "^2.0.0" + +dom-serializer@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz" + integrity sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg== + dependencies: + domelementtype "^2.3.0" + domhandler "^5.0.2" + entities "^4.2.0" + +domelementtype@1: + version "1.3.1" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + +domelementtype@^2.0.1, domelementtype@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz" + integrity sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A== + +domelementtype@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz" + integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== + +domexception@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz" + integrity sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg== + dependencies: + webidl-conversions "^5.0.0" + +domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz" + integrity sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g== + dependencies: + domelementtype "^2.2.0" + +domhandler@^5.0.2, domhandler@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz" + integrity sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w== + dependencies: + domelementtype "^2.3.0" + +domutils@^1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^2.5.2, domutils@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz" + integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.2.0" + domhandler "^4.2.0" + +domutils@^3.0.1: + version "3.1.0" + resolved "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz" + integrity sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA== + dependencies: + dom-serializer "^2.0.0" + domelementtype "^2.3.0" + domhandler "^5.0.3" + +dot-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz" + integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +dot-prop@^5.1.0, dot-prop@^5.2.0: + version "5.3.0" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== + dependencies: + is-obj "^2.0.0" + +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + +dotenv-expand@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz" + integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== + +dotenv@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz" + integrity sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q== + +dotenv@^16.3.1: + version "16.3.1" + resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz" + integrity sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ== + +duplexer3@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz" + integrity sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI= + +duplexer@^0.1.1, duplexer@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz" + integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +ejs@3.1.6, ejs@^3.1.6: + version "3.1.6" + resolved "https://registry.npmjs.org/ejs/-/ejs-3.1.6.tgz" + integrity sha512-9lt9Zse4hPucPkoP7FHDF0LQAlGyF9JVpnClFLFH3aSSbxmyoqINRpp/9wePWJTUl4KOQwRL72Iw3InHPDkoGw== + dependencies: + jake "^10.6.1" + +electron-to-chromium@^1.4.17: + version "1.4.48" + resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.48.tgz" + integrity sha512-RT3SEmpv7XUA+tKXrZGudAWLDpa7f8qmhjcLaM6OD/ERxjQ/zAojT8/Vvo0BSzbArkElFZ1WyZ9FuwAYbkdBNA== + +emittery@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/emittery/-/emittery-0.8.1.tgz" + integrity sha512-uDfvUjVrfGJJhymx/kz6prltenw1u7WrCg1oa94zYY8xxVpLLUu045LAT0dhDZdXG58/EpPL/5kA180fQ/qudg== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +encoding@^0.1.12, encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +entities@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz" + integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== + +entities@^4.2.0: + version "4.5.0" + resolved "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +env-paths@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz" + integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A== + +envinfo@^7.7.4: + version "7.8.1" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + +error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +error-stack-parser@^2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.6.tgz" + integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ== + dependencies: + stackframe "^1.1.1" + +es-abstract@^1.17.2, es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild-android-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.11.tgz#50402129c3e85bb06434e212374c5f693e4c5f01" + integrity sha512-rrwoXEiuI1kaw4k475NJpexs8GfJqQUKcD08VR8sKHmuW9RUuTR2VxcupVvHdiGh9ihxL9m3lpqB1kju92Ialw== + +esbuild-android-arm64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.11.tgz#49bee35218ea2ccf1a0c5f187af77c1c0a5dee71" + integrity sha512-/hDubOg7BHOhUUsT8KUIU7GfZm5bihqssvqK5PfO4apag7YuObZRZSzViyEKcFn2tPeHx7RKbSBXvAopSHDZJQ== + +esbuild-darwin-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.11.tgz#89a90c8cf6f0029ac4169bfedd012a0412c1575f" + integrity sha512-1DqHD0ms3AhiwkKnjRUzmiW7JnaJJr5FKrPiR7xuyMwnjDqvNWDdMq4rKSD9OC0piFNK6n0LghsglNMe2MwJtA== + +esbuild-darwin-arm64@0.15.11: + version "0.15.11" + resolved "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.11.tgz" + integrity sha512-OMzhxSbS0lwwrW40HHjRCeVIJTURdXFA8c3GU30MlHKuPCcvWNUIKVucVBtNpJySXmbkQMDJdJNrXzNDyvoqvQ== + +esbuild-freebsd-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.11.tgz#fd86fd1b3b65366048f35b996d9cdf3547384eee" + integrity sha512-8dKP26r0/Qyez8nTCwpq60QbuYKOeBygdgOAWGCRalunyeqWRoSZj9TQjPDnTTI9joxd3QYw3UhVZTKxO9QdRg== + +esbuild-freebsd-arm64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.11.tgz#d346bcacfe9779ebc1a11edac1bdedeff6dda3b1" + integrity sha512-aSGiODiukLGGnSg/O9+cGO2QxEacrdCtCawehkWYTt5VX1ni2b9KoxpHCT9h9Y6wGqNHmXFnB47RRJ8BIqZgmQ== + +esbuild-linux-32@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.11.tgz#64b50e774bf75af7dcc6a73ad509f2eb0ac4487b" + integrity sha512-lsrAfdyJBGx+6aHIQmgqUonEzKYeBnyfJPkT6N2dOf1RoXYYV1BkWB6G02tjsrz1d5wZzaTc3cF+TKmuTo/ZwA== + +esbuild-linux-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.11.tgz#fba3a78b95769772863f8f6dc316abca55cf8416" + integrity sha512-Y2Rh+PcyVhQqXKBTacPCltINN3uIw2xC+dsvLANJ1SpK5NJUtxv8+rqWpjmBgaNWKQT1/uGpMmA9olALy9PLVA== + +esbuild-linux-arm64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.11.tgz#c0cb31980eee066bfd39a4593660a0ecebe926cb" + integrity sha512-uhcXiTwTmD4OpxJu3xC5TzAAw6Wzf9O1XGWL448EE9bqGjgV1j+oK3lIHAfsHnuIn8K4nDW8yjX0Sv5S++oRuw== + +esbuild-linux-arm@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.11.tgz#7824d20099977aa671016c7de7a5038c9870010f" + integrity sha512-TJllTVk5aSyqPFvvcHTvf6Wu1ZKhWpJ/qNmZO8LL/XeB+LXCclm7HQHNEIz6MT7IX8PmlC1BZYrOiw2sXSB95A== + +esbuild-linux-mips64le@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.11.tgz#10627331c90164e553429ed25e025184bba485b6" + integrity sha512-WD61y/R1M4BLe4gxXRypoQ0Ci+Vjf714QYzcPNkiYv5I8K8WDz2ZR8Bm6cqKxd6rD+e/rZgPDbhQ9PCf7TMHmA== + +esbuild-linux-ppc64le@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.11.tgz#be42679a36a5246b893fc8b898135ebacb5a0a14" + integrity sha512-JVleZS9oPVLTlBhPTWgOwxFWU/wMUdlBwTbGA4GF8c38sLbS13cupj+C8bLq929jU7EMWry4SaL+tKGIaTlqKg== + +esbuild-linux-riscv64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.11.tgz#3ac2f328e3db73cbff833ada94314d8e79503e54" + integrity sha512-9aLIalZ2HFHIOZpmVU11sEAS9F8TnHw49daEjcgMpBXHFF57VuT9f9/9LKJhw781Gda0P9jDkuCWJ0tFbErvJw== + +esbuild-linux-s390x@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.11.tgz#e774e0df061b6847d86783bf3c8c4300a72e03ad" + integrity sha512-sZHtiXXOKsLI3XGBGoYO4qKBzJlb8xNsWmvFiwFMHFzA4AXgDP1KDp7Dawe9C2pavTRBDvl+Ok4n/DHQ59oaTg== + +esbuild-netbsd-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.11.tgz#55e265fa4489e3f396b16c81f6f5a11d6ca2a9a4" + integrity sha512-hUC9yN06K9sg7ju4Vgu9ChAPdsEgtcrcLfyNT5IKwKyfpLvKUwCMZSdF+gRD3WpyZelgTQfJ+pDx5XFbXTlB0A== + +esbuild-openbsd-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.11.tgz#bc04103ccfd8c2f2241e1add0b51a095955b73c4" + integrity sha512-0bBo9SQR4t66Wd91LGMAqmWorzO0TTzVjYiifwoFtel8luFeXuPThQnEm5ztN4g0fnvcp7AnUPPzS/Depf17wQ== + +esbuild-sunos-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.11.tgz#ccd580305d31fde07b5c386da79c942aaf069013" + integrity sha512-EuBdTGlsMTjEl1sQnBX2jfygy7iR6CKfvOzi+gEOfhDqbHXsmY1dcpbVtcwHAg9/2yUZSfMJHMAgf1z8M4yyyw== + +esbuild-windows-32@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.11.tgz#40fe1d48f9b20a76f6db5109aaaf1511aed58c71" + integrity sha512-O0/Wo1Wk6dc0rZSxkvGpmTNIycEznHmkObTFz2VHBhjPsO4ZpCgfGxNkCpz4AdAIeMczpTXt/8d5vdJNKEGC+Q== + +esbuild-windows-64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.11.tgz#80c58b1ef2ff030c78e3a06e7a922776cc4cb687" + integrity sha512-x977Q4HhNjnHx00b4XLAnTtj5vfbdEvkxaQwC1Zh5AN8g5EX+izgZ6e5QgqJgpzyRNJqh4hkgIJF1pyy1be0mQ== + +esbuild-windows-arm64@0.15.11: + version "0.15.11" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.11.tgz#018624023b5c3f0cca334cc99f5ef7134d396333" + integrity sha512-VwUHFACuBahrvntdcMKZteUZ9HaYrBRODoKe4tIWxguQRvvYoYb7iu5LrcRS/FQx8KPZNaa72zuqwVtHeXsITw== + +esbuild@^0.15.9: + version "0.15.11" + resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.15.11.tgz" + integrity sha512-OgHGuhlfZ//mToxjte1D5iiiQgWfJ2GByVMwEC/IuoXsBGkuyK1+KrjYu0laSpnN/L1UmLUCv0s25vObdc1bVg== + optionalDependencies: + "@esbuild/android-arm" "0.15.11" + "@esbuild/linux-loong64" "0.15.11" + esbuild-android-64 "0.15.11" + esbuild-android-arm64 "0.15.11" + esbuild-darwin-64 "0.15.11" + esbuild-darwin-arm64 "0.15.11" + esbuild-freebsd-64 "0.15.11" + esbuild-freebsd-arm64 "0.15.11" + esbuild-linux-32 "0.15.11" + esbuild-linux-64 "0.15.11" + esbuild-linux-arm "0.15.11" + esbuild-linux-arm64 "0.15.11" + esbuild-linux-mips64le "0.15.11" + esbuild-linux-ppc64le "0.15.11" + esbuild-linux-riscv64 "0.15.11" + esbuild-linux-s390x "0.15.11" + esbuild-netbsd-64 "0.15.11" + esbuild-openbsd-64 "0.15.11" + esbuild-sunos-64 "0.15.11" + esbuild-windows-32 "0.15.11" + esbuild-windows-64 "0.15.11" + esbuild-windows-arm64 "0.15.11" + +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== + +escape-html@^1.0.3, escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" + integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +escodegen@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz" + integrity sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw== + dependencies: + esprima "^4.0.1" + estraverse "^5.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +eslint-config-prettier@^8.3.0: + version "8.3.0" + resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz" + integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== + +eslint-config-prettier@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz#31af3d94578645966c082fcb71a5846d3c94867f" + integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw== + +eslint-config-react-app@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/eslint-config-react-app/-/eslint-config-react-app-7.0.0.tgz" + integrity sha512-xyymoxtIt1EOsSaGag+/jmcywRuieQoA2JbPCjnw9HukFj9/97aGPoZVFioaotzk1K5Qt9sHO5EutZbkrAXS0g== + dependencies: + "@babel/core" "^7.16.0" + "@babel/eslint-parser" "^7.16.3" + "@rushstack/eslint-patch" "^1.1.0" + "@typescript-eslint/eslint-plugin" "^5.5.0" + "@typescript-eslint/parser" "^5.5.0" + babel-preset-react-app "^10.0.1" + confusing-browser-globals "^1.0.11" + eslint-plugin-flowtype "^8.0.3" + eslint-plugin-import "^2.25.3" + eslint-plugin-jest "^25.3.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.27.1" + eslint-plugin-react-hooks "^4.3.0" + eslint-plugin-testing-library "^5.0.1" + +eslint-config-react-app@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-7.0.1.tgz#73ba3929978001c5c86274c017ea57eb5fa644b4" + integrity sha512-K6rNzvkIeHaTd8m/QEh1Zko0KI7BACWkkneSs6s9cKZC/J27X3eZR6Upt1jkmZ/4FK+XUOPPxMEN7+lbUXfSlA== + dependencies: + "@babel/core" "^7.16.0" + "@babel/eslint-parser" "^7.16.3" + "@rushstack/eslint-patch" "^1.1.0" + "@typescript-eslint/eslint-plugin" "^5.5.0" + "@typescript-eslint/parser" "^5.5.0" + babel-preset-react-app "^10.0.1" + confusing-browser-globals "^1.0.11" + eslint-plugin-flowtype "^8.0.3" + eslint-plugin-import "^2.25.3" + eslint-plugin-jest "^25.3.0" + eslint-plugin-jsx-a11y "^6.5.1" + eslint-plugin-react "^7.27.1" + eslint-plugin-react-hooks "^4.3.0" + eslint-plugin-testing-library "^5.0.1" + +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-module-utils@^2.7.2: + version "2.7.2" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.2.tgz" + integrity sha512-zquepFnWCY2ISMFwD/DqzaM++H+7PDzOpUvotJWm/y1BAFt5R4oeULgdrTejKqLkz7MA/tgstsUMNYc7wNdTrg== + dependencies: + debug "^3.2.7" + find-up "^2.1.0" + +eslint-plugin-flowtype@^8.0.3: + version "8.0.3" + resolved "https://registry.npmjs.org/eslint-plugin-flowtype/-/eslint-plugin-flowtype-8.0.3.tgz" + integrity sha512-dX8l6qUL6O+fYPtpNRideCFSpmWOUVx5QcaGLVqe/vlDiBSe4vYljDWDETwnyFzpl7By/WVIu6rcrniCgH9BqQ== + dependencies: + lodash "^4.17.21" + string-natural-compare "^3.0.1" + +eslint-plugin-import@^2.25.3: + version "2.25.4" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz" + integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.2" + has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" + minimatch "^3.0.4" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.12.0" + +eslint-plugin-jest@^25.3.0: + version "25.7.0" + resolved "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-25.7.0.tgz" + integrity sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ== + dependencies: + "@typescript-eslint/experimental-utils" "^5.0.0" + +eslint-plugin-jsx-a11y@^6.5.1: + version "6.5.1" + resolved "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.5.1.tgz" + integrity sha512-sVCFKX9fllURnXT2JwLN5Qgo24Ug5NF6dxhkmxsMEUZhXRcGg+X3e1JbJ84YePQKBl5E0ZjAH5Q4rkdcGY99+g== + dependencies: + "@babel/runtime" "^7.16.3" + aria-query "^4.2.2" + array-includes "^3.1.4" + ast-types-flow "^0.0.7" + axe-core "^4.3.5" + axobject-query "^2.2.0" + damerau-levenshtein "^1.0.7" + emoji-regex "^9.2.2" + has "^1.0.3" + jsx-ast-utils "^3.2.1" + language-tags "^1.0.5" + minimatch "^3.0.4" + +eslint-plugin-prettier@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.0.0.tgz" + integrity sha512-98MqmCJ7vJodoQK359bqQWaxOE0CS8paAz/GgjaZLyex4TTk3g9HugoO89EqWCrFiOqn9EVvcoo7gZzONCWVwQ== + dependencies: + prettier-linter-helpers "^1.0.0" + +eslint-plugin-react-hooks@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.3.0.tgz" + integrity sha512-XslZy0LnMn+84NEG9jSGR6eGqaZB3133L8xewQo3fQagbQuGt7a63gf+P1NGKZavEYEC3UXaWEAA/AqDkuN6xA== + +eslint-plugin-react@^7.27.1: + version "7.28.0" + resolved "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.28.0.tgz" + integrity sha512-IOlFIRHzWfEQQKcAD4iyYDndHwTQiCMcJVJjxempf203jnNLUnW34AXLrV33+nEXoifJE2ZEGmcjKPL8957eSw== + dependencies: + array-includes "^3.1.4" + array.prototype.flatmap "^1.2.5" + doctrine "^2.1.0" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.0.4" + object.entries "^1.1.5" + object.fromentries "^2.0.5" + object.hasown "^1.1.0" + object.values "^1.1.5" + prop-types "^15.7.2" + resolve "^2.0.0-next.3" + semver "^6.3.0" + string.prototype.matchall "^4.0.6" + +eslint-plugin-testing-library@^5.0.1: + version "5.0.4" + resolved "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-5.0.4.tgz" + integrity sha512-zA/NfAENCsJXujvwwiap5gsqLp2U6X7m2XA5nOksl4zzb6GpUmRNAleCll58rEP0brFVj7DZBprlIlMGIhoC7Q== + dependencies: + "@typescript-eslint/experimental-utils" "^5.9.0" + +eslint-scope@5.1.1, eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.0: + version "7.1.0" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.0.tgz" + integrity sha512-aWwkhnS0qAXqNOgKOK0dJ2nvzEbhEvpy8OlJ9kZ0FeZnA6zpjv1/Vei+puGFFX7zkPCkHHXb7IDX3A+7yPrRWg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^2.0.0, eslint-visitor-keys@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.2.0.tgz" + integrity sha512-IOzT0X126zn7ALX0dwFiUQEdsfzrm4+ISsQS8nukaJXwEyYKRSnEIIDULYg1mCtGp7UUXgfGl7BIolXREQK+XQ== + +eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint-visitor-keys@^3.4.1: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint-webpack-plugin@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/eslint-webpack-plugin/-/eslint-webpack-plugin-3.1.1.tgz" + integrity sha512-xSucskTN9tOkfW7so4EaiFIkulWLXwCB/15H917lR6pTv0Zot6/fetFucmENRb7J5whVSFKIvwnrnsa78SG2yg== + dependencies: + "@types/eslint" "^7.28.2" + jest-worker "^27.3.1" + micromatch "^4.0.4" + normalize-path "^3.0.0" + schema-utils "^3.1.1" + +eslint@^8.13.0: + version "8.13.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.13.0.tgz" + integrity sha512-D+Xei61eInqauAyTJ6C0q6x9mx7kTUC1KZ0m0LSEexR0V+e94K12LmWX076ZIsldwfQ2RONdaJe0re0TRGQbRQ== + dependencies: + "@eslint/eslintrc" "^1.2.1" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +eslint@^8.3.0: + version "8.7.0" + resolved "https://registry.npmjs.org/eslint/-/eslint-8.7.0.tgz" + integrity sha512-ifHYzkBGrzS2iDU7KjhCAVMGCvF6M3Xfs8X8b37cgrUlDt6bWRTpRh6T/gtSXv1HJ/BUGgmjvNvOEGu85Iif7w== + dependencies: + "@eslint/eslintrc" "^1.0.5" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.0" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.2.0" + espree "^9.3.0" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +esm@^3.2.25: + version "3.2.25" + resolved "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz" + integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA== + +espree@^9.2.0, espree@^9.3.0: + version "9.3.0" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.0.tgz" + integrity sha512-d/5nCsb0JcqsSEeQzFZ8DH1RmxPcglRWh24EFTlUEmCKoehXGdpsx0RkHDubqUI8LSAIKMQp4r9SzQ3n+sm4HQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.1.0" + +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.npmjs.org/espree/-/espree-9.3.1.tgz" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.3.0" + +esprima@^4.0.0, esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-1.0.1.tgz" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +estree-walker@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" + integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== + dependencies: + "@types/estree" "^1.0.0" + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter3@^4.0.0, eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.npmjs.org/events/-/events-3.3.0.tgz" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + 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.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +execa@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-8.0.1.tgz#51f6a5943b580f963c3ca9c6321796db8cc39b8c" + integrity sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^8.0.1" + human-signals "^5.0.0" + is-stream "^3.0.0" + merge-stream "^2.0.0" + npm-run-path "^5.1.0" + onetime "^6.0.0" + signal-exit "^4.1.0" + strip-final-newline "^3.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expect@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/expect/-/expect-27.4.6.tgz" + integrity sha512-1M/0kAALIaj5LaG66sFJTbRsWTADnylly82cu4bspI0nl+pgP4E6Bh/aqdHlTUjul06K7xQnnrAoqfxVU0+/ag== + dependencies: + "@jest/types" "^27.4.2" + jest-get-type "^27.4.0" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + +express-async-handler@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/express-async-handler/-/express-async-handler-1.2.0.tgz#ffc9896061d90f8d2e71a2d2b8668db5b0934391" + integrity sha512-rCSVtPXRmQSW8rmik/AIb2P0op6l7r1fMW538yyvTMltCO4xQEWMmobfrIxN2V1/mVrgxB8Az3reYF6yUZw37w== + +express-basic-auth@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/express-basic-auth/-/express-basic-auth-1.2.1.tgz" + integrity sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA== + dependencies: + basic-auth "^2.0.1" + +express-graphql@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/express-graphql/-/express-graphql-0.12.0.tgz" + integrity sha512-DwYaJQy0amdy3pgNtiTDuGGM2BLdj+YO2SgbKoLliCfuHv3VVTt7vNG/ZqK2hRYjtYHE2t2KB705EU94mE64zg== + dependencies: + accepts "^1.3.7" + content-type "^1.0.4" + http-errors "1.8.0" + raw-body "^2.4.1" + +express@4.17.3, express@^4.17.1: + version "4.17.3" + resolved "https://registry.npmjs.org/express/-/express-4.17.3.tgz" + integrity sha512-yuSQpz5I+Ch7gFrPCk4/c+dIBKlQUxtgwqzph132bsT6qhuzss6I8cLJQz7B3rFblzd6wtcI0ZbGltH/C4LjUg== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.19.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.4.2" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.9.7" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.17.2" + serve-static "1.14.2" + setprototypeof "1.2.0" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +express@~4.18.2: + version "4.18.2" + resolved "https://registry.npmjs.org/express/-/express-4.18.2.tgz" + integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.1" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.5.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.1" + resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz" + integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-diff@^1.1.2: + version "1.2.0" + resolved "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz" + integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6, fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fast-xml-parser@^4.0.11: + version "4.2.5" + resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.2.5.tgz" + integrity sha512-B9/wizE4WngqQftFPmdaMYlXoJlJOYxGQOanC77fq9k8+Z0v5dDSVh+3glErdIROP//s/jgb7ZuxKfB8nVyo0g== + dependencies: + strnum "^1.0.5" + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.npmjs.org/fecha/-/fecha-4.2.1.tgz" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +filelist@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz" + integrity sha512-z7O0IS8Plc39rTCq6i6iHxk43duYOn8uFJiWSewIq0Bww1RNybVHSCjahmcC87ZqAm4OTvFzlzeGu3XAzG1ctQ== + dependencies: + minimatch "^3.0.4" + +filesize@^8.0.6: + version "8.0.7" + resolved "https://registry.npmjs.org/filesize/-/filesize-8.0.7.tgz" + integrity sha512-pjmC+bkIF8XI7fWaH8KxHcZL3DPybs1roSKP4rKDvy20tAWwIObE4+JIseG2byfGKhud5ZnM4YSGKBz7Sh0ndQ== + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +filter-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz" + integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ== + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-root@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz" + integrity sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng== + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.4" + resolved "https://registry.npmjs.org/flatted/-/flatted-3.2.4.tgz" + integrity sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw== + +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + +follow-redirects@^1.0.0, follow-redirects@^1.14.8, follow-redirects@^1.15.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +fork-ts-checker-webpack-plugin@^6.5.0: + version "6.5.0" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-6.5.0.tgz" + integrity sha512-cS178Y+xxtIjEUorcHddKS7yCMlrDPV31mt47blKKRfMd70Kxu5xruAFE2o9sDY6wVC5deuob/u/alD04YYHnw== + dependencies: + "@babel/code-frame" "^7.8.3" + "@types/json-schema" "^7.0.5" + chalk "^4.1.0" + chokidar "^3.4.2" + cosmiconfig "^6.0.0" + deepmerge "^4.2.2" + fs-extra "^9.0.0" + glob "^7.1.6" + memfs "^3.1.2" + minimatch "^3.0.4" + schema-utils "2.7.0" + semver "^7.3.2" + tapable "^1.0.0" + +form-data@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz" + integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +formidable@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.1.2.tgz#fa973a2bec150e4ce7cac15589d7a25fc30ebd89" + integrity sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g== + dependencies: + dezalgo "^1.0.4" + hexoid "^1.0.0" + once "^1.4.0" + qs "^6.11.0" + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fraction.js@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.2.tgz" + integrity sha512-o2RiJQ6DZaR/5+Si0qJUIy637QMRudSi9kU/FFzx9EZazrIdnBgpU+3sEWCxAVhH2RtxW2Oz+T4p2o8uOPVcgA== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs-extra@^10.0.0: + version "10.0.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.0.tgz" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs-minipass@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz" + integrity sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA== + dependencies: + minipass "^2.6.0" + +fs-minipass@^2.0.0, fs-minipass@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz" + integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== + dependencies: + minipass "^3.0.0" + +fs-minipass@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-3.0.3.tgz#79a85981c4dc120065e96f62086bf6f9dc26cc54" + integrity sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw== + dependencies: + minipass "^7.0.3" + +fs-monkey@1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.3.tgz" + integrity sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q== + +fs-monkey@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.5.tgz#fe450175f0db0d7ea758102e1d84096acb925788" + integrity sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +gauge@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz" + integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.2" + console-control-strings "^1.0.0" + has-unicode "^2.0.1" + object-assign "^4.1.1" + signal-exit "^3.0.0" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.2" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz" + integrity sha1-LANAXHU4w51+s3sxcCLjJfsBi/c= + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.1, get-func-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.2.tgz#0d7cf20cd13fda808669ffa88f4ffc7a3943fc41" + integrity sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ== + +get-intrinsic@^1.0.2: + version "1.1.2" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz" + integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.3" + +get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-package-type@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz" + integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== + +get-pkg-repo@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-4.2.1.tgz" + integrity sha512-2+QbHjFRfGB74v/pYWjd5OhU3TDIC2Gv/YKUTk/tCvAz0pkn/Mz6P3uByuBimLOcPvN2jYdScl3xGFSrx0jEcA== + dependencies: + "@hutson/parse-repository-url" "^3.0.0" + hosted-git-info "^4.0.0" + through2 "^2.0.0" + yargs "^16.2.0" + +get-port@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-stream@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-stream@^5.1.0: + version "5.2.0" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz" + integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== + dependencies: + pump "^3.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +get-stream@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-8.0.1.tgz#def9dfd71742cd7754a7761ed43749a27d02eca2" + integrity sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA== + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +getopts@2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz" + integrity sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA== + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +git-raw-commits@^2.0.8: + version "2.0.11" + resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.11.tgz" + integrity sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A== + dependencies: + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" + +git-remote-origin-url@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz" + integrity sha1-UoJlna4hBxRaERJhEq0yFuxfpl8= + dependencies: + gitconfiglocal "^1.0.0" + pify "^2.3.0" + +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== + dependencies: + meow "^8.0.0" + semver "^6.0.0" + +git-up@^4.0.0: + version "4.0.5" + resolved "https://registry.npmjs.org/git-up/-/git-up-4.0.5.tgz" + integrity sha512-YUvVDg/vX3d0syBsk/CKUTib0srcQME0JyHkL5BaYdwLsiCslPWmDSi8PUMo9pXYjrryMcmsCoCgsTpSCJEQaA== + dependencies: + is-ssh "^1.3.0" + parse-url "^6.0.0" + +git-url-parse@^11.4.4: + version "11.6.0" + resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.6.0.tgz" + integrity sha512-WWUxvJs5HsyHL6L08wOusa/IXYtMuCAhrMmnTjQPpBU0TTHyDhnOATNH3xNQz7YOQUsqIIPTGr4xiVti1Hsk5g== + dependencies: + git-up "^4.0.0" + +gitconfiglocal@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz" + integrity sha1-QdBF84UaXqiPA/JMocYXgRRGS5s= + dependencies: + ini "^1.3.2" + +glob-parent@^5.1.1, glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1, glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^10.2.2, glob@^10.3.10: + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.6" + minimatch "^9.0.1" + minipass "^7.0.4" + path-scurry "^1.10.2" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: + version "7.2.0" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^8.0.3: + version "8.0.3" + resolved "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz" + integrity sha512-ull455NHSHI/Y1FqGaaYFaLGkNMMJbavMrEGFXG/PGrg6y7sutWHUHrz6gy6WEBH6akM1M414dWKCNs+IhKdiQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + +global-dirs@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.0.tgz" + integrity sha512-v8ho2DS5RiCjftj1nD9NmnfaOzTdud7RRnVd9kFNOjqZbISlx5DQ+OrTkywgd0dIt7oFCvKetZSHoHcP3sDdiA== + dependencies: + ini "2.0.0" + +global-modules@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz" + integrity sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A== + dependencies: + global-prefix "^3.0.0" + +global-prefix@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz" + integrity sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg== + dependencies: + ini "^1.3.5" + kind-of "^6.0.2" + which "^1.3.1" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.6.0, globals@^13.9.0: + version "13.12.0" + resolved "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz" + integrity sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.1, globby@^11.0.2, globby@^11.0.3, globby@^11.0.4, globby@^11.1.0: + version "11.1.0" + resolved "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +goober@^2.0.33: + version "2.1.13" + resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c" + integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ== + +got@^9.6.0: + version "9.6.0" + resolved "https://registry.npmjs.org/got/-/got-9.6.0.tgz" + 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" + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6, graceful-fs@^4.2.9: + version "4.2.9" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +graphql-executor@0.0.18: + version "0.0.18" + resolved "https://registry.npmjs.org/graphql-executor/-/graphql-executor-0.0.18.tgz" + integrity sha512-upUSl7tfZCZ5dWG1XkOvpG70Yk3duZKcCoi/uJso4WxJVT6KIrcK4nZ4+2X/hzx46pL8wAukgYHY6iNmocRN+g== + +graphql-middleware@^6.1.15: + version "6.1.15" + resolved "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-6.1.15.tgz" + integrity sha512-JiLuIM48EE3QLcr79K0VCCHqMt6c23esLlkZv2Nr9a/yHnv6eU9DKV9eXARl+wV9m4LkT9ZCg4cIamIa9vPidQ== + dependencies: + "@graphql-tools/delegate" "^8.5.1" + "@graphql-tools/schema" "^8.3.2" + +graphql-shield@^7.5.0: + version "7.5.0" + resolved "https://registry.npmjs.org/graphql-shield/-/graphql-shield-7.5.0.tgz" + integrity sha512-T1A6OreOe/dHDk/1Qg3AHCrKLmTkDJ3fPFGYpSOmUbYXyDnjubK4J5ab5FjHdKHK5fWQRZNTvA0SrBObYsyfaw== + dependencies: + "@types/yup" "0.29.11" + object-hash "^2.0.3" + yup "^0.31.0" + +graphql-tag@^2.12.3, graphql-tag@^2.12.6: + version "2.12.6" + resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz" + integrity sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg== + dependencies: + tslib "^2.1.0" + +graphql-tools@^8.2.0: + version "8.2.0" + resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-8.2.0.tgz" + integrity sha512-9axT/0exEzVCk+vMPykOPannlrA4VQNo6nuWgh25IJ5arPf92OKxvjSHAbm7dQIFmcWxE0hVvyD2rWHjDqZCgQ== + dependencies: + "@graphql-tools/schema" "^8.2.0" + tslib "~2.3.0" + optionalDependencies: + "@apollo/client" "~3.2.5 || ~3.3.0 || ~3.4.0" + +graphql@^15.6.0: + version "15.8.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.8.0.tgz" + integrity sha512-5gghUc24tP9HRznNpV2+FIoq3xKkj5dTQqf4v0CpdPbFVwFkWoxOM+o+2OC9ZSvjEMTjfmG9QT+gcvggTwW1zw== + +gzip-size@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz" + integrity sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q== + dependencies: + duplexer "^0.1.2" + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +handlebars@^4.7.7: + version "4.7.7" + resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz" + integrity sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA== + dependencies: + minimist "^1.2.5" + neo-async "^2.6.0" + source-map "^0.6.1" + wordwrap "^1.0.0" + optionalDependencies: + uglify-js "^3.1.4" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.5" + resolved "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz" + integrity sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w== + dependencies: + ajv "^6.12.3" + har-schema "^2.0.0" + +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + +harmony-reflect@^1.4.6: + version "1.6.2" + resolved "https://registry.npmjs.org/harmony-reflect/-/harmony-reflect-1.6.2.tgz" + integrity sha512-HIp/n38R9kQjDEziXyDTuW3vvoxxyxjxFzXLrBr18uB47GnSt+G9D29fqrpM5ZkspMcPICud3XsBJQ4Y2URg8g== + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has-unicode@^2.0.0, has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz" + integrity sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk= + +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/has/-/has-1.0.3.tgz" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0, he@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +hexoid@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18" + integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g== + +history@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/history/-/history-5.2.0.tgz" + integrity sha512-uPSF6lAJb3nSePJ43hN3eKj1dTWpN9gMod0ZssbFTIsen+WehTmEadgL+kg78xLJFdRfrrC//SavDzmRVdE+Ig== + dependencies: + "@babel/runtime" "^7.7.6" + +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + +hoopy@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz" + integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== + +hosted-git-info@^2.1.4: + version "2.8.9" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz" + integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== + +hosted-git-info@^4.0.0, hosted-git-info@^4.0.1: + version "4.1.0" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz" + integrity sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA== + dependencies: + lru-cache "^6.0.0" + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz" + integrity sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI= + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-encoding-sniffer@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz" + integrity sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ== + dependencies: + whatwg-encoding "^1.0.5" + +html-entities@^2.1.0, html-entities@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-2.3.2.tgz" + integrity sha512-c3Ab/url5ksaT0WyleslpBEthOzWhrjQbg75y7XUsfSzi3Dgzt0l8w5e7DylRn15MTlMMD58dTfzddNS2kcAjQ== + +html-escaper@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz" + integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== + +html-minifier-terser@^6.0.2: + version "6.1.0" + resolved "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz" + integrity sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw== + dependencies: + camel-case "^4.1.2" + clean-css "^5.2.2" + commander "^8.3.0" + he "^1.2.0" + param-case "^3.0.4" + relateurl "^0.2.7" + terser "^5.10.0" + +html-webpack-plugin@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.5.0.tgz" + integrity sha512-sy88PC2cRTVxvETRgUHFrL4No3UxvcH8G1NepGhqaTT+GXN2kTamqasot0inS5hXeg1cMbFDt27zzo9p35lZVw== + dependencies: + "@types/html-minifier-terser" "^6.0.0" + html-minifier-terser "^6.0.2" + lodash "^4.17.21" + pretty-error "^4.0.0" + tapable "^2.0.0" + +htmlparser2@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz" + integrity sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.5.2" + entities "^2.0.0" + +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0, http-cache-semantics@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz" + integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz" + integrity sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc= + +http-errors@1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@1.8.0: + version "1.8.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.0.tgz" + integrity sha512-4I8r0C5JDhT5VkvI47QktDW75rNlGVsUf/8hzjCC/wkWI/jdTRmBb9aI7erSG82r1bjKY3F6k28WnsVxB1C73A== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@1.8.1: + version "1.8.1" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz" + 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-errors@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2, http-errors@~1.6.3: + version "1.6.3" + resolved "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.5" + resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.5.tgz" + integrity sha512-x+JVEkO2PoM8qqpbPbOL3cqHPwerep7OwzK7Ay+sMQjKzaKCqWvjoXm5tqMP9tXWWTnTzAjIhXg+J99XYuPhPA== + +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + +http-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz" + integrity sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ== + dependencies: + agent-base "^7.1.0" + debug "^4.3.4" + +http-proxy-middleware@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.1.tgz" + integrity sha512-cfaXRVoZxSed/BmkA7SwBVNI9Kj7HFltaE5rqYOub5kWzWZ+gofV2koVN1j2rMW7pEfSSlCHGJ31xmuyFyfLOg== + dependencies: + "@types/http-proxy" "^1.17.5" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + 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.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +https-proxy-agent@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.1.tgz" + integrity sha512-Eun8zV0kcYS1g19r78osiQLEFIRspRUDd9tIfBCTBPBeMieF/EsJNL8VI3xOIdYRDEkjQnqOYPsZ2DsWsVsFwQ== + dependencies: + agent-base "^7.0.2" + debug "4" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +human-signals@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-5.0.0.tgz#42665a284f9ae0dade3ba41ebc37eb4b852f3a28" + integrity sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ== + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= + dependencies: + ms "^2.0.0" + +iconv-lite@0.4.24, iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +iconv-lite@^0.6.2, iconv-lite@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + +icss-utils@^5.0.0, icss-utils@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz" + integrity sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA== + +idb@^6.1.4: + version "6.1.5" + resolved "https://registry.npmjs.org/idb/-/idb-6.1.5.tgz" + integrity sha512-IJtugpKkiVXQn5Y+LteyBCNk1N8xpGV3wWZk9EVtZWH8DYkjBn0bX1XnGP9RkyZF0sAcywa6unHqSWKe7q4LGw== + +identity-obj-proxy@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/identity-obj-proxy/-/identity-obj-proxy-3.0.0.tgz" + integrity sha1-lNK9qWCERT7zb7xarsN+D3nx/BQ= + dependencies: + harmony-reflect "^1.4.6" + +ignore-by-default@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz" + integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= + +ignore-walk@^3.0.3: + version "3.0.4" + resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz" + integrity sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ== + dependencies: + minimatch "^3.0.4" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +immer@^9.0.6, immer@^9.0.7: + version "9.0.12" + resolved "https://registry.npmjs.org/immer/-/immer-9.0.12.tgz" + integrity sha512-lk7UNmSbAukB5B6dh9fnh5D0bJTOFKxVg2cyJWTYrWRfhLrLMBquONcUs3aFq507hNoIZEDDh8lb8UtOizSMhA== + +import-fresh@^3.0.0, import-fresh@^3.1.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +import-lazy@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz" + integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz" + integrity sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA== + +ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: + version "1.3.8" + resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz" + integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== + +init-package-json@^2.0.2: + version "2.0.5" + resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.5.tgz" + integrity sha512-u1uGAtEFu3VA6HNl/yUWw57jmKEMx8SKOxHhxjGnOFUiIlFnohKDFg4ZrPpv9wWqk44nDxGJAtqjdQFm+9XXQA== + dependencies: + npm-package-arg "^8.1.5" + promzard "^0.3.0" + read "~1.0.1" + read-package-json "^4.1.1" + semver "^7.3.5" + validate-npm-package-license "^3.0.4" + validate-npm-package-name "^3.0.0" + +inquirer@^7.3.3: + version "7.3.3" + resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz" + integrity sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.19" + mute-stream "0.0.8" + run-async "^2.4.0" + rxjs "^6.6.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +intl-messageformat@9.11.2: + version "9.11.2" + resolved "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.11.2.tgz" + integrity sha512-4wsinP2ObVK1Rz5C4121lgVeHeOCW32FOsqcVXtJNdlow+NypJKmnrije9rOc0bKxPwtto9IkXdgakXUmYXVHw== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/fast-memoize" "1.2.1" + "@formatjs/icu-messageformat-parser" "2.0.16" + tslib "^2.1.0" + +ioredis@^4.28.5: + version "4.28.5" + resolved "https://registry.npmjs.org/ioredis/-/ioredis-4.28.5.tgz" + integrity sha512-3GYo0GJtLqgNXj4YhrisLaNNvWSNwSS2wS4OELGfGxH8I69+XfNdnmV1AyN+ZqMh0i7eX+SWjrwFKDBDgfBC1A== + dependencies: + cluster-key-slot "^1.1.0" + debug "^4.3.1" + denque "^1.1.0" + lodash.defaults "^4.2.0" + lodash.flatten "^4.4.0" + lodash.isarguments "^3.1.0" + p-map "^2.1.0" + redis-commands "1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + +ioredis@^5.2.2: + version "5.2.3" + resolved "https://registry.npmjs.org/ioredis/-/ioredis-5.2.3.tgz" + integrity sha512-gQNcMF23/NpvjCaa1b5YycUyQJ9rBNH2xP94LWinNpodMWVUPP5Ai/xXANn/SM7gfIvI62B5CCvZxhg5pOgyMw== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.0.1" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +ip@^1.1.0, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.0.1.tgz" + integrity sha512-1qTgH9NG+IIJ4yfKs2e6Pp1bZg8wbDbKHT21HrLIeYBTRLgMYKnMTPAuI3Lcs61nfx5h1xlXnbJtH1kX5/d/ng== + +is-arguments@^1.0.4: + version "1.1.1" + resolved "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz" + integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-core-module@^2.2.0, is-core-module@^2.5.0, is-core-module@^2.8.0, is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.1.tgz" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +is-core-module@^2.9.0: + version "2.10.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz" + integrity sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-hotkey@^0.1.6: + version "0.1.8" + resolved "https://registry.npmjs.org/is-hotkey/-/is-hotkey-0.1.8.tgz" + integrity sha512-qs3NZ1INIS+H+yeo7cD9pDfwYV/jqRh1JG9S9zYrNudkoUQg7OL7ziXqRKu+InFjUIDoP2o6HIkLYMh1pcWgyQ== + +is-installed-globally@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz" + integrity sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ== + dependencies: + global-dirs "^3.0.0" + is-path-inside "^3.0.2" + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz" + integrity sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE= + +is-negative-zero@^2.0.1: + version "2.0.2" + resolved "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-npm@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-npm/-/is-npm-5.0.0.tgz" + integrity sha512-WW/rQLOazUq+ST/bCAVBp/2oMERWLsR7OrKyt052dNDk4DHcDE0/7QSXITlmi+VBcV13DfIbysG3tZJm5RfdBA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz" + integrity sha1-PkcprB9f3gJc19g6iW2rn09n2w8= + +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-cwd@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz" + integrity sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ== + +is-path-inside@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^1.0.0, is-plain-obj@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz" + integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= + +is-plain-obj@^2.0.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz" + integrity sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-plain-object@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz" + integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== + +is-potential-custom-element-name@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz" + integrity sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== + +is-regex@^1.0.4, is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz" + integrity sha1-/S2INUXEa6xaYz57mgnof6LLUGk= + +is-retry-allowed@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz" + integrity sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg== + +is-root@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/is-root/-/is-root-2.1.0.tgz" + integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg== + +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + +is-ssh@^1.3.0: + version "1.4.0" + resolved "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz" + integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== + dependencies: + protocols "^2.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-stream@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" + integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-text-path@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= + dependencies: + text-extensions "^1.0.0" + +is-typedarray@^1.0.0, is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isexe@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-3.1.1.tgz#4a407e2bd78ddfb14bea0c27c6f7072dde775f0d" + integrity sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz" + integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== + +istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.1.0.tgz" + integrity sha512-czwUz525rkOFDJxfKK6mYfIs9zBKILyrZQxjz3ABhjQXhbhFsSbo1HW/BFcsDnfJYJWA6thRR5/TUY2qs5W99Q== + dependencies: + "@babel/core" "^7.12.3" + "@babel/parser" "^7.14.7" + "@istanbuljs/schema" "^0.1.2" + istanbul-lib-coverage "^3.2.0" + semver "^6.3.0" + +istanbul-lib-report@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz" + integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== + dependencies: + istanbul-lib-coverage "^3.0.0" + make-dir "^3.0.0" + supports-color "^7.1.0" + +istanbul-lib-source-maps@^4.0.0: + version "4.0.1" + resolved "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz" + integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^3.0.0" + source-map "^0.6.1" + +istanbul-reports@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.3.tgz" + integrity sha512-x9LtDVtfm/t1GFiLl3NffC7hz+I1ragvgX1P/Lg1NlIagifZDKUkuuaAxH/qpwj2IuEfD8G2Bs/UKp+sZ/pKkg== + dependencies: + html-escaper "^2.0.0" + istanbul-lib-report "^3.0.0" + +jackspeak@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jake@^10.6.1: + version "10.8.2" + resolved "https://registry.npmjs.org/jake/-/jake-10.8.2.tgz" + integrity sha512-eLpKyrfG3mzvGE2Du8VoPbeSkRry093+tyNjdYaBbJS9v17knImYGNXQCUV0gLxQtF82m3E8iRb/wdSQZLoq7A== + dependencies: + async "0.9.x" + chalk "^2.4.2" + filelist "^1.0.1" + minimatch "^3.0.4" + +jest-changed-files@^27.4.2: + version "27.4.2" + resolved "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.4.2.tgz" + integrity sha512-/9x8MjekuzUQoPjDHbBiXbNEBauhrPU2ct7m8TfCg69ywt1y/N+yYwGh3gCpnqUS3klYWDU/lSNgv+JhoD2k1A== + dependencies: + "@jest/types" "^27.4.2" + execa "^5.0.0" + throat "^6.0.1" + +jest-circus@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-circus/-/jest-circus-27.4.6.tgz" + integrity sha512-UA7AI5HZrW4wRM72Ro80uRR2Fg+7nR0GESbSI/2M+ambbzVuA63mn5T1p3Z/wlhntzGpIG1xx78GP2YIkf6PhQ== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + dedent "^0.7.0" + expect "^27.4.6" + is-generator-fn "^2.0.0" + jest-each "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + pretty-format "^27.4.6" + slash "^3.0.0" + stack-utils "^2.0.3" + throat "^6.0.1" + +jest-cli@^27.4.7: + version "27.4.7" + resolved "https://registry.npmjs.org/jest-cli/-/jest-cli-27.4.7.tgz" + integrity sha512-zREYhvjjqe1KsGV15mdnxjThKNDgza1fhDT+iUsXWLCq3sxe9w5xnvyctcYVT5PcdLSjv7Y5dCwTS3FCF1tiuw== + dependencies: + "@jest/core" "^27.4.7" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" + chalk "^4.0.0" + exit "^0.1.2" + graceful-fs "^4.2.4" + import-local "^3.0.2" + jest-config "^27.4.7" + jest-util "^27.4.2" + jest-validate "^27.4.6" + prompts "^2.0.1" + yargs "^16.2.0" + +jest-config@^27.4.7: + version "27.4.7" + resolved "https://registry.npmjs.org/jest-config/-/jest-config-27.4.7.tgz" + integrity sha512-xz/o/KJJEedHMrIY9v2ParIoYSrSVY6IVeE4z5Z3i101GoA5XgfbJz+1C8EYPsv7u7f39dS8F9v46BHDhn0vlw== + dependencies: + "@babel/core" "^7.8.0" + "@jest/test-sequencer" "^27.4.6" + "@jest/types" "^27.4.2" + babel-jest "^27.4.6" + chalk "^4.0.0" + ci-info "^3.2.0" + deepmerge "^4.2.2" + glob "^7.1.1" + graceful-fs "^4.2.4" + jest-circus "^27.4.6" + jest-environment-jsdom "^27.4.6" + jest-environment-node "^27.4.6" + jest-get-type "^27.4.0" + jest-jasmine2 "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-runner "^27.4.6" + jest-util "^27.4.2" + jest-validate "^27.4.6" + micromatch "^4.0.4" + pretty-format "^27.4.6" + slash "^3.0.0" + +jest-diff@^27.0.0, jest-diff@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.4.6.tgz" + integrity sha512-zjaB0sh0Lb13VyPsd92V7HkqF6yKRH9vm33rwBt7rPYrpQvS1nCvlIy2pICbKta+ZjWngYLNn4cCK4nyZkjS/w== + dependencies: + chalk "^4.0.0" + diff-sequences "^27.4.0" + jest-get-type "^27.4.0" + pretty-format "^27.4.6" + +jest-docblock@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/jest-docblock/-/jest-docblock-27.4.0.tgz" + integrity sha512-7TBazUdCKGV7svZ+gh7C8esAnweJoG+SvcF6Cjqj4l17zA2q1cMwx2JObSioubk317H+cjcHgP+7fTs60paulg== + dependencies: + detect-newline "^3.0.0" + +jest-each@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-each/-/jest-each-27.4.6.tgz" + integrity sha512-n6QDq8y2Hsmn22tRkgAk+z6MCX7MeVlAzxmZDshfS2jLcaBlyhpF3tZSJLR+kXmh23GEvS0ojMR8i6ZeRvpQcA== + dependencies: + "@jest/types" "^27.4.2" + chalk "^4.0.0" + jest-get-type "^27.4.0" + jest-util "^27.4.2" + pretty-format "^27.4.6" + +jest-environment-jsdom@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.4.6.tgz" + integrity sha512-o3dx5p/kHPbUlRvSNjypEcEtgs6LmvESMzgRFQE6c+Prwl2JLA4RZ7qAnxc5VM8kutsGRTB15jXeeSbJsKN9iA== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + jest-mock "^27.4.6" + jest-util "^27.4.2" + jsdom "^16.6.0" + +jest-environment-node@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.4.6.tgz" + integrity sha512-yfHlZ9m+kzTKZV0hVfhVu6GuDxKAYeFHrfulmy7Jxwsq4V7+ZK7f+c0XP/tbVDMQW7E4neG2u147hFkuVz0MlQ== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + jest-mock "^27.4.6" + jest-util "^27.4.2" + +jest-get-type@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.4.0.tgz" + integrity sha512-tk9o+ld5TWq41DkK14L4wox4s2D9MtTpKaAVzXfr5CUKm5ZK2ExcaFE0qls2W71zE/6R2TxxrK9w2r6svAFDBQ== + +jest-haste-map@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-27.4.6.tgz" + integrity sha512-0tNpgxg7BKurZeFkIOvGCkbmOHbLFf4LUQOxrQSMjvrQaQe3l6E8x6jYC1NuWkGo5WDdbr8FEzUxV2+LWNawKQ== + dependencies: + "@jest/types" "^27.4.2" + "@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 "^27.4.0" + jest-serializer "^27.4.0" + jest-util "^27.4.2" + jest-worker "^27.4.6" + micromatch "^4.0.4" + walker "^1.0.7" + optionalDependencies: + fsevents "^2.3.2" + +jest-jasmine2@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-27.4.6.tgz" + integrity sha512-uAGNXF644I/whzhsf7/qf74gqy9OuhvJ0XYp8SDecX2ooGeaPnmJMjXjKt0mqh1Rl5dtRGxJgNrHlBQIBfS5Nw== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + co "^4.6.0" + expect "^27.4.6" + is-generator-fn "^2.0.0" + jest-each "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-runtime "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + pretty-format "^27.4.6" + throat "^6.0.1" + +jest-leak-detector@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-27.4.6.tgz" + integrity sha512-kkaGixDf9R7CjHm2pOzfTxZTQQQ2gHTIWKY/JZSiYTc90bZp8kSZnUMS3uLAfwTZwc0tcMRoEX74e14LG1WapA== + dependencies: + jest-get-type "^27.4.0" + pretty-format "^27.4.6" + +jest-matcher-utils@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-27.4.6.tgz" + integrity sha512-XD4PKT3Wn1LQnRAq7ZsTI0VRuEc9OrCPFiO1XL7bftTGmfNF0DcEwMHRgqiu7NGf8ZoZDREpGrCniDkjt79WbA== + dependencies: + chalk "^4.0.0" + jest-diff "^27.4.6" + jest-get-type "^27.4.0" + pretty-format "^27.4.6" + +jest-message-util@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-message-util/-/jest-message-util-27.4.6.tgz" + integrity sha512-0p5szriFU0U74czRSFjH6RyS7UYIAkn/ntwMuOwTGWrQIOh5NzXXrq72LOqIkJKKvFbPq+byZKuBz78fjBERBA== + dependencies: + "@babel/code-frame" "^7.12.13" + "@jest/types" "^27.4.2" + "@types/stack-utils" "^2.0.0" + chalk "^4.0.0" + graceful-fs "^4.2.4" + micromatch "^4.0.4" + pretty-format "^27.4.6" + slash "^3.0.0" + stack-utils "^2.0.3" + +jest-mock@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-mock/-/jest-mock-27.4.6.tgz" + integrity sha512-kvojdYRkst8iVSZ1EJ+vc1RRD9llueBjKzXzeCytH3dMM7zvPV/ULcfI2nr0v0VUgm3Bjt3hBCQvOeaBz+ZTHw== + dependencies: + "@jest/types" "^27.4.2" + "@types/node" "*" + +jest-pnp-resolver@^1.2.2: + version "1.2.2" + resolved "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz" + integrity sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w== + +jest-regex-util@^27.0.0, jest-regex-util@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-27.4.0.tgz" + integrity sha512-WeCpMpNnqJYMQoOjm1nTtsgbR4XHAk1u00qDoNBQoykM280+/TmgA5Qh5giC1ecy6a5d4hbSsHzpBtu5yvlbEg== + +jest-resolve-dependencies@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-27.4.6.tgz" + integrity sha512-W85uJZcFXEVZ7+MZqIPCscdjuctruNGXUZ3OHSXOfXR9ITgbUKeHj+uGcies+0SsvI5GtUfTw4dY7u9qjTvQOw== + dependencies: + "@jest/types" "^27.4.2" + jest-regex-util "^27.4.0" + jest-snapshot "^27.4.6" + +jest-resolve@^27.4.2, jest-resolve@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-resolve/-/jest-resolve-27.4.6.tgz" + integrity sha512-SFfITVApqtirbITKFAO7jOVN45UgFzcRdQanOFzjnbd+CACDoyeX7206JyU92l4cRr73+Qy/TlW51+4vHGt+zw== + dependencies: + "@jest/types" "^27.4.2" + chalk "^4.0.0" + graceful-fs "^4.2.4" + jest-haste-map "^27.4.6" + jest-pnp-resolver "^1.2.2" + jest-util "^27.4.2" + jest-validate "^27.4.6" + resolve "^1.20.0" + resolve.exports "^1.1.0" + slash "^3.0.0" + +jest-runner@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-runner/-/jest-runner-27.4.6.tgz" + integrity sha512-IDeFt2SG4DzqalYBZRgbbPmpwV3X0DcntjezPBERvnhwKGWTW7C5pbbA5lVkmvgteeNfdd/23gwqv3aiilpYPg== + dependencies: + "@jest/console" "^27.4.6" + "@jest/environment" "^27.4.6" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + emittery "^0.8.1" + exit "^0.1.2" + graceful-fs "^4.2.4" + jest-docblock "^27.4.0" + jest-environment-jsdom "^27.4.6" + jest-environment-node "^27.4.6" + jest-haste-map "^27.4.6" + jest-leak-detector "^27.4.6" + jest-message-util "^27.4.6" + jest-resolve "^27.4.6" + jest-runtime "^27.4.6" + jest-util "^27.4.2" + jest-worker "^27.4.6" + source-map-support "^0.5.6" + throat "^6.0.1" + +jest-runtime@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.4.6.tgz" + integrity sha512-eXYeoR/MbIpVDrjqy5d6cGCFOYBFFDeKaNWqTp0h6E74dK0zLHzASQXJpl5a2/40euBmKnprNLJ0Kh0LCndnWQ== + dependencies: + "@jest/environment" "^27.4.6" + "@jest/fake-timers" "^27.4.6" + "@jest/globals" "^27.4.6" + "@jest/source-map" "^27.4.0" + "@jest/test-result" "^27.4.6" + "@jest/transform" "^27.4.6" + "@jest/types" "^27.4.2" + chalk "^4.0.0" + cjs-module-lexer "^1.0.0" + collect-v8-coverage "^1.0.0" + execa "^5.0.0" + glob "^7.1.3" + graceful-fs "^4.2.4" + jest-haste-map "^27.4.6" + jest-message-util "^27.4.6" + jest-mock "^27.4.6" + jest-regex-util "^27.4.0" + jest-resolve "^27.4.6" + jest-snapshot "^27.4.6" + jest-util "^27.4.2" + slash "^3.0.0" + strip-bom "^4.0.0" + +jest-serializer@^27.4.0: + version "27.4.0" + resolved "https://registry.npmjs.org/jest-serializer/-/jest-serializer-27.4.0.tgz" + integrity sha512-RDhpcn5f1JYTX2pvJAGDcnsNTnsV9bjYPU8xcV+xPwOXnUPOQwf4ZEuiU6G9H1UztH+OapMgu/ckEVwO87PwnQ== + dependencies: + "@types/node" "*" + graceful-fs "^4.2.4" + +jest-snapshot@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-27.4.6.tgz" + integrity sha512-fafUCDLQfzuNP9IRcEqaFAMzEe7u5BF7mude51wyWv7VRex60WznZIC7DfKTgSIlJa8aFzYmXclmN328aqSDmQ== + 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/transform" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/babel__traverse" "^7.0.4" + "@types/prettier" "^2.1.5" + babel-preset-current-node-syntax "^1.0.0" + chalk "^4.0.0" + expect "^27.4.6" + graceful-fs "^4.2.4" + jest-diff "^27.4.6" + jest-get-type "^27.4.0" + jest-haste-map "^27.4.6" + jest-matcher-utils "^27.4.6" + jest-message-util "^27.4.6" + jest-util "^27.4.2" + natural-compare "^1.4.0" + pretty-format "^27.4.6" + semver "^7.3.2" + +jest-util@^27.4.2: + version "27.4.2" + resolved "https://registry.npmjs.org/jest-util/-/jest-util-27.4.2.tgz" + integrity sha512-YuxxpXU6nlMan9qyLuxHaMMOzXAl5aGZWCSzben5DhLHemYQxCc4YK+4L3ZrCutT8GPQ+ui9k5D8rUJoDioMnA== + dependencies: + "@jest/types" "^27.4.2" + "@types/node" "*" + chalk "^4.0.0" + ci-info "^3.2.0" + graceful-fs "^4.2.4" + picomatch "^2.2.3" + +jest-validate@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-validate/-/jest-validate-27.4.6.tgz" + integrity sha512-872mEmCPVlBqbA5dToC57vA3yJaMRfIdpCoD3cyHWJOMx+SJwLNw0I71EkWs41oza/Er9Zno9XuTkRYCPDUJXQ== + dependencies: + "@jest/types" "^27.4.2" + camelcase "^6.2.0" + chalk "^4.0.0" + jest-get-type "^27.4.0" + leven "^3.1.0" + pretty-format "^27.4.6" + +jest-watch-typeahead@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/jest-watch-typeahead/-/jest-watch-typeahead-1.0.0.tgz" + integrity sha512-jxoszalAb394WElmiJTFBMzie/RDCF+W7Q29n5LzOPtcoQoHWfdUtHFkbhgf5NwWe8uMOxvKb/g7ea7CshfkTw== + dependencies: + ansi-escapes "^4.3.1" + chalk "^4.0.0" + jest-regex-util "^27.0.0" + jest-watcher "^27.0.0" + slash "^4.0.0" + string-length "^5.0.1" + strip-ansi "^7.0.1" + +jest-watcher@^27.0.0, jest-watcher@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-watcher/-/jest-watcher-27.4.6.tgz" + integrity sha512-yKQ20OMBiCDigbD0quhQKLkBO+ObGN79MO4nT7YaCuQ5SM+dkBNWE8cZX0FjU6czwMvWw6StWbe+Wv4jJPJ+fw== + dependencies: + "@jest/test-result" "^27.4.6" + "@jest/types" "^27.4.2" + "@types/node" "*" + ansi-escapes "^4.2.1" + chalk "^4.0.0" + jest-util "^27.4.2" + string-length "^4.0.1" + +jest-worker@^26.2.1: + version "26.6.2" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^7.0.0" + +jest-worker@^27.0.2, jest-worker@^27.3.1, jest-worker@^27.4.1, jest-worker@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/jest-worker/-/jest-worker-27.4.6.tgz" + integrity sha512-gHWJF/6Xi5CTG5QCvROr6GcmpIqNYpDJyc8A1h/DyXqH1tD6SnRCM0d3U5msV31D2LB/U+E0M+W4oyvKV44oNw== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +jest@^27.4.3: + version "27.4.7" + resolved "https://registry.npmjs.org/jest/-/jest-27.4.7.tgz" + integrity sha512-8heYvsx7nV/m8m24Vk26Y87g73Ba6ueUd0MWed/NXMhSZIm62U/llVbS0PJe1SHunbyXjJ/BqG1z9bFjGUIvTg== + dependencies: + "@jest/core" "^27.4.7" + import-local "^3.0.2" + jest-cli "^27.4.7" + +join-component@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/join-component/-/join-component-1.1.0.tgz" + integrity sha1-uEF7dQZho5K+4sJTfGiyqdSXfNU= + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^16.6.0: + version "16.7.0" + resolved "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz" + integrity sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw== + dependencies: + abab "^2.0.5" + acorn "^8.2.4" + acorn-globals "^6.0.0" + cssom "^0.4.4" + cssstyle "^2.3.0" + data-urls "^2.0.0" + decimal.js "^10.2.1" + domexception "^2.0.1" + escodegen "^2.0.0" + form-data "^3.0.0" + html-encoding-sniffer "^2.0.1" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-potential-custom-element-name "^1.0.1" + nwsapi "^2.2.0" + parse5 "6.0.1" + saxes "^5.0.1" + symbol-tree "^3.2.4" + tough-cookie "^4.0.0" + w3c-hr-time "^1.0.2" + w3c-xmlserializer "^2.0.0" + webidl-conversions "^6.1.0" + whatwg-encoding "^1.0.5" + whatwg-mimetype "^2.3.0" + whatwg-url "^8.5.0" + ws "^7.4.6" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz" + integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= + +json-buffer@3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz" + integrity sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg= + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-parse-even-better-errors@^2.3.0, json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@0.4.0, json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +json5@^2.1.2, json5@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz" + integrity sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA== + dependencies: + minimist "^1.2.5" + +jsonc-parser@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.1.0.tgz" + integrity sha512-DRf0QjnNeCUds3xTjKlQQ3DpJD51GvDjJfnxUVWg6PZTo2otSm+slzNAxU/35hF8/oJIKoG9slq30JYOsF2azg== + +jsonc-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" + integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonparse@^1.2.0, jsonparse@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz" + integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= + +jsonpointer@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.0.tgz" + integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg== + +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + +jsprim@^1.2.2: + version "1.4.2" + resolved "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz" + integrity sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw== + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.4.0" + verror "1.10.0" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1: + version "3.2.1" + resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.1.tgz" + integrity sha512-uP5vu8xfy2F9A6LGC22KO7e2/vGTS1MhP+18f++ZNlf0Ohaxbc9nIEwHAsejlJKyzfZzU5UIhe5ItYkitcZnZA== + dependencies: + array-includes "^3.1.3" + object.assign "^4.1.2" + +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + +keyv@^3.0.0: + version "3.1.0" + resolved "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz" + integrity sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA== + dependencies: + json-buffer "3.0.0" + +kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +klona@^2.0.4, klona@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + +knex@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/knex/-/knex-2.4.0.tgz" + integrity sha512-i0GWwqYp1Hs2yvc2rlDO6nzzkLhwdyOZKRdsMTB8ZxOs2IXQyL5rBjSbS1krowCh6V65T4X9CJaKtuIfkaPGSA== + dependencies: + colorette "2.0.19" + commander "^9.1.0" + debug "4.3.4" + escalade "^3.1.1" + esm "^3.2.25" + get-package-type "^0.1.0" + getopts "2.3.0" + interpret "^2.2.0" + lodash "^4.17.21" + pg-connection-string "2.5.0" + rechoir "^0.8.0" + resolve-from "^5.0.0" + tarn "^3.0.2" + tildify "2.0.0" + +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + +language-subtag-registry@~0.3.2: + version "0.3.21" + resolved "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz" + integrity sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg== + +language-tags@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz" + integrity sha1-0yHbxNowuovzAk4ED6XBRmH5GTo= + dependencies: + language-subtag-registry "~0.3.2" + +latest-version@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" + +lerna@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz" + integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== + dependencies: + "@lerna/add" "4.0.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/changed" "4.0.0" + "@lerna/clean" "4.0.0" + "@lerna/cli" "4.0.0" + "@lerna/create" "4.0.0" + "@lerna/diff" "4.0.0" + "@lerna/exec" "4.0.0" + "@lerna/import" "4.0.0" + "@lerna/info" "4.0.0" + "@lerna/init" "4.0.0" + "@lerna/link" "4.0.0" + "@lerna/list" "4.0.0" + "@lerna/publish" "4.0.0" + "@lerna/run" "4.0.0" + "@lerna/version" "4.0.0" + import-local "^3.0.2" + npmlog "^4.1.2" + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +libnpmaccess@^4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.3.tgz" + integrity sha512-sPeTSNImksm8O2b6/pf3ikv4N567ERYEpeKRPSmqlNt1dTZbvgpJIzg5vAhXHpw2ISBsELFRelk0jEahj1c6nQ== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + +libnpmpublish@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.2.tgz" + integrity sha512-+AD7A2zbVeGRCFI2aO//oUmapCwy7GHqPXFJh3qpToSRNU+tXKJ2YFUgjt04LPPAf2dlEH95s6EhIHM1J7bmOw== + dependencies: + normalize-package-data "^3.0.2" + npm-package-arg "^8.1.2" + npm-registry-fetch "^11.0.0" + semver "^7.1.3" + ssri "^8.0.1" + +libphonenumber-js@^1.10.48: + version "1.10.48" + resolved "https://registry.yarnpkg.com/libphonenumber-js/-/libphonenumber-js-1.10.48.tgz#3c426b4aa21dfe3210bfbda47d208acffa3631bf" + integrity sha512-Vvcgt4+o8+puIBJZLdMshPYx9nRN3/kTT7HPtOyfYrSQuN9PGBF1KUv0g07fjNzt4E4GuA7FnsLb+WeAMzyRQg== + +lilconfig@^2.0.3, lilconfig@^2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.4.tgz" + integrity sha512-bfTIN7lEsiooCocSISTWXkiWJkRqtL9wYtYy+8EK3Y41qh3mpwPU0ycTOgjdY9ErwXCc8QyrQp82bdL0Xkm9yA== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== + dependencies: + graceful-fs "^4.1.15" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" + +loader-runner@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz" + integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== + +loader-utils@^1.4.0: + version "1.4.2" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^1.0.1" + +loader-utils@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz" + integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +loader-utils@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.0.tgz" + integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== + +local-pkg@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.5.0.tgz#093d25a346bae59a99f80e75f6e9d36d7e8c925c" + integrity sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg== + dependencies: + mlly "^1.4.2" + pkg-types "^1.0.3" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash-es@^4.17.11, lodash-es@^4.17.21: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash._reinterpolate@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz" + integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= + +lodash.clonedeep@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= + +lodash.curry@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz" + integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA= + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= + +lodash.flatten@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= + +lodash.get@^4.4.2: + version "4.4.2" + resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz" + integrity sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo= + +lodash.ismatch@^4.4.0: + version "4.4.0" + resolved "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz" + integrity sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc= + +lodash.isstring@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= + +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash.template@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== + dependencies: + lodash._reinterpolate "^3.0.0" + lodash.templatesettings "^4.0.0" + +lodash.templatesettings@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== + dependencies: + lodash._reinterpolate "^3.0.0" + +lodash.uniq@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz" + integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= + +lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.7.0: + version "4.17.21" + resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +logform@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/logform/-/logform-2.3.2.tgz" + integrity sha512-V6JiPThZzTsbVRspNO6TmHkR99oqYTs8fivMBYQkjZj6rxW92KxtDCPE6IkAk1DNBnYKNkjm4jYBm6JDUcyhOA== + dependencies: + colors "1.4.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^1.1.0" + triple-beam "^1.3.0" + +logform@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/logform/-/logform-2.4.0.tgz" + integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loupe@^2.3.6, loupe@^2.3.7: + version "2.3.7" + resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" + integrity sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA== + dependencies: + get-func-name "^2.0.1" + +lower-case@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz" + integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== + dependencies: + tslib "^2.0.3" + +lowercase-keys@^1.0.0, lowercase-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz" + integrity sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA== + +lowercase-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz" + integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== + +lru-cache@^10.0.1, lru-cache@^10.2.0: + version "10.2.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.1.tgz#e8d901141f22937968e45a6533d52824070151e4" + integrity sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +lru_map@^0.3.3: + version "0.3.3" + resolved "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz" + integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== + +luxon@2.5.2, luxon@^2.3.1: + version "2.5.2" + resolved "https://registry.npmjs.org/luxon/-/luxon-2.5.2.tgz" + integrity sha512-Yg7/RDp4nedqmLgyH0LwgGRvMEKVzKbUdkBYyCosbHgJ+kaOUx0qzSiSatVc3DFygnirTPYnMM2P5dg2uH1WvA== + +luxon@^1.28.0: + version "1.28.0" + resolved "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz" + integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ== + +luxon@^3.0.1: + version "3.0.4" + resolved "https://registry.npmjs.org/luxon/-/luxon-3.0.4.tgz" + integrity sha512-aV48rGUwP/Vydn8HT+5cdr26YYQiUZ42NM6ToMoaGKwYfWbfLeRkEu1wXWMHBZT6+KyLfcbbtVcoQFCbbPjKlw== + +lz-string@^1.4.4: + version "1.4.4" + resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz" + integrity sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY= + +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +magic-string@^0.30.5: + version "0.30.5" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" + integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +make-fetch-happen@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-13.0.0.tgz#705d6f6cbd7faecb8eac2432f551e49475bfedf0" + integrity sha512-7ThobcL8brtGo9CavByQrQi+23aIfgYU++wg4B87AIS8Rb2ZBt/MEaDqzA00Xwv/jUjAjYkLHjVolYuTLKda2A== + dependencies: + "@npmcli/agent" "^2.0.0" + cacache "^18.0.0" + http-cache-semantics "^4.1.1" + is-lambda "^1.0.1" + minipass "^7.0.2" + minipass-fetch "^3.0.0" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + ssri "^10.0.0" + +make-fetch-happen@^8.0.9: + version "8.0.14" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + +make-fetch-happen@^9.0.1: + version "9.1.0" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz" + integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.2.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.2" + promise-retry "^2.0.1" + socks-proxy-agent "^6.0.0" + ssri "^8.0.0" + +makeerror@1.0.12: + version "1.0.12" + resolved "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz" + integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== + dependencies: + tmpl "1.0.5" + +map-obj@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-obj@^4.0.0: + version "4.3.0" + resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" + integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== + +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + +mdn-data@2.0.14: + version "2.0.14" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz" + integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== + +mdn-data@2.0.4: + version "2.0.4" + resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz" + integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memfs@^3.1.2: + version "3.4.1" + resolved "https://registry.npmjs.org/memfs/-/memfs-3.4.1.tgz" + integrity sha512-1c9VPVvW5P7I85c35zAdEr1TD5+F11IToIHIlrVIcflfnzPkJa0ZoYEoEdYDP8KgPFoSZ/opDrUsAoZWym3mtw== + dependencies: + fs-monkey "1.0.3" + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +"memoize-one@>=3.1.1 <6": + version "5.2.1" + resolved "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz" + integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== + +memory-cache@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/memory-cache/-/memory-cache-0.2.0.tgz" + integrity sha512-OcjA+jzjOYzKmKS6IQVALHLVz+rNTMPoJvCztFaZxwG14wtAW7VRZjwTQu06vKCYOxh4jVnik7ya0SXTB0W+xA== + +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micro@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/micro/-/micro-10.0.1.tgz#2601e02b0dacd2eaee77e9de18f12b2e595c5951" + integrity sha512-9uwZSsUrqf6+4FLLpiPj5TRWQv5w5uJrJwsx1LR/TjqvQmKC1XnGQ9OHrFwR3cbZ46YqPqxO/XJCOpWnqMPw2Q== + dependencies: + arg "4.1.0" + content-type "1.0.4" + raw-body "2.4.1" + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mime-db@1.51.0, "mime-db@>= 1.43.0 < 2": + version "1.51.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz" + integrity sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.34" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz" + integrity sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A== + dependencies: + mime-db "1.51.0" + +mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.6.0.tgz#a2a682a95cd4d0cb1d6257e28f83da7e35800367" + integrity sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +mimic-fn@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" + integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== + +mimic-response@^1.0.0, mimic-response@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz" + integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +mini-css-extract-plugin@^2.4.5: + version "2.5.2" + resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.5.2.tgz" + integrity sha512-Lwgq9qLNyBK6yNLgzssXnq4r2+mB9Mz3cJWlM8kseysHIvTicFhDNimFgY94jjqlwhNzLPsq8wv4X+vOHtMdYA== + dependencies: + schema-utils "^4.0.0" + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^5.0.1: + version "5.1.0" + resolved "https://registry.npmjs.org/minimatch/-/minimatch-5.1.0.tgz" + integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + +minimist@^1.1.1, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: + version "1.2.7" + resolved "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz" + integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== + +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-collect@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-2.0.1.tgz#1621bc77e12258a12c60d34e2276ec5c20680863" + integrity sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw== + dependencies: + minipass "^7.0.3" + +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.4.1" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz" + integrity sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + +minipass-fetch@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-3.0.4.tgz#4d4d9b9f34053af6c6e597a64be8e66e42bf45b7" + integrity sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg== + dependencies: + minipass "^7.0.3" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + +minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^2.6.0, minipass@^2.9.0: + version "2.9.0" + resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz" + integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.0, minipass@^3.1.1, minipass@^3.1.3: + version "3.1.6" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.6.tgz" + integrity sha512-rty5kpw9/z8SX9dmxblFA6edItUmwJgMeYDZRrwlIVN27i8gysGbznJwUggw2V/FVqFSDdWy040ZPS811DYAqQ== + dependencies: + yallist "^4.0.0" + +minipass@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" + integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.2, minipass@^7.0.3, minipass@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + +minizlib@^1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz" + integrity sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q== + dependencies: + minipass "^2.9.0" + +minizlib@^2.0.0, minizlib@^2.1.1, minizlib@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== + dependencies: + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" + +mkdirp@^0.5.1, mkdirp@^0.5.5, mkdirp@~0.5.1: + version "0.5.5" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +mkdirp@^0.5.4: + version "0.5.6" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz" + integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== + dependencies: + minimist "^1.2.6" + +mkdirp@^1.0.3, mkdirp@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + +mlly@^1.2.0, mlly@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.2.tgz#7cf406aa319ff6563d25da6b36610a93f2a8007e" + integrity sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg== + dependencies: + acorn "^8.10.0" + pathe "^1.1.1" + pkg-types "^1.0.3" + ufo "^1.3.0" + +modify-values@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz" + integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== + +morgan@^1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz" + integrity sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ== + dependencies: + basic-auth "~2.0.1" + debug "2.6.9" + depd "~2.0.0" + on-finished "~2.3.0" + on-headers "~1.0.2" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +msgpackr-extract-darwin-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-1.1.0.tgz" + integrity sha512-s1kHoT12tS2cCQOv+Wl3I+/cYNJXBPtwQqGA+dPYoXmchhXiE0Nso+BIfvQ5PxbmAyjj54Q5o7PnLTqVquNfZA== + +msgpackr-extract-darwin-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-1.1.0.tgz#568cbdf5e819ac120659c02b0dbaabf483523ee3" + integrity sha512-yx/H/i12IKg4eWGu/eKdKzJD4jaYvvujQSaVmeOMCesbSQnWo5X6YR9TFjoiNoU9Aexk1KufzL9gW+1DozG1yw== + +msgpackr-extract-linux-arm64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-1.1.0.tgz#c0a30e6687cea4f79115f5762c5fdff90e4a20d4" + integrity sha512-AxFle3fHNwz2V4CYDIGFxI6o/ZuI0lBKg0uHI8EcCMUmDE5mVAUWYge5WXmORVvb8sVWyVgFlmi3MTu4Ve6tNQ== + +msgpackr-extract-linux-arm@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-1.1.0.tgz#38e8db873b6b3986558bde4d7bb15eacc8743a9e" + integrity sha512-0VvSCqi12xpavxl14gMrauwIzHqHbmSChUijy/uo3mpjB1Pk4vlisKpZsaOZvNJyNKj0ACi5jYtbWnnOd7hYGw== + +msgpackr-extract-linux-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-1.1.0.tgz#8c44ca5211d9fa6af77be64a8e687c0be0491ce7" + integrity sha512-O+XoyNFWpdB8oQL6O/YyzffPpmG5rTNrr1nKLW70HD2ENJUhcITzbV7eZimHPzkn8LAGls1tBaMTHQezTBpFOw== + +msgpackr-extract-win32-x64@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-1.1.0.tgz#7bf9bd258e334668842c7532e5e40a60ca3325d7" + integrity sha512-6AJdM5rNsL4yrskRfhujVSPEd6IBpgvsnIT/TPowKNLQ62iIdryizPY2PJNFiW3AJcY249AHEiDBXS1cTDPxzA== + +msgpackr-extract@^1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-1.1.4.tgz" + integrity sha512-WQbHvsThprXh+EqZYy+SQFEs7z6bNM7a0vgirwUfwUcphWGT2mdPcpyLCNiRsN6w5q5VKJUMblHY+tNEyceb9Q== + dependencies: + node-gyp-build-optional-packages "^4.3.2" + optionalDependencies: + msgpackr-extract-darwin-arm64 "1.1.0" + msgpackr-extract-darwin-x64 "1.1.0" + msgpackr-extract-linux-arm "1.1.0" + msgpackr-extract-linux-arm64 "1.1.0" + msgpackr-extract-linux-x64 "1.1.0" + msgpackr-extract-win32-x64 "1.1.0" + +msgpackr-extract@^2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-2.1.2.tgz" + integrity sha512-cmrmERQFb19NX2JABOGtrKdHMyI6RUyceaPBQ2iRz9GnDkjBWFjNJC0jyyoOfZl2U/LZE3tQCCQc4dlRyA8mcA== + dependencies: + node-gyp-build-optional-packages "5.0.3" + optionalDependencies: + "@msgpackr-extract/msgpackr-extract-darwin-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-darwin-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-arm64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-linux-x64" "2.1.2" + "@msgpackr-extract/msgpackr-extract-win32-x64" "2.1.2" + +msgpackr@^1.5.2: + version "1.5.6" + resolved "https://registry.npmjs.org/msgpackr/-/msgpackr-1.5.6.tgz" + integrity sha512-Y1Ia1AYKcz30JOAUyyC0jCicI7SeP8NK+SVCGZIeLg2oQs28wSwW2GbHXktk4ZZmrq9/v2jU0JAbvbp2d1ewpg== + optionalDependencies: + msgpackr-extract "^1.1.4" + +msgpackr@^1.6.2: + version "1.7.2" + resolved "https://registry.npmjs.org/msgpackr/-/msgpackr-1.7.2.tgz" + integrity sha512-mWScyHTtG6TjivXX9vfIy2nBtRupaiAj0HQ2mtmpmYujAmqZmaaEVPaSZ1NKLMvicaMLFzEaMk0ManxMRg8rMQ== + optionalDependencies: + msgpackr-extract "^2.1.2" + +mui-color-input@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mui-color-input/-/mui-color-input-2.0.0.tgz" + integrity sha512-Xw6OGsZVbtlZEAUVgJ08Lyv4u0YDQH+aTMJhhWm2fRin+1T+0IrVFyBtbSjJjrH4aBkkQPMCm75//7qO9zncLw== + dependencies: + "@ctrl/tinycolor" "^3.6.0" + +multer@1.4.5-lts.1: + version "1.4.5-lts.1" + resolved "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz" + integrity sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ== + dependencies: + append-field "^1.0.0" + busboy "^1.0.0" + concat-stream "^1.5.2" + mkdirp "^0.5.4" + object-assign "^4.1.1" + type-is "^1.6.4" + xtend "^4.0.0" + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^6.0.1: + version "6.2.3" + resolved "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz" + integrity sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g== + dependencies: + dns-packet "^1.3.1" + thunky "^1.0.2" + +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== + dependencies: + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" + minimatch "^3.0.4" + +mute-stream@0.0.8, mute-stream@~0.0.4: + version "0.0.8" + resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nanoclone@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/nanoclone/-/nanoclone-0.2.1.tgz" + integrity sha512-wynEP02LmIbLpcYw8uBKpcfF6dmg2vcpKqxeH5UcoKEYdExslsdUA4ugFauuaeYdTB76ez6gJW8XAZ6CgkXYxA== + +nanoid@^3.1.30: + version "3.2.0" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.2.0.tgz" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + +nanoid@^3.3.4: + version "3.3.4" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz" + integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.3, negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +negotiator@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +neo-async@^2.6.0, neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +no-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz" + integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== + dependencies: + lower-case "^2.0.2" + tslib "^2.0.3" + +node-addon-api@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762" + integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA== + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +node-fetch@^2.6.7: + version "2.7.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" + integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== + dependencies: + whatwg-url "^5.0.0" + +node-forge@^1.2.0: + version "1.3.0" + resolved "https://registry.npmjs.org/node-forge/-/node-forge-1.3.0.tgz" + integrity sha512-08ARB91bUi6zNKzVmaj3QO7cr397uiDT2nJ63cHjyNtCTWIgvS47j3eT0WfzUwS9+6Z5YshRaoasFkXCKrIYbA== + +node-gyp-build-optional-packages@5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.3.tgz" + integrity sha512-k75jcVzk5wnnc/FMxsf4udAoTEUv2jY3ycfdSd3yWu6Cnd1oee6/CfZJApyscA4FJOmdoixWwiwOyf16RzD5JA== + +node-gyp-build-optional-packages@^4.3.2: + version "4.3.2" + resolved "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-4.3.2.tgz" + integrity sha512-P5Ep3ISdmwcCkZIaBaQamQtWAG0facC89phWZgi5Z3hBU//J6S48OIvyZWSPPf6yQMklLZiqoosWAZUj7N+esA== + +node-gyp@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-10.1.0.tgz#75e6f223f2acb4026866c26a2ead6aab75a8ca7e" + integrity sha512-B4J5M1cABxPc5PwfjhbV5hoy2DP9p8lFXASnEN6hugXOa61416tnTZ29x9sSwAd0o99XNIcpvDDy1swAExsVKA== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^10.3.10" + graceful-fs "^4.2.6" + make-fetch-happen "^13.0.0" + nopt "^7.0.0" + proc-log "^3.0.0" + semver "^7.3.5" + tar "^6.1.2" + which "^4.0.0" + +node-gyp@^5.0.2: + version "5.1.1" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz" + integrity sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.2" + mkdirp "^0.5.1" + nopt "^4.0.1" + npmlog "^4.1.2" + request "^2.88.0" + rimraf "^2.6.3" + semver "^5.7.1" + tar "^4.4.12" + which "^1.3.1" + +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + +node-html-markdown@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/node-html-markdown/-/node-html-markdown-1.3.0.tgz" + integrity sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g== + dependencies: + node-html-parser "^6.1.1" + +node-html-parser@^6.1.1: + version "6.1.5" + resolved "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.5.tgz" + integrity sha512-fAaM511feX++/Chnhe475a0NHD8M7AxDInsqQpz6x63GRF7xYNdS8Vo5dKsIVPgsOvG7eioRRTZQnWBrhDHBSg== + dependencies: + css-select "^5.1.0" + he "1.2.0" + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-releases@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz" + integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== + +nodemailer@6.7.0: + version "6.7.0" + resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.0.tgz" + integrity sha512-AtiTVUFHLiiDnMQ43zi0YgkzHOEWUkhDgPlBXrsDzJiJvB29Alo4OKxHQ0ugF3gRqRQIneCLtZU3yiUo7pItZw== + +nodemon@^2.0.13: + version "2.0.15" + resolved "https://registry.npmjs.org/nodemon/-/nodemon-2.0.15.tgz" + integrity sha512-gdHMNx47Gw7b3kWxJV64NI+Q5nfl0y5DgDbiVtShiwa7Z0IZ07Ll4RLFo6AjrhzMtoEZn5PDE3/c2AbVsiCkpA== + 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@^4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + +nopt@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-7.2.0.tgz#067378c68116f602f552876194fd11f1292503d7" + integrity sha512-CVDtwCdhYIvnAzFoJ6NJ6dX3oga9/HyciQDnG1vQDjSLMeKLJ4A93ZqYKDrgYSr1FBY5/hMYC+2VCi24pgpkGA== + dependencies: + abbrev "^2.0.0" + +nopt@~1.0.10: + version "1.0.10" + resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz" + integrity sha1-bd0hvSoxQXuScn3Vhfim83YI6+4= + dependencies: + abbrev "1" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-package-data@^3.0.0, normalize-package-data@^3.0.2: + version "3.0.3" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz" + integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== + dependencies: + hosted-git-info "^4.0.1" + is-core-module "^2.5.0" + semver "^7.3.4" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" + integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= + +normalize-url@^4.1.0: + version "4.5.1" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz" + integrity sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA== + +normalize-url@^6.0.1, normalize-url@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz" + integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== + +notistack@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/notistack/-/notistack-3.0.1.tgz#daf59888ab7e2c30a1fa8f71f9cba2978773236e" + integrity sha512-ntVZXXgSQH5WYfyU+3HfcXuKaapzAJ8fBLQ/G618rn3yvSzEbnOB8ZSOwhX+dAORy/lw+GC2N061JA0+gYWTVA== + dependencies: + clsx "^1.1.0" + goober "^2.0.33" + +npm-bundled@^1.1.1: + version "1.1.2" + resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz" + integrity sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ== + dependencies: + npm-normalize-package-bin "^1.0.1" + +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-lifecycle@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz" + integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== + dependencies: + byline "^5.0.0" + graceful-fs "^4.1.15" + node-gyp "^5.0.2" + resolve-from "^4.0.0" + slide "^1.1.6" + uid-number "0.0.6" + umask "^1.1.0" + which "^1.3.1" + +npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz" + integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== + +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0, npm-package-arg@^8.1.2, npm-package-arg@^8.1.5: + version "8.1.5" + resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.5.tgz" + integrity sha512-LhgZrg0n0VgvzVdSm1oiZworPbTxYHUJCgtsJW8mGvlDpxTM1vSJc3m5QZeUkhAHIzbz3VCHd/R4osi1L1Tg/Q== + dependencies: + hosted-git-info "^4.0.1" + semver "^7.3.4" + validate-npm-package-name "^3.0.0" + +npm-packlist@^2.1.4: + version "2.2.2" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.2.2.tgz" + integrity sha512-Jt01acDvJRhJGthnUJVF/w6gumWOZxO7IkpY/lsX9//zqQgnF7OJaxgQXcerd4uQOLu7W5bkb4mChL9mdfm+Zg== + dependencies: + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0, npm-pick-manifest@^6.1.1: + version "6.1.1" + resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.1.tgz" + integrity sha512-dBsdBtORT84S8V8UTad1WlUyKIY9iMsAmqxHbLdeEeBNMLQDlDWWra3wYUx9EBEIiG/YwAy0XyNHDd2goAsfuA== + dependencies: + npm-install-checks "^4.0.0" + npm-normalize-package-bin "^1.0.1" + npm-package-arg "^8.1.2" + semver "^7.3.4" + +npm-registry-fetch@^11.0.0: + version "11.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-11.0.0.tgz" + integrity sha512-jmlgSxoDNuhAtxUIG6pVwwtz840i994dL14FoNVZisrmZW5kWd63IUTNv1m/hyRSGSqWjCUp/YZlS1BJyNp9XA== + dependencies: + make-fetch-happen "^9.0.1" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-registry-fetch@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz" + integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== + dependencies: + "@npmcli/ci-detect" "^1.0.0" + lru-cache "^6.0.0" + make-fetch-happen "^8.0.9" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +npm-run-path@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.2.0.tgz#224cdd22c755560253dd71b83a1ef2f758b2e955" + integrity sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg== + dependencies: + path-key "^4.0.0" + +npmlog@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz" + integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +npmlog@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz" + integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== + dependencies: + are-we-there-yet "^2.0.0" + console-control-strings "^1.1.0" + gauge "^3.0.0" + set-blocking "^2.0.0" + +nth-check@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + +nth-check@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz" + integrity sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w== + dependencies: + boolbase "^1.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +nwsapi@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +oauth-1.0a@^2.2.6: + version "2.2.6" + resolved "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz" + integrity sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4, object-assign@^4.1.0, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-hash@^2.0.3, object-hash@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.11.0: + version "1.12.0" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + +object-inspect@^1.9.0: + version "1.12.2" + resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz" + integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== + +object-is@^1.0.1: + version "1.1.5" + resolved "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz" + integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.0, object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.entries@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.entries/-/object.entries-1.1.5.tgz" + integrity sha512-TyxmjUoZggd4OrrU1W66FMDG6CuqJxsFvymeyXI51+vQLN67zYfZseptRge703kKQdo4uccgAKebXFcRCzk4+g== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.fromentries@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.5.tgz" + integrity sha512-CAyG5mWQRRiBU57Re4FKoTBjXfDoNwdFVH2Y1tS9PqCsfUTymAohOkEMSG3aRNKmv4lV3O7p1et7c187q6bynw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.getownpropertydescriptors@^2.0.3, object.getownpropertydescriptors@^2.1.0: + version "2.1.3" + resolved "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.3.tgz" + integrity sha512-VdDoCwvJI4QdC6ndjpqFmoL3/+HxffFBbcJzKi5hwLLqqx3mdbedRpfZDdK0SrOSauj8X4GzBvnDZl4vTN7dOw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.hasown@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.0.tgz" + integrity sha512-MhjYRfj3GBlhSkDHo6QmvgjRLXQ2zndabdf3nX0yTyZK9rPfxb6uRpAac8HXNLy1GpqWtZ81Qh4v3uOls2sRAg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.19.1" + +object.values@^1.1.0, object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +objection@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/objection/-/objection-3.0.1.tgz" + integrity sha512-rqNnyQE+C55UHjdpTOJEKQHJGZ/BGtBBtgxdUpKG4DQXRUmqxfmgS/MhPWxB9Pw0mLSVLEltr6soD4c0Sddy0Q== + dependencies: + ajv "^8.6.2" + db-errors "^0.2.3" + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.npmjs.org/once/-/once-1.4.0.tgz" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + +onetime@^5.1.0, onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +onetime@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" + integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== + dependencies: + mimic-fn "^4.0.0" + +open@^8.0.9, open@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/open/-/open-8.4.0.tgz" + integrity sha512-XgFPPM+B28FtCCgSb9I+s9szOC1vZRSwgWsRUA5ylIxRTgKozqjOCrVOqGsYABPYK5qnfqClxZTFBa8PKt2v6Q== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +optimism@^0.16.1: + version "0.16.1" + resolved "https://registry.npmjs.org/optimism/-/optimism-0.16.1.tgz" + integrity sha512-64i+Uw3otrndfq5kaoGNoY7pvOhSsjFEN4bdEFh80MWVk/dbgJfMv7VFDeCT8LxNAlEVhQmdVEbfE7X2nWNIIg== + dependencies: + "@wry/context" "^0.6.0" + "@wry/trie" "^0.3.0" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= + +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz" + integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +p-cancelable@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz" + integrity sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw== + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@3.1.0, p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-limit@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-5.0.0.tgz#6946d5b7140b649b7a33a027d89b4c625b3a5985" + integrity sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ== + dependencies: + yocto-queue "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== + +p-map@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz" + integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== + +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== + +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== + +p-retry@^4.5.0: + version "4.6.1" + resolved "https://registry.npmjs.org/p-retry/-/p-retry-4.6.1.tgz" + integrity sha512-e2xXGNhZOZ0lfgR9kL34iGlU8N/KO0xZnQxVEwdeOvpqNDQfdnxIYizvWtK8RglUa3bGqI8g0R/BdfzLMxRkiA== + dependencies: + "@types/retry" "^0.12.0" + retry "^0.13.1" + +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== + dependencies: + p-reduce "^2.0.0" + +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== + dependencies: + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" + +packet-reader@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz" + integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== + +pacote@^11.2.6: + version "11.3.5" + resolved "https://registry.npmjs.org/pacote/-/pacote-11.3.5.tgz" + integrity sha512-fT375Yczn4zi+6Hkk2TBe1x1sP8FgFsEIZ2/iWaXY2r/NkhDJfxbcn5paz1+RTFCyNf+dPnaoBDJoAxXSU8Bkg== + dependencies: + "@npmcli/git" "^2.1.0" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^11.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + +param-case@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz" + integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== + dependencies: + dot-case "^3.0.4" + tslib "^2.0.3" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse-json@^5.0.0: + version "5.2.0" + resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" + integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + +parse-path@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/parse-path/-/parse-path-4.0.4.tgz" + integrity sha512-Z2lWUis7jlmXC1jeOG9giRO2+FsuyNipeQ43HAjqAZjwSe3SEf+q/84FGPHoso3kyntbxa4c4i77t3m6fGf8cw== + dependencies: + is-ssh "^1.3.0" + protocols "^1.4.0" + qs "^6.9.4" + query-string "^6.13.8" + +parse-url@^6.0.0: + version "6.0.2" + resolved "https://registry.npmjs.org/parse-url/-/parse-url-6.0.2.tgz" + integrity sha512-uCSjOvD3T+6B/sPWhR+QowAZcU/o4bjPrVBQBGFxcDF6J6FraCGIaDBsdoQawiaaAVdHvtqBe3w3vKlfBKySOQ== + dependencies: + is-ssh "^1.3.0" + normalize-url "^6.1.0" + parse-path "^4.0.4" + protocols "^1.4.0" + +parse5@6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz" + integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascal-case@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz" + integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== + dependencies: + no-case "^3.0.4" + tslib "^2.0.3" + +passport-strategy@1.x.x, passport-strategy@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz" + integrity sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA== + +passport@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/passport/-/passport-0.6.0.tgz" + integrity sha512-0fe+p3ZnrWRW74fe8+SvCyf4a3Pb2/h7gFkQ8yTJpAO50gDzlfjZUZTO1k5Eg9kUct22OxHLqDZoKUWRHOh9ug== + dependencies: + passport-strategy "1.x.x" + pause "0.0.1" + utils-merge "^1.0.1" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-key@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" + integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== + +path-parse@^1.0.6, path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pathe@^1.1.0, pathe@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.2.tgz#6c4cb47a945692e48a1ddd6e4094d170516437ec" + integrity sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ== + +pathval@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" + integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== + +pause@0.0.1: + version "0.0.1" + resolved "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz" + integrity sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg== + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pg-connection-string@2.5.0, pg-connection-string@^2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.5.0.tgz" + integrity sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ== + +pg-int8@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz" + integrity sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw== + +pg-pool@^3.4.1: + version "3.4.1" + resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz" + integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ== + +pg-protocol@^1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz" + integrity sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ== + +pg-types@^2.1.0: + version "2.2.0" + resolved "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz" + integrity sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA== + dependencies: + pg-int8 "1.0.1" + postgres-array "~2.0.0" + postgres-bytea "~1.0.0" + postgres-date "~1.0.4" + postgres-interval "^1.1.0" + +pg@^8.7.1: + version "8.7.1" + resolved "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz" + integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.5.0" + pg-pool "^3.4.1" + pg-protocol "^1.5.0" + pg-types "^2.1.0" + pgpass "1.x" + +pgpass@1.x: + version "1.0.5" + resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz" + integrity sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug== + dependencies: + split2 "^4.1.0" + +php-serialize@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/php-serialize/-/php-serialize-4.0.2.tgz" + integrity sha512-73K9MqCnRn07sXxOht6kVLg+fg1lf/VYpecKy4n9ABcw1PJIAWfaxuQKML27EjolGHWxlXTy3rfh59AGrcUvIA== + +picocolors@^0.2.1: + version "0.2.1" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz" + integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pify@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/pify/-/pify-5.0.0.tgz" + integrity sha512-eW/gHNMlxdSP6dmG6uJip6FXN0EQBwm2clYYd8Wul42Cwu/DK8HEftzsapcNdYe2MfLiIwZqsDk2RDEsTE79hA== + +pirates@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/pirates/-/pirates-4.0.4.tgz" + integrity sha512-ZIrVPH+A52Dw84R0L3/VS9Op04PuQ2SEoJL6bkshmiTic/HldyW9Tf7oH5mhJZBK7NmDx27vSMrYEXPXclpDKw== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +pkg-types@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.3.tgz#988b42ab19254c01614d13f4f65a2cfc7880f868" + integrity sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A== + dependencies: + jsonc-parser "^3.2.0" + mlly "^1.2.0" + pathe "^1.1.0" + +pkg-up@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz" + integrity sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA== + dependencies: + find-up "^3.0.0" + +playwright-core@1.36.2: + version "1.36.2" + resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.2.tgz" + integrity sha512-sQYZt31dwkqxOrP7xy2ggDfEzUxM1lodjhsQ3NMMv5uGTRDsLxU0e4xf4wwMkF2gplIxf17QMBCodSFgm6bFVQ== + +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + +portfinder@^1.0.28: + version "1.0.28" + resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz" + integrity sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.5" + +postcss-attribute-case-insensitive@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-5.0.0.tgz" + integrity sha512-b4g9eagFGq9T5SWX4+USfVyjIb3liPnjhHHRMP7FMB2kFVpYyfEscV0wP3eaXhKlcHKUut8lt5BGoeylWA/dBQ== + dependencies: + postcss-selector-parser "^6.0.2" + +postcss-browser-comments@^4: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-browser-comments/-/postcss-browser-comments-4.0.0.tgz" + integrity sha512-X9X9/WN3KIvY9+hNERUqX9gncsgBA25XaeR+jshHz2j8+sYyHktHw1JdKuMjeLpGktXidqDhA7b/qm1mrBDmgg== + +postcss-calc@^8.2.0: + version "8.2.2" + resolved "https://registry.npmjs.org/postcss-calc/-/postcss-calc-8.2.2.tgz" + integrity sha512-B5R0UeB4zLJvxNt1FVCaDZULdzsKLPc6FhjFJ+xwFiq7VG4i9cuaJLxVjNtExNK8ocm3n2o4unXXLiVX1SCqxA== + dependencies: + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.0.2" + +postcss-color-functional-notation@^4.2.1: + version "4.2.1" + resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-4.2.1.tgz" + integrity sha512-62OBIXCjRXpQZcFOYIXwXBlpAVWrYk8ek1rcjvMING4Q2cf0ipyN9qT+BhHA6HmftGSEnFQu2qgKO3gMscl3Rw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-hex-alpha@^8.0.2: + version "8.0.2" + resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-8.0.2.tgz" + integrity sha512-gyx8RgqSmGVK156NAdKcsfkY3KPGHhKqvHTL3hhveFrBBToguKFzhyiuk3cljH6L4fJ0Kv+JENuPXs1Wij27Zw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-color-rebeccapurple@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-7.0.2.tgz" + integrity sha512-SFc3MaocHaQ6k3oZaFwH8io6MdypkUtEy/eXzXEB1vEQlO3S3oDc/FSZA8AsS04Z25RirQhlDlHLh3dn7XewWw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-colormin@^5.2.3: + version "5.2.3" + resolved "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-5.2.3.tgz" + integrity sha512-dra4xoAjub2wha6RUXAgadHEn2lGxbj8drhFcIGLOMn914Eu7DkPUurugDXgstwttCYkJtZ/+PkWRWdp3UHRIA== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + colord "^2.9.1" + postcss-value-parser "^4.2.0" + +postcss-convert-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-5.0.2.tgz" + integrity sha512-KQ04E2yadmfa1LqXm7UIDwW1ftxU/QWZmz6NKnHnUvJ3LEYbbcX6i329f/ig+WnEByHegulocXrECaZGLpL8Zg== + dependencies: + postcss-value-parser "^4.1.0" + +postcss-custom-media@^8.0.0: + version "8.0.0" + resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-8.0.0.tgz" + integrity sha512-FvO2GzMUaTN0t1fBULDeIvxr5IvbDXcIatt6pnJghc736nqNgsGao5NT+5+WVLAQiTt6Cb3YUms0jiPaXhL//g== + +postcss-custom-properties@^12.1.2: + version "12.1.3" + resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.3.tgz" + integrity sha512-rtu3otIeY532PnEuuBrIIe+N+pcdbX/7JMZfrcL09wc78YayrHw5E8UkDfvnlOhEUrI4ptCuzXQfj+Or6spbGA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-custom-selectors@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-6.0.0.tgz" + integrity sha512-/1iyBhz/W8jUepjGyu7V1OPcGbc636snN1yXEQCinb6Bwt7KxsiU7/bLQlp8GwAXzCh7cobBU5odNn/2zQWR8Q== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-dir-pseudo-class@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-6.0.3.tgz" + integrity sha512-qiPm+CNAlgXiMf0J5IbBBEXA9l/Q5HGsNGkL3znIwT2ZFRLGY9U2fTUpa4lqCUXQOxaLimpacHeQC80BD2qbDw== + dependencies: + postcss-selector-parser "^6.0.8" + +postcss-discard-comments@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-5.0.1.tgz" + integrity sha512-lgZBPTDvWrbAYY1v5GYEv8fEO/WhKOu/hmZqmCYfrpD6eyDWWzAOsl2rF29lpvziKO02Gc5GJQtlpkTmakwOWg== + +postcss-discard-duplicates@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-5.0.1.tgz" + integrity sha512-svx747PWHKOGpAXXQkCc4k/DsWo+6bc5LsVrAsw+OU+Ibi7klFZCyX54gjYzX4TH+f2uzXjRviLARxkMurA2bA== + +postcss-discard-empty@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-5.0.1.tgz" + integrity sha512-vfU8CxAQ6YpMxV2SvMcMIyF2LX1ZzWpy0lqHDsOdaKKLQVQGVP1pzhrI9JlsO65s66uQTfkQBKBD/A5gp9STFw== + +postcss-discard-overridden@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-5.0.2.tgz" + integrity sha512-+56BLP6NSSUuWUXjRgAQuho1p5xs/hU5Sw7+xt9S3JSg+7R6+WMGnJW7Hre/6tTuZ2xiXMB42ObkiZJ2hy/Pew== + +postcss-double-position-gradients@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-3.0.4.tgz" + integrity sha512-qz+s5vhKJlsHw8HjSs+HVk2QGFdRyC68KGRQGX3i+GcnUjhWhXQEmCXW6siOJkZ1giu0ddPwSO6I6JdVVVPoog== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-env-function@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/postcss-env-function/-/postcss-env-function-4.0.4.tgz" + integrity sha512-0ltahRTPtXSIlEZFv7zIvdEib7HN0ZbUQxrxIKn8KbiRyhALo854I/CggU5lyZe6ZBvSTJ6Al2vkZecI2OhneQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-flexbugs-fixes@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-5.0.2.tgz" + integrity sha512-18f9voByak7bTktR2QgDveglpn9DTbBWPUzSOe9g0N4WR/2eSt6Vrcbf0hmspvMI6YWGywz6B9f7jzpFNJJgnQ== + +postcss-focus-visible@^6.0.3: + version "6.0.3" + resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-6.0.3.tgz" + integrity sha512-ozOsg+L1U8S+rxSHnJJiET6dNLyADcPHhEarhhtCI9DBLGOPG/2i4ddVoFch9LzrBgb8uDaaRI4nuid2OM82ZA== + dependencies: + postcss-selector-parser "^6.0.8" + +postcss-focus-within@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-5.0.3.tgz" + integrity sha512-fk9y2uFS6/Kpp7/A9Hz9Z4rlFQ8+tzgBcQCXAFSrXFGAbKx+4ZZOmmfHuYjCOMegPWoz0pnC6fNzi8j7Xyqp5Q== + dependencies: + postcss-selector-parser "^6.0.8" + +postcss-font-variant@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz" + integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA== + +postcss-gap-properties@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-3.0.2.tgz" + integrity sha512-EaMy/pbxtQnKDsnbEjdqlkCkROTQZzolcLKgIE+3b7EuJfJydH55cZeHfm+MtIezXRqhR80VKgaztO/vHq94Fw== + +postcss-image-set-function@^4.0.4: + version "4.0.4" + resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-4.0.4.tgz" + integrity sha512-BlEo9gSTj66lXjRNByvkMK9dEdEGFXRfGjKRi9fo8s0/P3oEk74cAoonl/utiM50E2OPVb/XSu+lWvdW4KtE/Q== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-initial@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/postcss-initial/-/postcss-initial-4.0.1.tgz" + integrity sha512-0ueD7rPqX8Pn1xJIjay0AZeIuDoF+V+VvMt/uOnn+4ezUKhZM/NokDeP6DwMNyIoYByuN/94IQnt5FEkaN59xQ== + +postcss-js@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.0.tgz" + integrity sha512-77QESFBwgX4irogGVPgQ5s07vLvFqWr228qZY+w6lW599cRlK/HmnlivnnVUxkjHnCu4J16PDMHcH+e+2HbvTQ== + dependencies: + camelcase-css "^2.0.1" + +postcss-lab-function@^4.0.3: + version "4.0.3" + resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-4.0.3.tgz" + integrity sha512-MH4tymWmefdZQ7uVG/4icfLjAQmH6o2NRYyVh2mKoB4RXJp9PjsyhZwhH4ouaCQHvg+qJVj3RzeAR1EQpIlXZA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-load-config@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.1.tgz" + integrity sha512-c/9XYboIbSEUZpiD1UQD0IKiUe8n9WHYV7YFe7X7J+ZwCsEKkUJSFWjS9hBU1RR9THR7jMXst8sxiqP0jjo2mg== + dependencies: + lilconfig "^2.0.4" + yaml "^1.10.2" + +postcss-loader@^6.2.1: + version "6.2.1" + resolved "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz" + integrity sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q== + dependencies: + cosmiconfig "^7.0.0" + klona "^2.0.5" + semver "^7.3.5" + +postcss-logical@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-5.0.3.tgz" + integrity sha512-P5NcHWYrif0vK8rgOy/T87vg0WRIj3HSknrvp1wzDbiBeoDPVmiVRmkown2eSQdpPveat/MC1ess5uhzZFVnqQ== + +postcss-media-minmax@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-media-minmax/-/postcss-media-minmax-5.0.0.tgz" + integrity sha512-yDUvFf9QdFZTuCUg0g0uNSHVlJ5X1lSzDZjPSFaiCWvjgsvu8vEVxtahPrLMinIDEEGnx6cBe6iqdx5YWz08wQ== + +postcss-merge-longhand@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-5.0.4.tgz" + integrity sha512-2lZrOVD+d81aoYkZDpWu6+3dTAAGkCKbV5DoRhnIR7KOULVrI/R7bcMjhrH9KTRy6iiHKqmtG+n/MMj1WmqHFw== + dependencies: + postcss-value-parser "^4.1.0" + stylehacks "^5.0.1" + +postcss-merge-rules@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-5.0.4.tgz" + integrity sha512-yOj7bW3NxlQxaERBB0lEY1sH5y+RzevjbdH4DBJurjKERNpknRByFNdNe+V72i5pIZL12woM9uGdS5xbSB+kDQ== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + cssnano-utils "^3.0.0" + postcss-selector-parser "^6.0.5" + +postcss-minify-font-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-5.0.2.tgz" + integrity sha512-R6MJZryq28Cw0AmnyhXrM7naqJZZLoa1paBltIzh2wM7yb4D45TLur+eubTQ4jCmZU9SGeZdWsc5KcSoqTMeTg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-minify-gradients@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-5.0.4.tgz" + integrity sha512-RVwZA7NC4R4J76u8X0Q0j+J7ItKUWAeBUJ8oEEZWmtv3Xoh19uNJaJwzNpsydQjk6PkuhRrK+YwwMf+c+68EYg== + dependencies: + colord "^2.9.1" + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" + +postcss-minify-params@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-5.0.3.tgz" + integrity sha512-NY92FUikE+wralaiVexFd5gwb7oJTIDhgTNeIw89i1Ymsgt4RWiPXfz3bg7hDy4NL6gepcThJwOYNtZO/eNi7Q== + dependencies: + alphanum-sort "^1.0.2" + browserslist "^4.16.6" + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" + +postcss-minify-selectors@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-5.1.1.tgz" + integrity sha512-TOzqOPXt91O2luJInaVPiivh90a2SIK5Nf1Ea7yEIM/5w+XA5BGrZGUSW8aEx9pJ/oNj7ZJBhjvigSiBV+bC1Q== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" + +postcss-modules-extract-imports@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz" + integrity sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw== + +postcss-modules-local-by-default@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz" + integrity sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ== + dependencies: + icss-utils "^5.0.0" + postcss-selector-parser "^6.0.2" + postcss-value-parser "^4.1.0" + +postcss-modules-scope@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz" + integrity sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg== + dependencies: + postcss-selector-parser "^6.0.4" + +postcss-modules-values@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz" + integrity sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ== + dependencies: + icss-utils "^5.0.0" + +postcss-nested@5.0.6: + version "5.0.6" + resolved "https://registry.npmjs.org/postcss-nested/-/postcss-nested-5.0.6.tgz" + integrity sha512-rKqm2Fk0KbA8Vt3AdGN0FB9OBOMDVajMG6ZCf/GoHgdxUJ4sBFp0A/uMIRm+MJUdo33YXEtjqIz8u7DAp8B7DA== + dependencies: + postcss-selector-parser "^6.0.6" + +postcss-nesting@^10.1.2: + version "10.1.2" + resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-10.1.2.tgz" + integrity sha512-dJGmgmsvpzKoVMtDMQQG/T6FSqs6kDtUDirIfl4KnjMCiY9/ETX8jdKyCd20swSRAbUYkaBKV20pxkzxoOXLqQ== + dependencies: + postcss-selector-parser "^6.0.8" + +postcss-normalize-charset@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-5.0.1.tgz" + integrity sha512-6J40l6LNYnBdPSk+BHZ8SF+HAkS4q2twe5jnocgd+xWpz/mx/5Sa32m3W1AA8uE8XaXN+eg8trIlfu8V9x61eg== + +postcss-normalize-display-values@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-5.0.2.tgz" + integrity sha512-RxXoJPUR0shSjkMMzgEZDjGPrgXUVYyWA/YwQRicb48H15OClPuaDR7tYokLAlGZ2tCSENEN5WxjgxSD5m4cUw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-positions@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-5.0.2.tgz" + integrity sha512-tqghWFVDp2btqFg1gYob1etPNxXLNh3uVeWgZE2AQGh6b2F8AK2Gj36v5Vhyh+APwIzNjmt6jwZ9pTBP+/OM8g== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-repeat-style@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.0.2.tgz" + integrity sha512-/rIZn8X9bBzC7KvY4iKUhXUGW3MmbXwfPF23jC9wT9xTi7kAvgj8sEgwxjixBmoL6MVa4WOgxNz2hAR6wTK8tw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-string@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-5.0.2.tgz" + integrity sha512-zaI1yzwL+a/FkIzUWMQoH25YwCYxi917J4pYm1nRXtdgiCdnlTkx5eRzqWEC64HtRa06WCJ9TIutpb6GmW4gFw== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-timing-functions@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.0.2.tgz" + integrity sha512-Ao0PP6MoYsRU1LxeVUW740ioknvdIUmfr6uAA3xWlQJ9s69/Tupy8qwhuKG3xWfl+KvLMAP9p2WXF9cwuk/7Bg== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize-unicode@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-5.0.2.tgz" + integrity sha512-3y/V+vjZ19HNcTizeqwrbZSUsE69ZMRHfiiyLAJb7C7hJtYmM4Gsbajy7gKagu97E8q5rlS9k8FhojA8cpGhWw== + dependencies: + browserslist "^4.16.6" + postcss-value-parser "^4.2.0" + +postcss-normalize-url@^5.0.4: + version "5.0.4" + resolved "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-5.0.4.tgz" + integrity sha512-cNj3RzK2pgQQyNp7dzq0dqpUpQ/wYtdDZM3DepPmFjCmYIfceuD9VIAcOdvrNetjIU65g1B4uwdP/Krf6AFdXg== + dependencies: + normalize-url "^6.0.1" + postcss-value-parser "^4.2.0" + +postcss-normalize-whitespace@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.0.2.tgz" + integrity sha512-CXBx+9fVlzSgbk0IXA/dcZn9lXixnQRndnsPC5ht3HxlQ1bVh77KQDL1GffJx1LTzzfae8ftMulsjYmO2yegxA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-normalize@^10.0.1: + version "10.0.1" + resolved "https://registry.npmjs.org/postcss-normalize/-/postcss-normalize-10.0.1.tgz" + integrity sha512-+5w18/rDev5mqERcG3W5GZNMJa1eoYYNGo8gB7tEwaos0ajk3ZXAI4mHGcNT47NE+ZnZD1pEpUOFLvltIwmeJA== + dependencies: + "@csstools/normalize.css" "*" + postcss-browser-comments "^4" + sanitize.css "*" + +postcss-ordered-values@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-5.0.3.tgz" + integrity sha512-T9pDS+P9bWeFvqivXd5ACzQmrCmHjv3ZP+djn8E1UZY7iK79pFSm7i3WbKw2VSmFmdbMm8sQ12OPcNpzBo3Z2w== + dependencies: + cssnano-utils "^3.0.0" + postcss-value-parser "^4.2.0" + +postcss-overflow-shorthand@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-3.0.2.tgz" + integrity sha512-odBMVt6PTX7jOE9UNvmnLrFzA9pXS44Jd5shFGGtSHY80QCuJF+14McSy0iavZggRZ9Oj//C9vOKQmexvyEJMg== + +postcss-page-break@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz" + integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ== + +postcss-place@^7.0.3: + version "7.0.3" + resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-7.0.3.tgz" + integrity sha512-tDQ3m+GYoOar+KoQgj+pwPAvGHAp/Sby6vrFiyrELrMKQJ4AejL0NcS0mm296OKKYA2SRg9ism/hlT/OLhBrdQ== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-preset-env@^7.0.1: + version "7.2.3" + resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-7.2.3.tgz" + integrity sha512-Ok0DhLfwrcNGrBn8sNdy1uZqWRk/9FId0GiQ39W4ILop5GHtjJs8bu1MY9isPwHInpVEPWjb4CEcEaSbBLpfwA== + dependencies: + autoprefixer "^10.4.2" + browserslist "^4.19.1" + caniuse-lite "^1.0.30001299" + css-blank-pseudo "^3.0.2" + css-has-pseudo "^3.0.3" + css-prefers-color-scheme "^6.0.2" + cssdb "^5.0.0" + postcss-attribute-case-insensitive "^5.0.0" + postcss-color-functional-notation "^4.2.1" + postcss-color-hex-alpha "^8.0.2" + postcss-color-rebeccapurple "^7.0.2" + postcss-custom-media "^8.0.0" + postcss-custom-properties "^12.1.2" + postcss-custom-selectors "^6.0.0" + postcss-dir-pseudo-class "^6.0.3" + postcss-double-position-gradients "^3.0.4" + postcss-env-function "^4.0.4" + postcss-focus-visible "^6.0.3" + postcss-focus-within "^5.0.3" + postcss-font-variant "^5.0.0" + postcss-gap-properties "^3.0.2" + postcss-image-set-function "^4.0.4" + postcss-initial "^4.0.1" + postcss-lab-function "^4.0.3" + postcss-logical "^5.0.3" + postcss-media-minmax "^5.0.0" + postcss-nesting "^10.1.2" + postcss-overflow-shorthand "^3.0.2" + postcss-page-break "^3.0.4" + postcss-place "^7.0.3" + postcss-pseudo-class-any-link "^7.0.2" + postcss-replace-overflow-wrap "^4.0.0" + postcss-selector-not "^5.0.0" + +postcss-pseudo-class-any-link@^7.0.2: + version "7.0.2" + resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-7.0.2.tgz" + integrity sha512-CG35J1COUH7OOBgpw5O+0koOLUd5N4vUGKUqSAuIe4GiuLHWU96Pqp+UPC8QITTd12zYAFx76pV7qWT/0Aj/TA== + dependencies: + postcss-selector-parser "^6.0.8" + +postcss-reduce-initial@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-5.0.2.tgz" + integrity sha512-v/kbAAQ+S1V5v9TJvbGkV98V2ERPdU6XvMcKMjqAlYiJ2NtsHGlKYLPjWWcXlaTKNxooId7BGxeraK8qXvzKtw== + dependencies: + browserslist "^4.16.6" + caniuse-api "^3.0.0" + +postcss-reduce-transforms@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-5.0.2.tgz" + integrity sha512-25HeDeFsgiPSUx69jJXZn8I06tMxLQJJNF5h7i9gsUg8iP4KOOJ8EX8fj3seeoLt3SLU2YDD6UPnDYVGUO7DEA== + dependencies: + postcss-value-parser "^4.2.0" + +postcss-replace-overflow-wrap@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz" + integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw== + +postcss-selector-not@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-5.0.0.tgz" + integrity sha512-/2K3A4TCP9orP4TNS7u3tGdRFVKqz/E6pX3aGnriPG0jU78of8wsUcqE4QAhWEU0d+WnMSF93Ah3F//vUtK+iQ== + dependencies: + balanced-match "^1.0.0" + +postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5: + version "6.0.8" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.8.tgz" + integrity sha512-D5PG53d209Z1Uhcc0qAZ5U3t5HagH3cxu+WLZ22jt3gLUpXM4eXXfiO14jiDWST3NNooX/E8wISfOhZ9eIjGTQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.8: + version "6.0.9" + resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.9.tgz" + integrity sha512-UO3SgnZOVTwu4kyLR22UQ1xZh086RyNZppb7lLAKBFK8a32ttG5i87Y/P3+2bRSjZNyJ1B7hfFNo273tKe9YxQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-svgo@^5.0.3: + version "5.0.3" + resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.0.3.tgz" + integrity sha512-41XZUA1wNDAZrQ3XgWREL/M2zSw8LJPvb5ZWivljBsUQAGoEKMYm6okHsTjJxKYI4M75RQEH4KYlEM52VwdXVA== + dependencies: + postcss-value-parser "^4.1.0" + svgo "^2.7.0" + +postcss-unique-selectors@^5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-5.0.2.tgz" + integrity sha512-w3zBVlrtZm7loQWRPVC0yjUwwpty7OM6DnEHkxcSQXO1bMS3RJ+JUS5LFMSDZHJcvGsRwhZinCWVqn8Kej4EDA== + dependencies: + alphanum-sort "^1.0.2" + postcss-selector-parser "^6.0.5" + +postcss-value-parser@^4.0.2, postcss-value-parser@^4.1.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@^7.0.35: + version "7.0.39" + resolved "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz" + integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== + dependencies: + picocolors "^0.2.1" + source-map "^0.6.1" + +postcss@^8.1.10: + version "8.4.16" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz" + integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ== + dependencies: + nanoid "^3.3.4" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8.2.15, postcss@^8.3.5, postcss@^8.4.4: + version "8.4.5" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.5.tgz" + integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg== + dependencies: + nanoid "^3.1.30" + picocolors "^1.0.0" + source-map-js "^1.0.1" + +postcss@^8.4.18: + version "8.4.24" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" + integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +postgres-array@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz" + integrity sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA== + +postgres-bytea@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz" + integrity sha1-AntTPAqokOJtFy1Hz5zOzFIazTU= + +postgres-date@~1.0.4: + version "1.0.7" + resolved "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz" + integrity sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q== + +postgres-interval@^1.1.0: + version "1.2.0" + resolved "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz" + integrity sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ== + dependencies: + xtend "^4.0.0" + +preact@^10.0.0: + version "10.10.2" + resolved "https://registry.npmjs.org/preact/-/preact-10.10.2.tgz" + integrity sha512-GUXSsfwq4NKhlLYY5ctfNE0IjFk7Xo4952yPI8yMkXdhzeQmQ+FahZITe7CeHXMPyKBVQ8SoCmGNIy9TSOdhgQ== + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prepend-http@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz" + integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= + +prettier-linter-helpers@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz" + integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w== + dependencies: + fast-diff "^1.1.2" + +prettier@^2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== + +prettier@^3.2.5: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + +pretty-bytes@^5.3.0, pretty-bytes@^5.4.1: + version "5.6.0" + resolved "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-error@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz" + integrity sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw== + dependencies: + lodash "^4.17.20" + renderkid "^3.0.0" + +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== + dependencies: + "@jest/types" "^26.6.2" + ansi-regex "^5.0.0" + ansi-styles "^4.0.0" + react-is "^17.0.1" + +pretty-format@^27.0.0, pretty-format@^27.4.6: + version "27.4.6" + resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.4.6.tgz" + integrity sha512-NblstegA1y/RJW2VyML+3LlpFjzx62cUrtBIKIWDXEDkjNeleA7Od7nrzcs/VLQvAeV4CgSYhrN39DRN88Qi/g== + dependencies: + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^17.0.1" + +pretty-format@^29.7.0: + version "29.7.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" + integrity sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ== + dependencies: + "@jest/schemas" "^29.6.3" + ansi-styles "^5.0.0" + react-is "^18.0.0" + +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz" + integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= + +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + +promise@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/promise/-/promise-8.1.0.tgz" + integrity sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q== + dependencies: + asap "~2.0.6" + +prompts@^2.0.1, prompts@^2.4.2: + version "2.4.2" + resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz" + integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.5" + +promzard@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz" + integrity sha1-JqXW7ox97kyxIggwWs+5O6OCqe4= + dependencies: + read "1" + +prop-types@^15.6.2, prop-types@^15.7.2, prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +property-expr@^2.0.4: + version "2.0.5" + resolved "https://registry.npmjs.org/property-expr/-/property-expr-2.0.5.tgz" + integrity sha512-IJUkICM5dP5znhCckHSv30Q4b5/JA5enCtkRHYaOVOAocnH/1BQEYTC5NMfT3AVl/iXKdr3aqQbQn9DxyWknwA== + +proto-list@~1.2.1: + version "1.2.4" + resolved "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz" + integrity sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk= + +protocols@^1.4.0: + version "1.4.8" + resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.8.tgz" + integrity sha512-IgjKyaUSjsROSO8/D49Ab7hP8mJgTYcqApOqdPhLoPxAplXmkp+zRvsrSQjFn5by0rhm4VH0GAUELIPpx7B1yg== + +protocols@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz" + integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + +psl@^1.1.28, psl@^1.1.33: + version "1.8.0" + resolved "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +pstree.remy@^1.1.8: + version "1.1.8" + resolved "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz" + integrity sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pupa@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz" + integrity sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A== + dependencies: + escape-goat "^2.0.0" + +q@^1.1.2, q@^1.5.1: + version "1.5.1" + resolved "https://registry.npmjs.org/q/-/q-1.5.1.tgz" + integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= + +qs@6.11.0, qs@^6.11.0, qs@^6.9.4: + version "6.11.0" + resolved "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +qs@6.9.7: + version "6.9.7" + resolved "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz" + integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== + +qs@~6.5.2: + version "6.5.3" + resolved "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz" + integrity sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA== + +query-string@^6.13.8: + version "6.14.1" + resolved "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz" + integrity sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw== + dependencies: + decode-uri-component "^0.2.0" + filter-obj "^1.1.0" + split-on-first "^1.0.0" + strict-uri-encode "^2.0.0" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + +raf@^3.4.1: + version "3.4.1" + resolved "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz" + integrity sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA== + dependencies: + performance-now "^2.1.0" + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" + integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== + dependencies: + bytes "3.1.0" + http-errors "1.7.3" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.4.3: + version "2.4.3" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.3.tgz" + integrity sha512-UlTNLIcu0uzb4D2f4WltY6cVjLi+/jEN4lgEUj3E04tpMDpUlkBo/eSn6zou9hum2VMNpCCUone0O0WeJim07g== + dependencies: + bytes "3.1.2" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@2.5.1: + version "2.5.1" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz" + integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@^2.4.1: + version "2.4.2" + resolved "https://registry.npmjs.org/raw-body/-/raw-body-2.4.2.tgz" + integrity sha512-RPMAFUJP19WIet/99ngh6Iv8fzAbqum4Li7AD6DtGaW2RpMB/11xDoalPiJMTbu6I3hkbMVkATvZrqb9EEqeeQ== + dependencies: + bytes "3.1.1" + http-errors "1.8.1" + iconv-lite "0.4.24" + unpipe "1.0.0" + +raw-body@^2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +rc@^1.2.8: + version "1.2.8" + resolved "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz" + 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-app-polyfill@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-3.0.0.tgz" + integrity sha512-sZ41cxiU5llIB003yxxQBYrARBqe0repqPTTYBTmMqTz9szeBbE37BehCE891NZsmdZqqP+xWKdT3eo3vOzN8w== + dependencies: + core-js "^3.19.2" + object-assign "^4.1.1" + promise "^8.1.0" + raf "^3.4.1" + regenerator-runtime "^0.13.9" + whatwg-fetch "^3.6.2" + +react-base16-styling@^0.9.1: + version "0.9.1" + resolved "https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.9.1.tgz" + integrity sha512-1s0CY1zRBOQ5M3T61wetEpvQmsYSNtWEcdYzyZNxKa8t7oDvaOn9d21xrGezGAHFWLM7SHcktPuPTrvoqxSfKw== + dependencies: + "@babel/runtime" "^7.16.7" + "@types/base16" "^1.0.2" + "@types/lodash" "^4.14.178" + base16 "^1.0.0" + color "^3.2.1" + csstype "^3.0.10" + lodash.curry "^4.1.1" + +react-dev-utils@^12.0.0: + version "12.0.0" + resolved "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.0.tgz" + integrity sha512-xBQkitdxozPxt1YZ9O1097EJiVpwHr9FoAuEVURCKV0Av8NBERovJauzP7bo1ThvuhZ4shsQ1AJiu4vQpoT1AQ== + dependencies: + "@babel/code-frame" "^7.16.0" + address "^1.1.2" + browserslist "^4.18.1" + chalk "^4.1.2" + cross-spawn "^7.0.3" + detect-port-alt "^1.1.6" + escape-string-regexp "^4.0.0" + filesize "^8.0.6" + find-up "^5.0.0" + fork-ts-checker-webpack-plugin "^6.5.0" + global-modules "^2.0.0" + globby "^11.0.4" + gzip-size "^6.0.0" + immer "^9.0.7" + is-root "^2.1.0" + loader-utils "^3.2.0" + open "^8.4.0" + pkg-up "^3.1.0" + prompts "^2.4.2" + react-error-overlay "^6.0.10" + recursive-readdir "^2.2.2" + shell-quote "^1.7.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +react-dom@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-error-overlay@^6.0.10: + version "6.0.10" + resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.10.tgz" + integrity sha512-mKR90fX7Pm5seCOfz8q9F+66VCc1PGsWSBxKbITjfKVQHMNF2zudxHnMdJiB1fRCb+XsbQV9sO9DCkgsMQgBIA== + +react-hook-form@^7.45.2: + version "7.45.2" + resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.45.2.tgz" + integrity sha512-9s45OdTaKN+4NSTbXVqeDITd/nwIg++nxJGL8+OD5uf1DxvhsXQ641kaYHk5K28cpIOTYm71O/fYk7rFaygb3A== + +react-intl@^5.20.12: + version "5.24.3" + resolved "https://registry.npmjs.org/react-intl/-/react-intl-5.24.3.tgz" + integrity sha512-SrV0Qs8Rg+Mlo2u0OqGJZ3pH3cF0lv3cVtHvVPksptrjlgvt6Lbc4vfzD1nPog/CPKzSSdNlLoMs5suHFdBnTw== + dependencies: + "@formatjs/ecma402-abstract" "1.11.1" + "@formatjs/icu-messageformat-parser" "2.0.16" + "@formatjs/intl" "1.18.3" + "@formatjs/intl-displaynames" "5.4.0" + "@formatjs/intl-listformat" "6.5.0" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/react" "16 || 17" + hoist-non-react-statics "^3.3.2" + intl-messageformat "9.11.2" + tslib "^2.1.0" + +react-is@^16.13.1, react-is@^16.7.0: + version "16.13.1" + resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-is@^17.0.1: + version "17.0.2" + resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz" + integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== + +react-is@^18.0.0, react-is@^18.2.0: + version "18.2.0" + resolved "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + +react-json-tree@^0.16.2: + version "0.16.2" + resolved "https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.16.2.tgz" + integrity sha512-80F7ZTqeOl1YaS/sDce4tYBcSe69/d0mlUmcIhyXezPFctWrtvyN56EMExX9jWsq3XMdvsUKKPUeNo8QCBy2jg== + dependencies: + "@babel/runtime" "^7.17.8" + "@types/prop-types" "^15.7.4" + prop-types "^15.8.1" + react-base16-styling "^0.9.1" + +react-refresh@^0.11.0: + version "0.11.0" + resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" + integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== + +react-router-dom@^6.0.2: + version "6.2.1" + resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.2.1.tgz" + integrity sha512-I6Zax+/TH/cZMDpj3/4Fl2eaNdcvoxxHoH1tYOREsQ22OKDYofGebrNm6CTPUcvLvZm63NL/vzCYdjf9CUhqmA== + dependencies: + history "^5.2.0" + react-router "6.2.1" + +react-router@6.2.1: + version "6.2.1" + resolved "https://registry.npmjs.org/react-router/-/react-router-6.2.1.tgz" + integrity sha512-2fG0udBtxou9lXtK97eJeET2ki5//UWfQSl1rlJ7quwe6jrktK9FCCc8dQb5QY6jAv3jua8bBQRhhDOM/kVRsg== + dependencies: + history "^5.2.0" + +react-scripts@5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz" + integrity sha512-3i0L2CyIlROz7mxETEdfif6Sfhh9Lfpzi10CtcGs1emDQStmZfWjJbAIMtRD0opVUjQuFWqHZyRZ9PPzKCFxWg== + dependencies: + "@babel/core" "^7.16.0" + "@pmmmwh/react-refresh-webpack-plugin" "^0.5.3" + "@svgr/webpack" "^5.5.0" + babel-jest "^27.4.2" + babel-loader "^8.2.3" + babel-plugin-named-asset-import "^0.3.8" + babel-preset-react-app "^10.0.1" + bfj "^7.0.2" + browserslist "^4.18.1" + camelcase "^6.2.1" + case-sensitive-paths-webpack-plugin "^2.4.0" + css-loader "^6.5.1" + css-minimizer-webpack-plugin "^3.2.0" + dotenv "^10.0.0" + dotenv-expand "^5.1.0" + eslint "^8.3.0" + eslint-config-react-app "^7.0.0" + eslint-webpack-plugin "^3.1.1" + file-loader "^6.2.0" + fs-extra "^10.0.0" + html-webpack-plugin "^5.5.0" + identity-obj-proxy "^3.0.0" + jest "^27.4.3" + jest-resolve "^27.4.2" + jest-watch-typeahead "^1.0.0" + mini-css-extract-plugin "^2.4.5" + postcss "^8.4.4" + postcss-flexbugs-fixes "^5.0.2" + postcss-loader "^6.2.1" + postcss-normalize "^10.0.1" + postcss-preset-env "^7.0.1" + prompts "^2.4.2" + react-app-polyfill "^3.0.0" + react-dev-utils "^12.0.0" + react-refresh "^0.11.0" + resolve "^1.20.0" + resolve-url-loader "^4.0.0" + sass-loader "^12.3.0" + semver "^7.3.5" + source-map-loader "^3.0.0" + style-loader "^3.3.1" + tailwindcss "^3.0.2" + terser-webpack-plugin "^5.2.5" + webpack "^5.64.4" + webpack-dev-server "^4.6.0" + webpack-manifest-plugin "^4.0.2" + workbox-webpack-plugin "^6.4.1" + optionalDependencies: + fsevents "^2.3.2" + +react-transition-group@^4.4.5: + version "4.4.5" + resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" + integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== + dependencies: + "@babel/runtime" "^7.5.5" + dom-helpers "^5.0.1" + loose-envify "^1.4.0" + prop-types "^15.6.2" + +react-window@^1.8.9: + version "1.8.9" + resolved "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz" + integrity sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + +react@^18.2.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +reactflow@^11.11.2: + version "11.11.2" + resolved "https://registry.yarnpkg.com/reactflow/-/reactflow-11.11.2.tgz#4968866a9372e6004ad1e424a2141996f0ba769a" + integrity sha512-o1fT3stSdhzW+SedCGNSmEvZvULZygZIMLyW67NcWNZrgwx1wuJfzLg5fuQ0Nzf389wItumZX/zP3zdaPX7lEw== + dependencies: + "@reactflow/background" "11.3.12" + "@reactflow/controls" "11.2.12" + "@reactflow/core" "11.11.2" + "@reactflow/minimap" "11.7.12" + "@reactflow/node-resizer" "2.2.12" + "@reactflow/node-toolbar" "1.3.12" + +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== + +read-package-json-fast@^2.0.1: + version "2.0.3" + resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.3.tgz" + integrity sha512-W/BKtbL+dUjTuRL2vziuYhp76s5HZ9qQhd/dKfWIZveD0O40453QNyZhC0e63lqZrAQ4jiOapVoeJ7JrszenQQ== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^2.0.0: + version "2.1.2" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz" + integrity sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^2.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.1.tgz" + integrity sha512-aLcPqxovhJTVJcsnROuuzQvv6oziQx4zd3JvG0vGCL5MjTONUc4uJ90zCBC6R7W7oUKBNoR/F8pkyfVwlbxqng== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-json@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-4.1.1.tgz" + integrity sha512-P82sbZJ3ldDrWCOSKxJT0r/CXMWR0OR3KRh55SgKo3p91GSIEEC32v3lSHAvO/UcH3/IoL7uqhOFBduAnwdldw== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@^5.3.1: + version "5.3.1" + resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz" + integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== + dependencies: + read-package-json "^2.0.0" + readdir-scoped-modules "^1.0.0" + util-promisify "^2.1.0" + +read-pkg-up@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz" + integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= + dependencies: + find-up "^2.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz" + 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" + +read@1, read@~1.0.1: + version "1.0.7" + resolved "https://registry.npmjs.org/read/-/read-1.0.7.tgz" + integrity sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ= + dependencies: + mute-stream "~0.0.4" + +readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.0.6, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdir-scoped-modules@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.1.0.tgz" + integrity sha512-asaikDeqAQg7JifRsZn1NJZXo9E+VwlyCfbkZhwyISinqk5zNS6266HS5kah6P0SaQKGF6SkNnZVHUzHFYxYDw== + dependencies: + debuglog "^1.0.1" + dezalgo "^1.0.0" + graceful-fs "^4.1.2" + once "^1.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.8.0: + version "0.8.0" + resolved "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz" + integrity sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ== + dependencies: + resolve "^1.20.0" + +recursive-readdir@^2.2.2: + version "2.2.2" + resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz" + integrity sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg== + dependencies: + minimatch "3.0.4" + +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== + dependencies: + indent-string "^4.0.0" + strip-indent "^3.0.0" + +redis-commands@1.7.0: + version "1.7.0" + resolved "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz" + integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-info@^3.0.8: + version "3.1.0" + resolved "https://registry.npmjs.org/redis-info/-/redis-info-3.1.0.tgz" + integrity sha512-ER4L9Sh/vm63DkIE0bkSjxluQlioBiBgf5w1UuldaW/3vPcecdljVDisZhmnCMvsxHNiARTTDDHGg9cGwTfrKg== + dependencies: + lodash "^4.17.11" + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +regenerate-unicode-properties@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz" + integrity sha512-3E12UeNSPfjrgwjkR81m5J7Aw/T55Tu7nUyZVQYCKEOs+2dkxEY+DpPtZzO4YruuiPb7NkYLVcyJC4+zCbk5pA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.9: + version "0.13.9" + resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regenerator-transform@^0.14.2: + version "0.14.5" + resolved "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz" + integrity sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw== + dependencies: + "@babel/runtime" "^7.8.4" + +regex-parser@^2.2.11: + version "2.2.11" + resolved "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz" + integrity sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q== + +regexp.prototype.flags@^1.2.0, regexp.prototype.flags@^1.3.1: + version "1.4.1" + resolved "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz" + integrity sha512-pMR7hBVUUGI7PMA37m2ofIdQCsomVnas+Jn5UPGAHQ+/LlwKm/aTLJHdasmHRzlfeZwHiAOaRSo2rbBDm3nNUQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +regexpu-core@^4.7.1: + version "4.8.0" + resolved "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.8.0.tgz" + integrity sha512-1F6bYsoYiz6is+oz70NWur2Vlh9KWtswuRuzJOfeYUrfPX2o8n74AnUVaOGDbUqVGO9fNHu48/pjJO4sNVwsOg== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^9.0.0" + regjsgen "^0.5.2" + regjsparser "^0.7.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.0.0" + +registry-auth-token@^4.0.0: + version "4.2.1" + resolved "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz" + integrity sha512-6gkSb4U6aWJB4SF2ZvLb76yCBjcvufXBqvvEx1HbmKPkutswjW1xNVRY0+daljIYRbogN7O0etYSlbiaEQyMyw== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== + dependencies: + rc "^1.2.8" + +regjsgen@^0.5.2: + version "0.5.2" + resolved "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== + +regjsparser@^0.7.0: + version "0.7.0" + resolved "https://registry.npmjs.org/regjsparser/-/regjsparser-0.7.0.tgz" + integrity sha512-A4pcaORqmNMDVwUjWoTzuhwMGpP+NykpfqAsEgI1FSH/EzC7lrN5TMd+kN8YCovX+jMpu8eaqXgXPCa0g8FQNQ== + dependencies: + jsesc "~0.5.0" + +relateurl@^0.2.7: + version "0.2.7" + resolved "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz" + integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +remove-trailing-slash@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/remove-trailing-slash/-/remove-trailing-slash-0.1.1.tgz" + integrity sha512-o4S4Qh6L2jpnCy83ysZDau+VORNvnFw07CKSAymkd6ICNVEPisMyzlc00KlvvicsxKck94SEwhDnMNdICzO+tA== + +renderkid@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz" + integrity sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg== + dependencies: + css-select "^4.1.3" + dom-converter "^0.2.0" + htmlparser2 "^6.1.0" + lodash "^4.17.21" + strip-ansi "^6.0.1" + +request@^2.88.0, request@^2.88.2: + version "2.88.2" + resolved "https://registry.npmjs.org/request/-/request-2.88.2.tgz" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@5.0.0, resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-url-loader@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz" + integrity sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA== + dependencies: + adjust-sourcemap-loader "^4.0.0" + convert-source-map "^1.7.0" + loader-utils "^2.0.0" + postcss "^7.0.35" + source-map "0.6.1" + +resolve.exports@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz" + integrity sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ== + +resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.19.0, resolve@^1.20.0: + version "1.21.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.21.0.tgz" + integrity sha512-3wCbTpk5WJlyE4mSOtDLhqQmGFi0/TD9VPwmiolnk8U0wRgMEktqCXd3vy5buTO3tljvalNvKrjHEfrd2WpEKA== + dependencies: + is-core-module "^2.8.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.21.0: + version "1.22.0" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^1.22.1: + version "1.22.1" + resolved "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.3: + version "2.0.0-next.3" + resolved "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz" + integrity sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q== + dependencies: + is-core-module "^2.2.0" + path-parse "^1.0.6" + +responselike@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz" + integrity sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec= + dependencies: + lowercase-keys "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +retry@^0.12.0: + version "0.12.0" + resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.0, rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup-plugin-terser@^7.0.0: + version "7.0.2" + resolved "https://registry.npmjs.org/rollup-plugin-terser/-/rollup-plugin-terser-7.0.2.tgz" + integrity sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ== + dependencies: + "@babel/code-frame" "^7.10.4" + jest-worker "^26.2.1" + serialize-javascript "^4.0.0" + terser "^5.0.0" + +rollup@^2.43.1: + version "2.66.0" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.66.0.tgz" + integrity sha512-L6mKOkdyP8HK5kKJXaiWG7KZDumPJjuo1P+cfyHOJPNNTK3Moe7zCH5+fy7v8pVmHXtlxorzaBjvkBMB23s98g== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^2.79.1: + version "2.79.1" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz" + integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^4.13.0: + version "4.14.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.14.0.tgz#c3e2cd479f1b2358b65c1f810fa05b51603d7be8" + integrity sha512-Qe7w62TyawbDzB4yt32R0+AbIo6m1/sqO7UPzFS8Z/ksL5mrfhA0v4CavfdmFav3D+ub4QeAgsGEe84DoWe/nQ== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.14.0" + "@rollup/rollup-android-arm64" "4.14.0" + "@rollup/rollup-darwin-arm64" "4.14.0" + "@rollup/rollup-darwin-x64" "4.14.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.14.0" + "@rollup/rollup-linux-arm64-gnu" "4.14.0" + "@rollup/rollup-linux-arm64-musl" "4.14.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.14.0" + "@rollup/rollup-linux-riscv64-gnu" "4.14.0" + "@rollup/rollup-linux-s390x-gnu" "4.14.0" + "@rollup/rollup-linux-x64-gnu" "4.14.0" + "@rollup/rollup-linux-x64-musl" "4.14.0" + "@rollup/rollup-win32-arm64-msvc" "4.14.0" + "@rollup/rollup-win32-ia32-msvc" "4.14.0" + "@rollup/rollup-win32-x64-msvc" "4.14.0" + fsevents "~2.3.2" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@^6.6.0: + version "6.6.7" + resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-stable-stringify@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-1.1.1.tgz" + integrity sha512-ERq4hUjKDbJfE4+XtZLFPCDi8Vb1JqaxAPTxWFLBx8XcAlf9Bda/ZJdVezs/NAfsMQScyIlUMx+Yeu7P7rx5jw== + +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sanitize.css@*: + version "13.0.0" + resolved "https://registry.npmjs.org/sanitize.css/-/sanitize.css-13.0.0.tgz" + integrity sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA== + +sass-loader@^12.3.0: + version "12.4.0" + resolved "https://registry.npmjs.org/sass-loader/-/sass-loader-12.4.0.tgz" + integrity sha512-7xN+8khDIzym1oL9XyS6zP6Ges+Bo2B2xbPrjdMHEYyV3AQYhd/wXeru++3ODHF0zMjYmVadblSKrPrjEkL8mg== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sax@1.2.x, sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +saxes@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz" + integrity sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw== + dependencies: + xmlchars "^2.2.0" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +schema-utils@2.7.0: + version "2.7.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.0.tgz" + integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== + dependencies: + "@types/json-schema" "^7.0.4" + ajv "^6.12.2" + ajv-keywords "^3.4.1" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-4.0.0.tgz" + integrity sha512-1edyXKgh6XnJsJSQ8mKWXnN/BVaIbFMLpouRUrXgVq7WYne5kw3MW7UPhO44uRXQSIpTSXoJbmrR2X0w9kUTyg== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.8.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.0.0" + +scroll-into-view-if-needed@^2.2.20: + version "2.2.29" + resolved "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.29.tgz" + integrity sha512-hxpAR6AN+Gh53AdAimHM6C8oTN1ppwVZITihix+WqalywBeFcQ6LdQP5ABNl26nX8GTEL7VT+b8lKpdqq65wXg== + dependencies: + compute-scroll-into-view "^1.0.17" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz" + integrity sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo= + +selfsigned@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/selfsigned/-/selfsigned-2.0.0.tgz" + integrity sha512-cUdFiCbKoa1mZ6osuJs2uDHrs0k0oprsKveFiiaBKCNq3SYyb5gs2HxhQyDNLCmL51ZZThqi4YNDpCK6GOP1iQ== + dependencies: + node-forge "^1.2.0" + +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== + dependencies: + semver "^6.3.0" + +"semver@2 || 3 || 4 || 5", semver@^5.6.0, semver@^5.7.1: + version "5.7.2" + resolved "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz" + integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== + +semver@7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz" + integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.2.0, semver@^6.3.0: + version "6.3.1" + resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.1.1, semver@^7.1.3, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.5.4" + resolved "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + +semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + +send@0.17.2: + version "0.17.2" + resolved "https://registry.npmjs.org/send/-/send-0.17.2.tgz" + integrity sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "1.8.1" + mime "1.6.0" + ms "2.1.3" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.npmjs.org/send/-/send-0.18.0.tgz" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz" + integrity sha1-03aNabHn2C5c4FD/9bRTvqEqkjk= + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.14.2: + version "1.14.2" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz" + integrity sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-blocking@^2.0.0, set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz" + integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== + +shiki@^0.11.1: + version "0.11.1" + resolved "https://registry.npmjs.org/shiki/-/shiki-0.11.1.tgz" + integrity sha512-EugY9VASFuDqOexOgXR18ZV+TbFrQHeCpEYaXamO+SZlsnT/2LxuLBX25GGtIrwaEVFXUAbUQ601SWE2rMwWHA== + dependencies: + jsonc-parser "^3.0.0" + vscode-oniguruma "^1.6.1" + vscode-textmate "^6.0.0" + +showdown@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/showdown/-/showdown-2.1.0.tgz" + integrity sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ== + dependencies: + commander "^9.0.0" + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +siginfo@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/siginfo/-/siginfo-2.0.0.tgz#32e76c70b79724e3bb567cb9d543eb858ccfaf30" + integrity sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +signal-exit@^4.0.1, signal-exit@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + +sisteransi@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz" + integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== + +sitemap@^7.1.1: + version "7.1.1" + resolved "https://registry.npmjs.org/sitemap/-/sitemap-7.1.1.tgz" + integrity sha512-mK3aFtjz4VdJN0igpIJrinf3EO8U8mxOPsTBzSsy06UtjZQJ3YY3o3Xa7zSc5nMqcMrRwlChHZ18Kxg0caiPBg== + dependencies: + "@types/node" "^17.0.5" + "@types/sax" "^1.2.1" + arg "^5.0.0" + sax "^1.2.4" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +slate-history@^0.93.0: + version "0.93.0" + resolved "https://registry.npmjs.org/slate-history/-/slate-history-0.93.0.tgz" + integrity sha512-Gr1GMGPipRuxIz41jD2/rbvzPj8eyar56TVMyJBvBeIpQSSjNISssvGNDYfJlSWM8eaRqf6DAcxMKzsLCYeX6g== + dependencies: + is-plain-object "^5.0.0" + +slate-react@^0.94.2: + version "0.94.2" + resolved "https://registry.npmjs.org/slate-react/-/slate-react-0.94.2.tgz" + integrity sha512-4wDSuTuGBkdQ609CS55uc2Yhfa5but21usBgAtCVhPJQazL85kzN2vUUYTmGb7d/mpP9tdnJiVPopIyhqlRJ8Q== + dependencies: + "@juggle/resize-observer" "^3.4.0" + "@types/is-hotkey" "^0.1.1" + "@types/lodash" "^4.14.149" + direction "^1.0.3" + is-hotkey "^0.1.6" + is-plain-object "^5.0.0" + lodash "^4.17.4" + scroll-into-view-if-needed "^2.2.20" + tiny-invariant "1.0.6" + +slate@^0.94.1: + version "0.94.1" + resolved "https://registry.npmjs.org/slate/-/slate-0.94.1.tgz" + integrity sha512-GH/yizXr1ceBoZ9P9uebIaHe3dC/g6Plpf9nlUwnvoyf6V1UOYrRwkabtOCd3ZfIGxomY4P7lfgLr7FPH8/BKA== + dependencies: + immer "^9.0.6" + is-plain-object "^5.0.0" + tiny-warning "^1.0.3" + +slide@^1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz" + integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= + +smart-buffer@^4.1.0, smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +sockjs@^0.3.21: + version "0.3.24" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks-proxy-agent@^6.0.0: + version "6.1.1" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.1.1.tgz" + integrity sha512-t8J0kG3csjA4g6FTbsMOWws+7R7vuRC8aQ/wy3/1OWmsgwA68zs/+cExQ0koSitUDXqhufF/YJr9wtNMZHw5Ew== + dependencies: + agent-base "^6.0.2" + debug "^4.3.1" + socks "^2.6.1" + +socks-proxy-agent@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-8.0.3.tgz#6b2da3d77364fde6292e810b496cb70440b9b89d" + integrity sha512-VNegTZKhuGq5vSD6XNKlbqWhyt/40CgoEw8XxD6dhnm8Jq9IEa3nIa4HwnM8XOqU0CdB0BwWVXusqiFXfHB3+A== + dependencies: + agent-base "^7.1.1" + debug "^4.3.4" + socks "^2.7.1" + +socks@^2.3.3, socks@^2.6.1: + version "2.6.1" + resolved "https://registry.npmjs.org/socks/-/socks-2.6.1.tgz" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + +socks@^2.7.1: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +sort-keys@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz" + integrity sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg= + dependencies: + is-plain-obj "^1.0.0" + +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + +source-list-map@^2.0.0, source-list-map@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz" + integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw== + +source-map-js@^1.0.1, source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +source-map-loader@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.1.tgz" + integrity sha512-Vp1UsfyPvgujKQzi4pyDiTOnE3E4H+yHvkVRN3c/9PJmQS4CQJExvcDvaX/D+RV+xQben9HJ56jMJS3CgUeWyA== + dependencies: + abab "^2.0.5" + iconv-lite "^0.6.3" + source-map-js "^1.0.1" + +source-map-resolve@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz" + integrity sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + +source-map-support@^0.5.6, source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" + 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.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz" + integrity sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw== + +source-map@0.6.1, source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.5.0, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spdx-correct@^3.0.0: + version "3.1.1" + resolved "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz" + 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.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz" + integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + +spdx-expression-parse@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz" + 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.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz" + integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +split-on-first@^1.0.0: + version "1.1.0" + resolved "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz" + integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== + +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== + dependencies: + readable-stream "^3.0.0" + +split2@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/split2/-/split2-4.1.0.tgz" + integrity sha512-VBiJxFkxiXRlUIeyMQi8s4hgvKCSjtknJv/LVYbrgALPwf5zSKmEwV9Lst25AkvMDnvxODugjdl6KZgwKM1WYQ== + +split@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/split/-/split-1.0.1.tgz" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.17.0" + resolved "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz" + integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +ssri@^10.0.0: + version "10.0.5" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-10.0.5.tgz#e49efcd6e36385196cb515d3a2ad6c3f0265ef8c" + integrity sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A== + dependencies: + minipass "^7.0.3" + +ssri@^8.0.0, ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + +stable@^0.1.8: + version "0.1.8" + resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz" + integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== + +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + +stack-utils@^2.0.3: + version "2.0.5" + resolved "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz" + integrity sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA== + dependencies: + escape-string-regexp "^2.0.0" + +stackback@0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" + integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== + +stackframe@^1.1.1: + version "1.2.0" + resolved "https://registry.npmjs.org/stackframe/-/stackframe-1.2.0.tgz" + integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA== + +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +std-env@^3.5.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2" + integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +strict-uri-encode@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz" + integrity sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ== + +string-length@^4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz" + integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== + dependencies: + char-regex "^1.0.2" + strip-ansi "^6.0.0" + +string-length@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/string-length/-/string-length-5.0.1.tgz" + integrity sha512-9Ep08KAMUn0OadnVaBuRdE2l615CQ508kr0XMadjClfYpdCyvrbFp6Taebo8yyxokQ4viUd/xPPUA4FGgUa0ow== + dependencies: + char-regex "^2.0.0" + strip-ansi "^7.0.1" + +string-natural-compare@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/string-natural-compare/-/string-natural-compare-3.0.1.tgz" + integrity sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw== + +"string-width-cjs@npm:string-width@^4.2.0": + 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== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"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.npmjs.org/string-width/-/string-width-4.2.3.tgz" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.6: + version "4.0.6" + resolved "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.6.tgz" + integrity sha512-6WgDX8HmQqvEd7J+G6VtAahhsQIssiZ8zl7zKh1VDMFyL3hRTJP4FTNA3RbIp2TOQ9AYNDcc7e3fH0Qbup+DBg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + regexp.prototype.flags "^1.3.1" + side-channel "^1.0.4" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.0, strip-ansi@^7.0.1: + version "7.0.1" + resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.0.1.tgz" + integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-bom@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz" + integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +strip-final-newline@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" + integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +strip-literal@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.3.0.tgz#db3942c2ec1699e6836ad230090b84bb458e3a07" + integrity sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg== + dependencies: + acorn "^8.10.0" + +strnum@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" + integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== + +strong-log-transformer@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz" + integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== + dependencies: + duplexer "^0.1.1" + minimist "^1.2.0" + through "^2.3.4" + +style-loader@^3.3.1: + version "3.3.1" + resolved "https://registry.npmjs.org/style-loader/-/style-loader-3.3.1.tgz" + integrity sha512-GPcQ+LDJbrcxHORTRes6Jy2sfvK2kS6hpSfI/fXhPt+spVzxF6LJ1dHLN9zIGmVaaP044YKaIatFaufENRiDoQ== + +stylehacks@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/stylehacks/-/stylehacks-5.0.1.tgz" + integrity sha512-Es0rVnHIqbWzveU1b24kbw92HsebBepxfcqe5iix7t9j0PQqhs0IxXVXv0pY2Bxa08CgMkzD6OWql7kbGOuEdA== + dependencies: + browserslist "^4.16.0" + postcss-selector-parser "^6.0.4" + +stylis@4.0.13: + version "4.0.13" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.0.13.tgz" + integrity sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag== + +stylis@4.1.3: + version "4.1.3" + resolved "https://registry.npmjs.org/stylis/-/stylis-4.1.3.tgz" + integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== + +superagent@^8.0.5: + version "8.1.2" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-8.1.2.tgz#03cb7da3ec8b32472c9d20f6c2a57c7f3765f30b" + integrity sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.4" + debug "^4.3.4" + fast-safe-stringify "^2.1.1" + form-data "^4.0.0" + formidable "^2.1.2" + methods "^1.1.2" + mime "2.6.0" + qs "^6.11.0" + semver "^7.3.8" + +supertest@^6.3.3: + version "6.3.3" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-6.3.3.tgz#42f4da199fee656106fd422c094cf6c9578141db" + integrity sha512-EMCG6G8gDu5qEqRQ3JjjPs6+FYT1a7Hv5ApHvtSghmOFJYtsU5S+pSb6Y2EUeCEY3CmEL3mmQ8YWlPOzQomabA== + dependencies: + methods "^1.1.2" + superagent "^8.0.5" + +supports-color@^5.3.0, supports-color@^5.5.0: + version "5.5.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.0.0, supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-hyperlinks@^2.0.0: + version "2.2.0" + resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz" + integrity sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ== + dependencies: + has-flag "^4.0.0" + supports-color "^7.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svg-parser@^2.0.2: + version "2.0.4" + resolved "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz" + integrity sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ== + +svgo@^1.2.2: + version "1.3.2" + resolved "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz" + integrity sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw== + dependencies: + chalk "^2.4.1" + coa "^2.0.2" + css-select "^2.0.0" + css-select-base-adapter "^0.1.1" + css-tree "1.0.0-alpha.37" + csso "^4.0.2" + js-yaml "^3.13.1" + mkdirp "~0.5.1" + object.values "^1.1.0" + sax "~1.2.4" + stable "^0.1.8" + unquote "~1.1.1" + util.promisify "~1.0.0" + +svgo@^2.7.0: + version "2.8.0" + resolved "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz" + integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== + dependencies: + "@trysound/sax" "0.2.0" + commander "^7.2.0" + css-select "^4.1.3" + css-tree "^1.1.3" + csso "^4.2.0" + picocolors "^1.0.0" + stable "^0.1.8" + +symbol-observable@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz" + integrity sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ== + +symbol-tree@^3.2.4: + version "3.2.4" + resolved "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +tailwindcss@^3.0.2: + version "3.0.15" + resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.0.15.tgz" + integrity sha512-bT2iy7FtjwgsXik4ZoJnHXR+SRCiGR1W95fVqpLZebr64m4ahwUwRbIAc5w5+2fzr1YF4Ct2eI7dojMRRl8sVQ== + dependencies: + arg "^5.0.1" + chalk "^4.1.2" + chokidar "^3.5.2" + color-name "^1.1.4" + cosmiconfig "^7.0.1" + detective "^5.2.0" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.2.7" + glob-parent "^6.0.2" + is-glob "^4.0.3" + normalize-path "^3.0.0" + object-hash "^2.2.0" + postcss-js "^4.0.0" + postcss-load-config "^3.1.0" + postcss-nested "5.0.6" + postcss-selector-parser "^6.0.8" + postcss-value-parser "^4.2.0" + quick-lru "^5.1.1" + resolve "^1.21.0" + +tapable@^1.0.0: + version "1.1.3" + resolved "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz" + integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== + +tapable@^2.0.0, tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +tar@^4.4.12: + version "4.4.19" + resolved "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz" + integrity sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA== + dependencies: + chownr "^1.1.4" + fs-minipass "^1.2.7" + minipass "^2.9.0" + minizlib "^1.3.3" + mkdirp "^0.5.5" + safe-buffer "^5.2.1" + yallist "^3.1.1" + +tar@^6.0.2, tar@^6.1.0, tar@^6.1.11: + version "6.1.11" + resolved "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz" + integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tar@^6.1.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^5.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + +tarn@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz" + integrity sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ== + +temp-dir@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz" + integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== + dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" + temp-dir "^1.0.0" + uuid "^3.3.2" + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terminal-link@^2.0.0: + version "2.1.1" + resolved "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz" + integrity sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ== + dependencies: + ansi-escapes "^4.2.1" + supports-hyperlinks "^2.0.0" + +terser-webpack-plugin@^5.1.3, terser-webpack-plugin@^5.2.5: + version "5.3.0" + resolved "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz" + integrity sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ== + dependencies: + jest-worker "^27.4.1" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + source-map "^0.6.1" + terser "^5.7.2" + +terser@^5.0.0, terser@^5.10.0, terser@^5.7.2: + version "5.14.2" + resolved "https://registry.npmjs.org/terser/-/terser-5.14.2.tgz" + integrity sha512-oL0rGeM/WFQCUd0y2QrWxYnq7tfSuKBiqTjRPWrRgB46WD/kiwHwF8T23z78H6Q6kGCuuHcPB+KULHRdxvVGQA== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +test-exclude@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz" + integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== + dependencies: + "@istanbuljs/schema" "^0.1.2" + glob "^7.1.4" + minimatch "^3.0.4" + +text-extensions@^1.0.0: + version "1.9.0" + resolved "https://registry.npmjs.org/text-extensions/-/text-extensions-1.9.0.tgz" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== + +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +throat@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/throat/-/throat-6.0.1.tgz" + integrity sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w== + +through2@^2.0.0: + version "2.0.5" + resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + +through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6: + version "2.3.8" + resolved "https://registry.npmjs.org/through/-/through-2.3.8.tgz" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tildify@2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz" + integrity sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw== + +timsort@^0.3.0: + version "0.3.0" + resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz" + integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q= + +tiny-invariant@1.0.6: + version "1.0.6" + resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.0.6.tgz" + integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== + +tiny-warning@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" + integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== + +tinybench@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.1.tgz#3408f6552125e53a5a48adee31261686fd71587e" + integrity sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg== + +tinypool@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.8.1.tgz#b6c4e4972ede3e3e5cda74a3da1679303d386b03" + integrity sha512-zBTCK0cCgRROxvs9c0CGK838sPkeokNGdQVUUwHAbynHFlmyJYj825f/oRs528HaIJ97lo0pLIlDUzwN+IorWg== + +tinyspy@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.2.0.tgz#9dc04b072746520b432f77ea2c2d17933de5d6ce" + integrity sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +tmpl@1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz" + integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-readable-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz" + integrity sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + +touch@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz" + integrity sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA== + dependencies: + nopt "~1.0.10" + +tough-cookie@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz" + integrity sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg== + dependencies: + psl "^1.1.33" + punycode "^2.1.1" + universalify "^0.1.2" + +tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tr46@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz" + integrity sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw== + dependencies: + punycode "^2.1.1" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +trim-newlines@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz" + integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== + +triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + +tryer@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz" + integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== + +ts-api-utils@^1.0.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" + integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== + +ts-invariant@^0.10.3: + version "0.10.3" + resolved "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz" + integrity sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ== + dependencies: + tslib "^2.1.0" + +ts-invariant@^0.9.0: + version "0.9.4" + resolved "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.9.4.tgz" + integrity sha512-63jtX/ZSwnUNi/WhXjnK8kz4cHHpYS60AnmA6ixz17l7E12a5puCWFlNpkne5Rl0J8TBPVHpGjsj4fxs8ObVLQ== + dependencies: + tslib "^2.1.0" + +tsconfig-paths@^3.12.0: + version "3.12.0" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@~2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== + +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.4, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz" + 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" + resolved "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= + +ufo@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.3.2.tgz#c7d719d0628a1c80c006d2240e0d169f6e3c0496" + integrity sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA== + +uglify-js@^3.1.4: + version "3.14.5" + resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.14.5.tgz" + integrity sha512-qZukoSxOG0urUTvjc2ERMTcAy+BiFh3weWAkeurLwjrCba73poHmG3E36XEjd/JGukMzwTL7uCxZiAexj8ppvQ== + +uid-number@0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz" + integrity sha1-DqEOgDXo61uOREnwbaHHMGY7qoE= + +umask@^1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz" + integrity sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0= + +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +undefsafe@^2.0.5: + version "2.0.5" + resolved "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz" + integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.0.0.tgz" + integrity sha512-7Yhkc0Ye+t4PNYzOGKedDhXbYIBe1XEQYQxOPyhcXNMJ0WCABqqj6ckydd6pWRZTHV4GuCPKdBAUiMc60tsKVw== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.0.0.tgz" + integrity sha512-5Zfuy9q/DFr4tfO7ZPeVXb1aPoeQSdeFMLpYuFebehDAhbuevLs5yxSZmIFN1tP5F9Wl4IpJrYojg85/zgyZHQ== + +unique-filename@^1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz" + integrity sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ== + dependencies: + unique-slug "^2.0.0" + +unique-filename@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-3.0.0.tgz#48ba7a5a16849f5080d26c760c86cf5cf05770ea" + integrity sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g== + dependencies: + unique-slug "^4.0.0" + +unique-slug@^2.0.0: + version "2.0.2" + resolved "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz" + integrity sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w== + dependencies: + imurmurhash "^0.1.4" + +unique-slug@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-4.0.0.tgz#6bae6bb16be91351badd24cdce741f892a6532e3" + integrity sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ== + dependencies: + imurmurhash "^0.1.4" + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universal-user-agent@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz" + integrity sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w== + +universalify@^0.1.2: + version "0.1.2" + resolved "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +unixify@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz" + integrity sha1-OmQcjC/7zk2mg6XHDwOkYpQMIJA= + dependencies: + normalize-path "^2.1.1" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unquote@~1.1.1: + version "1.1.1" + resolved "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz" + integrity sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ= + +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + +update-notifier@^5.1.0: + version "5.1.0" + resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-5.1.0.tgz" + integrity sha512-ItnICHbeMh9GqUy31hFPrD1kcuZ3rpxDZbf4KUDavXwS0bW5m7SLbDQpGX3UYr072cbrF5hFUs3r5tUsPwjfHw== + dependencies: + 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" + resolved "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +url-parse-lax@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz" + integrity sha1-FrXK/Afb42dsGxmZF3gj1lA6yww= + dependencies: + prepend-http "^2.0.0" + +use-sync-external-store@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" + integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== + +util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util-promisify@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/util-promisify/-/util-promisify-2.1.0.tgz" + integrity sha1-PCI2R2xNMsX/PEcAKt18E7moKlM= + dependencies: + object.getownpropertydescriptors "^2.0.3" + +util.promisify@~1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utila@~0.4: + version "0.4.0" + resolved "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz" + integrity sha1-ihagXURWV6Oupe7MWxKk+lN5dyw= + +utils-merge@1.0.1, utils-merge@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.0, uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +uuid@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + +uuid@^9.0.1: + version "9.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" + integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +v8-to-istanbul@^8.1.0: + version "8.1.1" + resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-8.1.1.tgz" + 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, validate-npm-package-license@^3.0.4: + version "3.0.4" + resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +validate-npm-package-name@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz" + integrity sha1-X6kS2B630MdK/BQN5zF/DKffQ34= + dependencies: + builtins "^1.0.3" + +value-or-promise@1.0.11: + version "1.0.11" + resolved "https://registry.npmjs.org/value-or-promise/-/value-or-promise-1.0.11.tgz" + integrity sha512-41BrgH+dIbCFXClcSapVs5M6GkENd3gQOJpEfPDNa71LsUGMXDL0jMWpI/Rh7WhX+Aalfz2TTS3Zt5pUsbnhLg== + +vary@^1, vary@~1.1.2: + version "1.1.2" + resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +vite-node@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-1.1.3.tgz#196de20a7c2e0467a07da0dd1fe67994f5b79695" + integrity sha512-BLSO72YAkIUuNrOx+8uznYICJfTEbvBAmWClY3hpath5+h1mbPS5OMn42lrTxXuyCazVyZoDkSRnju78GiVCqA== + dependencies: + cac "^6.7.14" + debug "^4.3.4" + pathe "^1.1.1" + picocolors "^1.0.0" + vite "^5.0.0" + +vite@^3.1.6: + version "3.2.10" + resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.10.tgz#7ac79fead82cfb6b5bf65613cd82fba6dcc81340" + integrity sha512-Dx3olBo/ODNiMVk/cA5Yft9Ws+snLOXrhLtrI3F4XLt4syz2Yg8fayZMWScPKoz12v5BUv7VEmQHnsfpY80fYw== + dependencies: + esbuild "^0.15.9" + postcss "^8.4.18" + resolve "^1.22.1" + rollup "^2.79.1" + optionalDependencies: + fsevents "~2.3.2" + +vite@^5.0.0: + version "5.2.8" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.8.tgz#a99e09939f1a502992381395ce93efa40a2844aa" + integrity sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vitepress@^1.0.0-alpha.21: + version "1.0.0-alpha.21" + resolved "https://registry.npmjs.org/vitepress/-/vitepress-1.0.0-alpha.21.tgz" + integrity sha512-D/tkoDW16uUZ9pnWd28Kk1vX26zNiTml3m9oGbfx2pAfYg99PHd1GceZyEm4jZsJU0+n9S++1ctFxoQvsq376A== + dependencies: + "@docsearch/css" "^3.2.1" + "@docsearch/js" "^3.2.1" + "@vitejs/plugin-vue" "^3.1.2" + "@vue/devtools-api" "^6.4.4" + "@vueuse/core" "^9.3.0" + body-scroll-lock "4.0.0-beta.0" + shiki "^0.11.1" + vite "^3.1.6" + vue "^3.2.40" + +vitest@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-1.1.3.tgz#c911bcbcfd2266d44db6ecb08273b91e0ec20dc7" + integrity sha512-2l8om1NOkiA90/Y207PsEvJLYygddsOyr81wLQ20Ra8IlLKbyQncWsGZjnbkyG2KwwuTXLQjEPOJuxGMG8qJBQ== + dependencies: + "@vitest/expect" "1.1.3" + "@vitest/runner" "1.1.3" + "@vitest/snapshot" "1.1.3" + "@vitest/spy" "1.1.3" + "@vitest/utils" "1.1.3" + acorn-walk "^8.3.1" + cac "^6.7.14" + chai "^4.3.10" + debug "^4.3.4" + execa "^8.0.1" + local-pkg "^0.5.0" + magic-string "^0.30.5" + pathe "^1.1.1" + picocolors "^1.0.0" + std-env "^3.5.0" + strip-literal "^1.3.0" + tinybench "^2.5.1" + tinypool "^0.8.1" + vite "^5.0.0" + vite-node "1.1.3" + why-is-node-running "^2.2.2" + +vscode-oniguruma@^1.6.1: + version "1.6.2" + resolved "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.6.2.tgz" + integrity sha512-KH8+KKov5eS/9WhofZR8M8dMHWN2gTxjMsG4jd04YhpbPR91fUj7rYQ2/XjeHCJWbg7X++ApRIU9NUwM2vTvLA== + +vscode-textmate@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-6.0.0.tgz" + integrity sha512-gu73tuZfJgu+mvCSy4UZwd2JXykjK9zAZsfmDeut5dx/1a7FeTk0XwJsSuqQn+cuMCGVbIBfl+s53X4T19DnzQ== + +vue-demi@*: + version "0.13.7" + resolved "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.7.tgz" + integrity sha512-hbhlvpx1gFW3TB5HxJ0mNxyA9Jh5iQt409taOs6zkhpvfJ7YzLs1rsLufJmDsjH5PI1cOyfikY1fE/meyHfU5A== + +vue@^3.2.37: + version "3.2.37" + resolved "https://registry.npmjs.org/vue/-/vue-3.2.37.tgz" + integrity sha512-bOKEZxrm8Eh+fveCqS1/NkG/n6aMidsI6hahas7pa0w/l7jkbssJVsRhVDs07IdDq7h9KHswZOgItnwJAgtVtQ== + dependencies: + "@vue/compiler-dom" "3.2.37" + "@vue/compiler-sfc" "3.2.37" + "@vue/runtime-dom" "3.2.37" + "@vue/server-renderer" "3.2.37" + "@vue/shared" "3.2.37" + +vue@^3.2.40: + version "3.2.41" + resolved "https://registry.npmjs.org/vue/-/vue-3.2.41.tgz" + integrity sha512-uuuvnrDXEeZ9VUPljgHkqB5IaVO8SxhPpqF2eWOukVrBnRBx2THPSGQBnVRt0GrIG1gvCmFXMGbd7FqcT1ixNQ== + dependencies: + "@vue/compiler-dom" "3.2.41" + "@vue/compiler-sfc" "3.2.41" + "@vue/runtime-dom" "3.2.41" + "@vue/server-renderer" "3.2.41" + "@vue/shared" "3.2.41" + +w3c-hr-time@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +w3c-xmlserializer@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz" + integrity sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA== + dependencies: + xml-name-validator "^3.0.0" + +walker@^1.0.7: + version "1.0.8" + resolved "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz" + integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== + dependencies: + makeerror "1.0.12" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +wcwidth@^1.0.0: + version "1.0.1" + resolved "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + +web-vitals@^1.0.1: + version "1.1.2" + resolved "https://registry.npmjs.org/web-vitals/-/web-vitals-1.1.2.tgz" + integrity sha512-PFMKIY+bRSXlMxVAQ+m2aw9c/ioUYfDgrYot0YUa+/xa0sakubWhSDyxAKwzymvXVdF4CZI71g06W+mqhzu6ig== + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webidl-conversions@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz" + integrity sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA== + +webidl-conversions@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz" + integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w== + +webpack-dev-middleware@^5.3.0: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.6.0: + version "4.7.3" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.7.3.tgz" + integrity sha512-mlxq2AsIw2ag016nixkzUkdyOE8ST2GTy34uKSABp1c4nhjZvH90D5ZRR+UOLSsG4Z3TFahAi72a3ymRtfRm+Q== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/serve-index" "^1.9.1" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.2.2" + ansi-html-community "^0.0.8" + bonjour "^3.5.0" + chokidar "^3.5.2" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^1.6.0" + default-gateway "^6.0.3" + del "^6.0.0" + express "^4.17.1" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.0" + ipaddr.js "^2.0.1" + open "^8.0.9" + p-retry "^4.5.0" + portfinder "^1.0.28" + schema-utils "^4.0.0" + selfsigned "^2.0.0" + serve-index "^1.9.1" + sockjs "^0.3.21" + spdy "^4.0.2" + strip-ansi "^7.0.0" + webpack-dev-middleware "^5.3.0" + ws "^8.1.0" + +webpack-manifest-plugin@^4.0.2: + version "4.1.1" + resolved "https://registry.npmjs.org/webpack-manifest-plugin/-/webpack-manifest-plugin-4.1.1.tgz" + integrity sha512-YXUAwxtfKIJIKkhg03MKuiFAD72PlrqCiwdwO4VEXdRO5V0ORCNwaOwAZawPZalCbmH9kBDmXnNeQOw+BIEiow== + dependencies: + tapable "^2.0.0" + webpack-sources "^2.2.0" + +webpack-sources@^1.4.3: + version "1.4.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz" + integrity sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ== + dependencies: + source-list-map "^2.0.0" + source-map "~0.6.1" + +webpack-sources@^2.2.0: + version "2.3.1" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz" + integrity sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA== + dependencies: + source-list-map "^2.0.1" + source-map "^0.6.1" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.64.4: + version "5.76.1" + resolved "https://registry.npmjs.org/webpack/-/webpack-5.76.1.tgz" + integrity sha512-4+YIK4Abzv8172/SGqObnUjaIHjLEuUasz9EwQj/9xmPPkYJy2Mh03Q/lJfSD3YLzbxy5FeTq5Uw0323Oh6SJQ== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +whatwg-encoding@^1.0.5: + version "1.0.5" + resolved "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-fetch@^3.6.2: + version "3.6.2" + resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz" + integrity sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA== + +whatwg-mimetype@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^8.0.0, whatwg-url@^8.4.0, whatwg-url@^8.5.0: + version "8.7.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz" + integrity sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg== + dependencies: + lodash "^4.7.0" + tr46 "^2.1.0" + webidl-conversions "^6.1.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/which/-/which-1.3.1.tgz" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1, which@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +which@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/which/-/which-4.0.0.tgz#cd60b5e74503a3fbcfbf6cd6b4138a8bae644c1a" + integrity sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg== + dependencies: + isexe "^3.1.1" + +why-is-node-running@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/why-is-node-running/-/why-is-node-running-2.2.2.tgz#4185b2b4699117819e7154594271e7e344c9973e" + integrity sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA== + dependencies: + siginfo "^2.0.0" + stackback "0.0.2" + +wide-align@^1.1.0, wide-align@^1.1.2: + version "1.1.5" + resolved "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== + dependencies: + string-width "^4.0.0" + +winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.npmjs.org/winston-transport/-/winston-transport-4.5.0.tgz" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.6.0, winston@^3.7.1: + version "3.7.2" + resolved "https://registry.npmjs.org/winston/-/winston-3.7.2.tgz" + integrity sha512-QziIqtojHBoyzUOdQvQiar1DH0Xp9nF1A1y7NVy2DGEsz82SBDtOalS0ulTRGVT14xPX3WRWkCsdcJKqNflKng== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + +word-wrap@^1.2.3, word-wrap@~1.2.3: + version "1.2.4" + resolved "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz" + integrity sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA== + +wordwrap@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +workbox-background-sync@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-6.4.2.tgz" + integrity sha512-P7c8uG5X2k+DMICH9xeSA9eUlCOjHHYoB42Rq+RtUpuwBxUOflAXR1zdsMWj81LopE4gjKXlTw7BFd1BDAHo7g== + dependencies: + idb "^6.1.4" + workbox-core "6.4.2" + +workbox-broadcast-update@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-6.4.2.tgz" + integrity sha512-qnBwQyE0+PWFFc/n4ISXINE49m44gbEreJUYt2ldGH3+CNrLmJ1egJOOyUqqu9R4Eb7QrXcmB34ClXG7S37LbA== + dependencies: + workbox-core "6.4.2" + +workbox-build@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-build/-/workbox-build-6.4.2.tgz" + integrity sha512-WMdYLhDIsuzViOTXDH+tJ1GijkFp5khSYolnxR/11zmfhNDtuo7jof72xPGFy+KRpsz6tug39RhivCj77qqO0w== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.11.1" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^11.2.1" + "@rollup/plugin-replace" "^2.4.1" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + rollup-plugin-terser "^7.0.0" + source-map "^0.8.0-beta.0" + source-map-url "^0.4.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "6.4.2" + workbox-broadcast-update "6.4.2" + workbox-cacheable-response "6.4.2" + workbox-core "6.4.2" + workbox-expiration "6.4.2" + workbox-google-analytics "6.4.2" + workbox-navigation-preload "6.4.2" + workbox-precaching "6.4.2" + workbox-range-requests "6.4.2" + workbox-recipes "6.4.2" + workbox-routing "6.4.2" + workbox-strategies "6.4.2" + workbox-streams "6.4.2" + workbox-sw "6.4.2" + workbox-window "6.4.2" + +workbox-cacheable-response@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-6.4.2.tgz" + integrity sha512-9FE1W/cKffk1AJzImxgEN0ceWpyz1tqNjZVtA3/LAvYL3AC5SbIkhc7ZCO82WmO9IjTfu8Vut2X/C7ViMSF7TA== + dependencies: + workbox-core "6.4.2" + +workbox-core@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-core/-/workbox-core-6.4.2.tgz" + integrity sha512-1U6cdEYPcajRXiboSlpJx6U7TvhIKbxRRerfepAJu2hniKwJ3DHILjpU/zx3yvzSBCWcNJDoFalf7Vgd7ey/rw== + +workbox-expiration@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-6.4.2.tgz" + integrity sha512-0hbpBj0tDnW+DZOUmwZqntB/8xrXOgO34i7s00Si/VlFJvvpRKg1leXdHHU8ykoSBd6+F2KDcMP3swoCi5guLw== + dependencies: + idb "^6.1.4" + workbox-core "6.4.2" + +workbox-google-analytics@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-6.4.2.tgz" + integrity sha512-u+gxs3jXovPb1oul4CTBOb+T9fS1oZG+ZE6AzS7l40vnyfJV79DaLBvlpEZfXGv3CjMdV1sT/ltdOrKzo7HcGw== + dependencies: + workbox-background-sync "6.4.2" + workbox-core "6.4.2" + workbox-routing "6.4.2" + workbox-strategies "6.4.2" + +workbox-navigation-preload@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-6.4.2.tgz" + integrity sha512-viyejlCtlKsbJCBHwhSBbWc57MwPXvUrc8P7d+87AxBGPU+JuWkT6nvBANgVgFz6FUhCvRC8aYt+B1helo166g== + dependencies: + workbox-core "6.4.2" + +workbox-precaching@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-6.4.2.tgz" + integrity sha512-CZ6uwFN/2wb4noHVlALL7UqPFbLfez/9S2GAzGAb0Sk876ul9ukRKPJJ6gtsxfE2HSTwqwuyNVa6xWyeyJ1XSA== + dependencies: + workbox-core "6.4.2" + workbox-routing "6.4.2" + workbox-strategies "6.4.2" + +workbox-range-requests@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-6.4.2.tgz" + integrity sha512-SowF3z69hr3Po/w7+xarWfzxJX/3Fo0uSG72Zg4g5FWWnHpq2zPvgbWerBZIa81zpJVUdYpMa3akJJsv+LaO1Q== + dependencies: + workbox-core "6.4.2" + +workbox-recipes@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-6.4.2.tgz" + integrity sha512-/oVxlZFpAjFVbY+3PoGEXe8qyvtmqMrTdWhbOfbwokNFtUZ/JCtanDKgwDv9x3AebqGAoJRvQNSru0F4nG+gWA== + dependencies: + workbox-cacheable-response "6.4.2" + workbox-core "6.4.2" + workbox-expiration "6.4.2" + workbox-precaching "6.4.2" + workbox-routing "6.4.2" + workbox-strategies "6.4.2" + +workbox-routing@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-routing/-/workbox-routing-6.4.2.tgz" + integrity sha512-0ss/n9PAcHjTy4Ad7l2puuod4WtsnRYu9BrmHcu6Dk4PgWeJo1t5VnGufPxNtcuyPGQ3OdnMdlmhMJ57sSrrSw== + dependencies: + workbox-core "6.4.2" + +workbox-strategies@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-6.4.2.tgz" + integrity sha512-YXh9E9dZGEO1EiPC3jPe2CbztO5WT8Ruj8wiYZM56XqEJp5YlGTtqRjghV+JovWOqkWdR+amJpV31KPWQUvn1Q== + dependencies: + workbox-core "6.4.2" + +workbox-streams@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-streams/-/workbox-streams-6.4.2.tgz" + integrity sha512-ROEGlZHGVEgpa5bOZefiJEVsi5PsFjJG9Xd+wnDbApsCO9xq9rYFopF+IRq9tChyYzhBnyk2hJxbQVWphz3sog== + dependencies: + workbox-core "6.4.2" + workbox-routing "6.4.2" + +workbox-sw@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-sw/-/workbox-sw-6.4.2.tgz" + integrity sha512-A2qdu9TLktfIM5NE/8+yYwfWu+JgDaCkbo5ikrky2c7r9v2X6DcJ+zSLphNHHLwM/0eVk5XVf1mC5HGhYpMhhg== + +workbox-webpack-plugin@^6.4.1: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-webpack-plugin/-/workbox-webpack-plugin-6.4.2.tgz" + integrity sha512-CiEwM6kaJRkx1cP5xHksn13abTzUqMHiMMlp5Eh/v4wRcedgDTyv6Uo8+Hg9MurRbHDosO5suaPyF9uwVr4/CQ== + dependencies: + fast-json-stable-stringify "^2.1.0" + pretty-bytes "^5.4.1" + source-map-url "^0.4.0" + upath "^1.2.0" + webpack-sources "^1.4.3" + workbox-build "6.4.2" + +workbox-window@6.4.2: + version "6.4.2" + resolved "https://registry.npmjs.org/workbox-window/-/workbox-window-6.4.2.tgz" + integrity sha512-KVyRKmrJg7iB+uym/B/CnEUEFG9CvnTU1Bq5xpXHbtgD9l+ShDekSl1wYpqw/O0JfeeQVOFb8CiNfvnwWwqnWQ== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "6.4.2" + +"wrap-ansi-cjs@npm: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" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@^2.4.2: + version "2.4.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz" + integrity sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: + version "3.0.3" + resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== + dependencies: + imurmurhash "^0.1.4" + is-typedarray "^1.0.0" + signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" + +write-json-file@^3.2.0: + version "3.2.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz" + integrity sha512-3xZqT7Byc2uORAatYiP3DHUUAVEkNOswEWNs9H5KXiicRTvzYzYqKjYc4G7p+8pltvAw641lVByKVtMpf+4sYQ== + dependencies: + detect-indent "^5.0.0" + graceful-fs "^4.1.15" + make-dir "^2.1.0" + pify "^4.0.1" + sort-keys "^2.0.0" + write-file-atomic "^2.4.2" + +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== + dependencies: + sort-keys "^2.0.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" + +ws@^7.4.6: + version "7.5.6" + resolved "https://registry.npmjs.org/ws/-/ws-7.5.6.tgz" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== + +ws@^8.1.0: + version "8.4.2" + resolved "https://registry.npmjs.org/ws/-/ws-8.4.2.tgz" + integrity sha512-Kbk4Nxyq7/ZWqr/tarI9yIt/+iNNFOjBXEWgTb4ydaNHBNGgvf2QHbS9fdfsndfjFlFwEd4Al+mw83YkaD10ZA== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xml-crypto@^3.0.1: + version "3.1.0" + resolved "https://registry.npmjs.org/xml-crypto/-/xml-crypto-3.1.0.tgz" + integrity sha512-GPDprzBeCvn2ByTzeX+DOXbQ7V2IHmE6H1WZkrR+5LPrRQrwwYC9RoCYZ2++y2yJTYzRre1qY4gqNjmJLKdQ6Q== + dependencies: + "@xmldom/xmldom" "0.8.7" + xpath "0.0.32" + +xml-encryption@^3.0.2: + version "3.0.2" + resolved "https://registry.npmjs.org/xml-encryption/-/xml-encryption-3.0.2.tgz" + integrity sha512-VxYXPvsWB01/aqVLd6ZMPWZ+qaj0aIdF+cStrVJMcFj3iymwZeI0ABzB3VqMYv48DkSpRhnrXqTUkR34j+UDyg== + dependencies: + "@xmldom/xmldom" "^0.8.5" + escape-html "^1.0.3" + xpath "0.0.32" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@^0.5.0: + version "0.5.0" + resolved "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz" + integrity sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA== + dependencies: + sax ">=0.6.0" + xmlbuilder "~11.0.0" + +xmlbuilder@8.2.x: + version "8.2.2" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz" + integrity sha512-eKRAFz04jghooy8muekqzo8uCSVNeyRedbuJrp0fovbLIi7wlsYtdUn3vBAAPq2Y3/0xMz2WMEUQ8yhVVO9Stw== + +xmlbuilder@^15.1.1: + version "15.1.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz" + integrity sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg== + +xmlbuilder@~11.0.0: + version "11.0.1" + resolved "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz" + integrity sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA== + +xmlchars@^2.2.0: + version "2.2.0" + resolved "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz" + integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== + +xmlrpc@^1.3.2: + version "1.3.2" + resolved "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz" + integrity sha512-jQf5gbrP6wvzN71fgkcPPkF4bF/Wyovd7Xdff8d6/ihxYmgETQYSuTc+Hl+tsh/jmgPLro/Aro48LMFlIyEKKQ== + dependencies: + sax "1.2.x" + xmlbuilder "8.2.x" + +xpath@0.0.27: + version "0.0.27" + resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.27.tgz" + integrity sha512-fg03WRxtkCV6ohClePNAECYsmpKKTv5L8y/X3Dn1hQrec3POx2jHZ/0P2qQ6HvsrU1BmeqXcof3NGGueG6LxwQ== + +xpath@0.0.32: + version "0.0.32" + resolved "https://registry.npmjs.org/xpath/-/xpath-0.0.32.tgz" + integrity sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw== + +xtend@^4.0.0, xtend@^4.0.2, xtend@~4.0.1: + version "4.0.2" + resolved "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^3.0.0, yallist@^3.1.1: + version "3.1.1" + resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^1.10.0, yaml@^1.10.2, yaml@^1.7.2: + version "1.10.2" + resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz" + integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== + +yargs-parser@20.2.4: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + +yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.9" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +yocto-queue@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" + integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== + +yup@^0.31.0: + version "0.31.1" + resolved "https://registry.npmjs.org/yup/-/yup-0.31.1.tgz" + integrity sha512-Lf6648jDYOWR75IlWkVfwesPyW6oj+50NpxlKvsQlpPsB8eI+ndI7b4S1VrwbmeV9hIZDu1MzrlIL4W+gK1jPw== + dependencies: + "@babel/runtime" "^7.10.5" + lodash "^4.17.20" + lodash-es "^4.17.11" + property-expr "^2.0.4" + toposort "^2.0.2" + +yup@^0.32.11: + version "0.32.11" + resolved "https://registry.npmjs.org/yup/-/yup-0.32.11.tgz" + integrity sha512-Z2Fe1bn+eLstG8DRR6FTavGD+MeAwyfmouhHsIUgaADz8jvFKbO/fXc2trJKZg+5EBjh4gGm3iU/t3onKlXHIg== + dependencies: + "@babel/runtime" "^7.15.4" + "@types/lodash" "^4.14.175" + lodash "^4.17.21" + lodash-es "^4.17.21" + nanoclone "^0.2.1" + property-expr "^2.0.4" + toposort "^2.0.2" + +zen-observable-ts@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.2.5.tgz" + integrity sha512-QZWQekv6iB72Naeake9hS1KxHlotfRpe+WGNbNx5/ta+R3DNjVO2bswf63gXlWDcs+EMd7XY8HfVQyP1X6T4Zg== + dependencies: + zen-observable "0.8.15" + +zen-observable-ts@~1.1.0: + version "1.1.0" + resolved "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-1.1.0.tgz" + integrity sha512-1h4zlLSqI2cRLPJUHJFL8bCWHhkpuXkF+dbGkRaWjgDIG26DmzyshUMrdV/rL3UnR+mhaX4fRq8LPouq0MYYIA== + dependencies: + "@types/zen-observable" "0.8.3" + zen-observable "0.8.15" + +zen-observable@0.8.15: + version "0.8.15" + resolved "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ== + +zustand@^4.4.1: + version "4.5.2" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848" + integrity sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g== + dependencies: + use-sync-external-store "1.2.0"