diff --git a/.commitlintrc.json b/.commitlintrc.json index 7185c74f33d..1e59a32dc9b 100644 --- a/.commitlintrc.json +++ b/.commitlintrc.json @@ -1,10 +1,23 @@ { - "extends": ["@commitlint/config-conventional"], + "extends": [ + "@commitlint/config-conventional" + ], "rules": { + "body-max-line-length": [ + 0, + "always" + ], "subject-case": [ 2, "always", - ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case", "camel-case"] + [ + "sentence-case", + "start-case", + "pascal-case", + "upper-case", + "lower-case", + "camel-case" + ] ], "type-enum": [ 2, diff --git a/.cspell.json b/.cspell.json index 6ad454ad71d..5b4cbf3728f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -567,6 +567,9 @@ "upserted", "upstash", "Upstash", + "usecase", + "USECASE", + "Vonage", "Krakend", "ratelimit", "Ratelimit", @@ -600,19 +603,26 @@ "cpack", "pulumi", "hostedtoolcache", + "OTLP", + "otlp", + "hostedtoolcache", "pyroscope", "HEAY", "Pyroscope", "PYROSCOPE", "usecases", - "hbspt", + "hbspt", "prepopulating", "Vonage", "fieldtype", "usecase", "zulip", "uuidv", - "Vonage" + "Vonage", + "runtimes", + "cafebabe", + "Icann", + "limitbar", ], "flagWords": [], "patterns": [ @@ -675,6 +685,7 @@ "apps/api/src/.env.test", "apps/ws/src/.env.test", "apps/ws/src/.example.env", + "apps/web/playwright.config.ts", ".cspell.json", "package.json", "package-lock.json", @@ -696,6 +707,6 @@ "angular.json", "ng-package.json", "libs/shared/src/types/timezones/timezones.types.ts", - "*.riv" + "*.riv", ] } diff --git a/.eslintrc.js b/.eslintrc.js index eb5537025dd..324b870bf86 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -66,6 +66,7 @@ module.exports = { { patterns: [ '@novu/shared/*', + '!@novu/shared/utils', '@novu/dal/*', '!import2/good', '*../libs/dal/*', diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 85904f46bdf..2cf10e5d3e3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,12 +1,16 @@ -### What change does this PR introduce? +### What changed? Why was the change needed? + - +### Screenshots + -### Why was this change needed? +
+Expand for optional sections - +### Related enterprise PR + -### Other information (Screenshots) +### Special notes for your reviewer + - - +
diff --git a/.github/actions/run-backend/action.yml b/.github/actions/run-backend/action.yml index 398c09440a6..1613f8eb6e0 100644 --- a/.github/actions/run-backend/action.yml +++ b/.github/actions/run-backend/action.yml @@ -13,6 +13,10 @@ inputs: cypress_github_oauth_client_secret: description: 'Cypress GitHub client secret' required: true + ci_ee_test: + description: 'Whether the app should import ee packages for testing' + required: false + default: 'false' runs: using: composite @@ -28,7 +32,7 @@ runs: TZ: "UTC" GITHUB_OAUTH_REDIRECT: "http://127.0.0.1:1336/v1/auth/github/callback" LAUNCH_DARKLY_SDK_KEY: ${{ inputs.launch_darkly_sdk_key }} - CI_EE_TEST: "true" + CI_EE_TEST: ${{ inputs.ci_ee_test }} run: cd apps/api && pnpm start:build & - name: Start Worker @@ -38,7 +42,7 @@ runs: PORT: "1342" TZ: "UTC" LAUNCH_DARKLY_SDK_KEY: ${{ inputs.launch_darkly_sdk_key }} - CI_EE_TEST: "true" + CI_EE_TEST: ${{ inputs.ci_ee_test }} run: cd apps/worker && pnpm start:prod & - name: Wait on API and Worker diff --git a/.github/workflows/pr-manager.yml b/.github/workflows/pr-manager.yml index a88c7dc8166..ef3cfd440df 100644 --- a/.github/workflows/pr-manager.yml +++ b/.github/workflows/pr-manager.yml @@ -11,11 +11,20 @@ jobs: runs-on: ubuntu-latest steps: - name: Process stale PRs - uses: Sonia-corporation/stale@2.5.0 + uses: actions/stale@v9 with: - issue-processing: false - pull-request-days-before-stale: 14 - pull-request-days-before-close: 180 - pull-request-ignore-draft: true - pull-request-ignore-all-milestones: true - pull-request-stale-label: 'stale' + # Get issues in descending (newest first) order. + ascending: false + # After 6 months, mark issue as stale. + days-before-issue-stale: 180 + # Do not auto-close issues marked as stale. + days-before-issue-close: -1 + # After 3 months, mark PR as stale. + days-before-pr-stale: 90 + # Auto-close PRs marked as stale a month later. + days-before-pr-close: 31 + # Delete the branch when closing PRs. GitHub's "restore branch" function works indefinitely, so no reason not to. + delete-branch: true + stale-pr-message: "This PR is being marked as stale due to inactivity." + close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued." + operations-per-run: 100 diff --git a/.github/workflows/reusable-web-e2e.yml b/.github/workflows/reusable-web-e2e.yml index 8ead1159c5f..c96469d8e4b 100644 --- a/.github/workflows/reusable-web-e2e.yml +++ b/.github/workflows/reusable-web-e2e.yml @@ -24,6 +24,10 @@ jobs: # leaving the Dashboard hanging ... # https://github.com/cypress-io/github-action/issues/48 fail-fast: false + matrix: + # run 5 copies of the current job in parallel + containers: [1, 2, 3, 4, 5] + total: [5] # The type of runner that the job will run on runs-on: ubuntu-latest @@ -74,6 +78,7 @@ jobs: cypress_github_oauth_client_id: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_ID }} cypress_github_oauth_client_secret: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_SECRET }} launch_darkly_sdk_key: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }} + ci_ee_test: ${{ steps.setup.outputs.has_token }} - name: Start Client working-directory: apps/web @@ -103,8 +108,7 @@ jobs: - run: | echo "BROWSER_PATH=$(which chrome)" >> $GITHUB_ENV - - name: Cypress run EE e2e - if: ${{ inputs.ee }} + - name: Cypress run e2e uses: cypress-io/github-action@v6 env: NODE_ENV: test @@ -116,35 +120,64 @@ jobs: with: working-directory: apps/web browser: "${{ env.BROWSER_PATH }}" - record: false + record: true + parallel: true install: false - parallel: false config-file: cypress.config.ts - spec: "cypress/tests/**/*.spec-ee.{js,jsx,ts,tsx}" + spec: | + cypress/tests/**/*.spec.ts + ${{ steps.setup.outputs.has_token == 'true' && inputs.ee && 'cypress/tests/**/*.spec-ee.ts' }} - - name: Cypress run e2e - uses: cypress-io/github-action@v6 - env: - NODE_ENV: test - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_WEB_KEY }} - CYPRESS_GITHUB_USER_EMAIL: ${{ secrets.CYPRESS_GITHUB_USER_EMAIL }} - CYPRESS_GITHUB_USER_PASSWORD: ${{ secrets.CYPRESS_GITHUB_USER_PASSWORD }} - CYPRESS_IS_CI: true + - name: Playwright Install + working-directory: apps/web + run: pnpm playwright:install + + - name: Run Playwright tests + working-directory: apps/web + run: pnpm playwright:test --shard=${{ matrix.containers }}/${{ matrix.total }} + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} with: - working-directory: apps/web - browser: "${{ env.BROWSER_PATH }}" - record: false - parallel: false - install: false - config-file: cypress.config.ts - spec: "cypress/tests/**/*.spec.{js,jsx,ts,tsx}" + name: blob-report-${{ matrix.containers }} + path: apps/web/blob-report + retention-days: 1 - - uses: actions/upload-artifact@v3 - if: failure() + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} with: - name: cypress-screenshots + name: cypress-screenshots-${{ matrix.containers }} path: apps/web/cypress/screenshots + retention-days: 30 + + merge-reports: + # Merge reports after playwright-tests, even if some shards have failed + if: ${{ !cancelled() }} + needs: [e2e_web] + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.8.1 + + - name: Download blob reports from GitHub Actions Artifacts + uses: actions/download-artifact@v4 + with: + path: all-blob-reports + pattern: blob-report-* + merge-multiple: true + + - name: Merge into HTML Report + run: npx playwright merge-reports --reporter html ./all-blob-reports + + - name: Upload HTML report + uses: actions/upload-artifact@v4 + with: + name: html-report--attempt-${{ github.run_attempt }} + path: playwright-report + retention-days: 14 component_web: if: "!contains(github.event.head_commit.message, 'ci skip')" diff --git a/.github/workflows/reusable-widget-e2e.yml b/.github/workflows/reusable-widget-e2e.yml index 38afd733f39..a8d4578eb06 100644 --- a/.github/workflows/reusable-widget-e2e.yml +++ b/.github/workflows/reusable-widget-e2e.yml @@ -68,6 +68,7 @@ jobs: cypress_github_oauth_client_id: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_ID }} cypress_github_oauth_client_secret: ${{ secrets.CYPRESS_GITHUB_OAUTH_CLIENT_SECRET }} launch_darkly_sdk_key: ${{ secrets.LAUNCH_DARKLY_SDK_KEY }} + ci_ee_test: ${{ steps.setup.outputs.has_token }} # Runs a single command using the runners shell - name: Start Client diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 86a5862f34c..1bfa4f329a9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -108,30 +108,6 @@ jobs: if: ${{ contains(fromJson(needs.get-affected.outputs.test-cypress), '@novu/widget') || contains(fromJson(needs.get-affected.outputs.test-unit), '@novu/notification-center') || contains(fromJson(needs.get-affected.outputs.test-unit), '@novu/ws') }} secrets: inherit - build_docker_api: - name: Build Docker API - runs-on: ubuntu-latest - timeout-minutes: 80 - needs: [get-affected] - if: ${{ contains(fromJson(needs.get-affected.outputs.test-e2e), '@novu/api') }} - permissions: - contents: read - packages: write - deployments: write - id-token: write - strategy: - matrix: - name: ['novu/api', 'novu/api-ee'] - steps: - - uses: actions/checkout@v3 - - uses: ./.github/actions/setup-project - - uses: ./.github/actions/setup-redis-cluster - - uses: ./.github/actions/docker/build-api - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - fork: ${{ github.event.pull_request.head.repo.fork }} - docker_name: ${{ matrix.name }} - test_providers: name: Unit Test Providers runs-on: ubuntu-latest diff --git a/.source b/.source index f619061f8bf..3e53a1769dd 160000 --- a/.source +++ b/.source @@ -1 +1 @@ -Subproject commit f619061f8bfb97a06b726ba80f5838ccc2c02bfb +Subproject commit 3e53a1769ddf90743a73e1731beb26e40a0a75a4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index befcd559119..7097333e25c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -56,10 +56,10 @@ You can open a new issue with this [issue form](https://github.com/novuhq/novu/i The project is a monorepo, meaning that it is a collection of multiple packages managed in the same repository. -To learn more about the project structure and running the project locally, please have a look [here](https://docs.novu.co/community-support/introduction#run-novu-locally). +To learn more about the project structure and running the project locally, please have a look [here](https://docs.novu.co/community-support/introduction#run-novu-locally?utm_campaign=github-contrib). After cloning your fork, you will need to run the `npm run setup:project` command to install and build all dependencies. -To learn a detailed guide on running the project locally, checkout our guide on [how to run novu in local machine](https://docs.novu.co/community/run-in-local-machine). +To learn a detailed guide on running the project locally, checkout our guide on [how to run novu in local machine](https://docs.novu.co/community/run-in-local-machine?utm_campaign=github-contrib). ## Missing a Feature? @@ -86,8 +86,8 @@ Questions, suggestions, and thoughts are most welcome. Feel free to open a [GitH - Help create tutorials and blog posts - Request a feature by submitting a proposal - Report a bug -- **Improve documentation** - fix incomplete or missing [docs](https://docs.novu.co/), bad wording, examples or explanations. +- **Improve documentation** - fix incomplete or missing [docs](https://docs.novu.co/?utm_campaign=github-contrib), bad wording, examples or explanations. ## Missing a provider? -If you are in need of a provider we do not yet have, you can request a new one by [submitting an issue](#submitting-an-issue). Or you can build a new one by following our [create a provider guide](https://docs.novu.co/community/add-a-new-provider). +If you are in need of a provider we do not yet have, you can request a new one by [submitting an issue](#submitting-an-issue). Or you can build a new one by following our [create a provider guide](https://docs.novu.co/community/add-a-new-provider?utm_campaign=github-contrib). diff --git a/README.md b/README.md index ff02f18a536..6703ab98ee1 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,7 @@ npx novu init After setting up your account using the cloud or docker version, you can trigger the API using the `@novu/node` package. -For API documentation and reference, please visit [Novu API Reference] (https://docs.novu.co/api-reference/events/trigger-event). +For API documentation and reference, please visit [Novu API Reference] (https://docs.novu.co/api-reference/events/trigger-event?utm_campaign=github-readme). To get started with the Node.js package, you can install it using npm: @@ -148,7 +148,7 @@ Create notification workflows right from your IDE and integrate with MJML/React - Define workflow and step validations with Zod or JSON Schema - Modify content and behavior via Web management input panel -[Request Early Access](https://novu.co/novu-echo-coming-soon/?utm_campaign=echo_github) +[Request Early Access](https://novu.co/novu-echo-coming-soon/?utm_campaign=github-readme) ```ts @@ -202,17 +202,17 @@ client.workflow('comment-on-post', async ({step, subscriber}) => { ## Embeddable Notification Center -Using the Novu API and admin panel, you can easily add a real-time notification center to your web app without building it yourself. You can use our [React](https://docs.novu.co/notification-center/client/react/get-started) / [Vue](https://docs.novu.co/notification-center/client/vue) / [Angular](https://docs.novu.co/notification-center/client/angular) components or an [iframe embed](https://docs.novu.co/notification-center/client/iframe), as well as a [Web component](https://docs.novu.co/notification-center/client/web-component). +Using the Novu API and admin panel, you can easily add a real-time notification center to your web app without building it yourself. You can use our [React](https://docs.novu.co/notification-center/client/react/get-started?utm_campaign=github-readme) / [Vue](https://docs.novu.co/notification-center/client/vue?utm_campaign=github-readme) / [Angular](https://docs.novu.co/notification-center/client/angular?utm_campaign=github-readme) components or an [iframe embed](https://docs.novu.co/notification-center/client/iframe?utm_campaign=github-readme), as well as a [Web component](https://docs.novu.co/notification-center/client/web-component?utm_campaign=github-readme).
notification-center-912bb96e009fb3a69bafec23bcde00b0 -Read more about how to add a notification center to your app with the Novu API [here](https://docs.novu.co/notification-center/getting-started) +Read more about how to add a notification center to your app with the Novu API [here](https://docs.novu.co/notification-center/getting-started?utm_campaign=github-readme)

- React Component - · Vue Component - · Angular Component + React Component + · Vue Component + · Angular Component

@@ -277,7 +277,7 @@ Novu provides a single API to manage providers across multiple channels with a s #### 📱 In-App -- [x] [Novu](https://docs.novu.co/notification-center/getting-started) +- [x] [Novu](https://docs.novu.co/notification-center/getting-started?utm_campaign=github-readme) - [ ] MagicBell #### Other (Coming Soon...) @@ -298,9 +298,9 @@ We are more than happy to help you. If you are getting any errors or facing prob ## 🔗 Links -- [Home page](https://novu.co?utm_source=github) +- [Home page](https://novu.co?utm_campaign=github-readme) - [Contribution Guidelines](https://github.com/novuhq/novu/blob/main/CONTRIBUTING.md) -- [Run Novu Locally](https://docs.novu.co/community/run-in-local-machine) +- [Run Novu Locally](https://docs.novu.co/community/run-in-local-machine?utm_campaign=github-readme) ## 🛡️ License diff --git a/apps/api/README.md b/apps/api/README.md index cc9d7b27658..5417b826f49 100644 --- a/apps/api/README.md +++ b/apps/api/README.md @@ -27,7 +27,7 @@ The command will return warnings and errors that must be fixed before the Github ## Running the API -See the docs for [Run in Local Machine](https://docs.novu.co/community/run-in-local-machine) to get setup. Then run: +See the docs for [Run in Local Machine](https://docs.novu.co/community/run-in-local-machine?utm_campaign=github-api-readme) to get setup. Then run: ```bash # Run the API in watch mode @@ -43,7 +43,7 @@ $ npm run test ``` ### E2E tests -See the docs for [Running on Local Machine - API Tests](https://docs.novu.co/community/run-in-local-machine#api). +See the docs for [Running on Local Machine - API Tests](https://docs.novu.co/community/run-in-local-machine#api?utm_campaign=github-api-readme). ## Migrations Database migrations are included for features that have a hard dependency on specific data being available on database entities. These migrations are run by both Novu Cloud and Novu Self-Hosted users to support new feature releases. diff --git a/apps/api/jarvis-api-intro.md b/apps/api/jarvis-api-intro.md index be820f9e590..f5ef84776ef 100644 --- a/apps/api/jarvis-api-intro.md +++ b/apps/api/jarvis-api-intro.md @@ -11,7 +11,7 @@ This issue was tagged as related to `@novu/api` and the related code is located If that's the first time you want to contribute to Novu here are a few simple steps to get you started: 1. Fork the repository and clone your fork to your local machine. 2. Install the dependencies using `npm run setup:project`. - 3. Create a new branch with the number of the issue, for example: `1454-fix-something-cool` and start contributing based on the [Contributing Guide](https://docs.novu.co/community/run-in-local-machine) or the short guide in the section below. + 3. Create a new branch with the number of the issue, for example: `1454-fix-something-cool` and start contributing based on the [Contributing Guide](https://docs.novu.co/community/run-in-local-machine?utm_campaign=github-jarvis) or the short guide in the section below. 4. Create a Pull request and follow the template of creation diff --git a/apps/api/migrations/encrypt-api-keys/encrypt-api-keys-migration.ts b/apps/api/migrations/encrypt-api-keys/encrypt-api-keys-migration.ts index 0efcf935df3..d91ed5924a6 100644 --- a/apps/api/migrations/encrypt-api-keys/encrypt-api-keys-migration.ts +++ b/apps/api/migrations/encrypt-api-keys/encrypt-api-keys-migration.ts @@ -1,13 +1,22 @@ +/* eslint-disable */ +import '../../src/config'; + import { EnvironmentRepository, IApiKey } from '@novu/dal'; import { encryptSecret } from '@novu/application-generic'; import { EncryptedSecret } from '@novu/shared'; import { createHash } from 'crypto'; +import { NestFactory } from '@nestjs/core'; + +import { AppModule } from '../../src/app.module'; export async function encryptApiKeysMigration() { // eslint-disable-next-line no-console console.log('start migration - encrypt api keys'); - const environmentRepository = new EnvironmentRepository(); + const app = await NestFactory.create(AppModule, { + logger: false, + }); + const environmentRepository = app.get(EnvironmentRepository); const environments = await environmentRepository.find({}); for (const environment of environments) { @@ -68,3 +77,5 @@ export interface IEncryptedApiKey { _userId: string; hash: string; } + +encryptApiKeysMigration(); diff --git a/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.spec.ts b/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.spec.ts new file mode 100644 index 00000000000..e15bee7f094 --- /dev/null +++ b/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.spec.ts @@ -0,0 +1,467 @@ +import { SubscriberRepository } from '@novu/dal'; +import { SubscribersService, UserSession } from '@novu/testing'; + +import { removeDuplicatedSubscribers } from './remove-duplicated-subscribers.migration'; +import { expect } from 'chai'; +import { ChatProviderIdEnum, IChannelSettings, ISubscriber } from '@novu/shared'; + +describe('Migration: Remove Duplicated Subscribers', () => { + let session: UserSession; + let subscriberService: SubscribersService; + const subscriberRepository = new SubscriberRepository(); + + beforeEach(async () => { + session = new UserSession(); + await session.initialize(); + subscriberService = new SubscribersService(session.organization._id, session.environment._id); + }); + + it('should remove duplicated subscribers', async () => { + const duplicatedSubscriberId = '123'; + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_subscriber', + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'mid_subscriber', + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'last_subscriber', + _environmentId: session.environment._id, + _organizationId: session.organization._id, + }); + + const duplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + + expect(duplicates.length).to.equal(3); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0].firstName).to.equal('last_subscriber'); + }); + + it('should always keep one subscriber per environment', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'env_1', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'env_1', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + const secondEnvironmentId = session.organization._id; + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'env_2', + _environmentId: secondEnvironmentId, + _organizationId: session.organization._id, + }); + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'env_2', + _environmentId: secondEnvironmentId, + _organizationId: session.organization._id, + }); + + const duplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(duplicates.length).to.equal(2); + + const duplicates2 = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(duplicates2.length).to.equal(2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0].firstName).to.equal('env_1'); + + const remainingDuplicates2 = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: secondEnvironmentId, + }); + expect(remainingDuplicates2.length).to.equal(1); + expect(remainingDuplicates2[0].firstName).to.equal('env_2'); + }); + + it('should merge the metadata across duplicated subscribers', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + lastName: 'last_name', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + const duplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(duplicates.length).to.equal(2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0].firstName).to.equal('first_name'); + expect(remainingDuplicates[0].lastName).to.equal('last_name'); + }); + + it('should merge the metadata across duplicated subscribers by latest created subscriber', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + const firstCreatedSubscriber = await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + email: 'email_1', + phone: 'phone_1', + avatar: 'avatar_1', + locale: 'locale_1', + data: { key: 'value_1' }, + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_2', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + email: 'email_3', + phone: 'phone_3', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + avatar: 'avatar_4', + data: { newStuff: 'value_4' }, + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_5', + locale: 'locale_5', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + const duplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(duplicates.length).to.equal(5); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0]._id).to.equal(firstCreatedSubscriber._id); + expect(remainingDuplicates[0]._organizationId).to.equal(firstCreatedSubscriber._organizationId); + expect(remainingDuplicates[0]._environmentId).to.equal(firstCreatedSubscriber._environmentId); + expect(remainingDuplicates[0].__v).to.equal(firstCreatedSubscriber.__v); + + expect(remainingDuplicates[0].firstName).to.equal('first_name_5'); + expect(remainingDuplicates[0].lastName).to.equal('last_name_1'); + expect(remainingDuplicates[0].email).to.equal('email_3'); + expect(remainingDuplicates[0].phone).to.equal('phone_3'); + expect(remainingDuplicates[0].avatar).to.equal('avatar_4'); + expect(remainingDuplicates[0].locale).to.equal('locale_5'); + expect(remainingDuplicates[0].data?.key).to.be.undefined; + expect(remainingDuplicates[0].data?.newStuff).to.equal('value_4'); + }); + + it('should merge 2 channel integration', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + const subscriber1: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [{ _integrationId: '1', providerId: ChatProviderIdEnum.Slack, credentials: { webhookUrl: 'url_1' } }], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + const firstCreatedSubscriber = await subscriberRepository.create(subscriber1); + + const subscriber2: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [ + { + _integrationId: '2', + providerId: ChatProviderIdEnum.Discord, + credentials: { deviceTokens: ['token_123', 'token_123'] }, + }, + ], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + await subscriberRepository.create(subscriber2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0]._id).to.equal(firstCreatedSubscriber._id); + expect(remainingDuplicates[0]._organizationId).to.equal(firstCreatedSubscriber._organizationId); + expect(remainingDuplicates[0]._environmentId).to.equal(firstCreatedSubscriber._environmentId); + expect(remainingDuplicates[0].email).to.equal('email_1'); + expect(remainingDuplicates[0].firstName).to.equal('first_name_1'); + expect(remainingDuplicates[0].lastName).to.equal('last_name_1'); + expect(remainingDuplicates[0].createdAt).to.equal('2021-01-01T00:00:00.000Z'); + + const firstChannel: IChannelSettings | undefined = remainingDuplicates[0].channels?.find( + (channel) => channel._integrationId === '1' + ); + expect(firstChannel?._integrationId).to.equal('1'); + expect(firstChannel?.providerId).to.equal(ChatProviderIdEnum.Slack); + expect(firstChannel?.credentials.webhookUrl).to.equal('url_1'); + + const secondChannel: IChannelSettings | undefined = remainingDuplicates[0].channels?.find( + (channel) => channel._integrationId === '2' + ); + expect(secondChannel?._integrationId).to.equal('2'); + expect(secondChannel?.providerId).to.equal(ChatProviderIdEnum.Discord); + expect(secondChannel?.credentials.deviceTokens).to.deep.equal(['token_123']); + }); + + it('should merge 2 channel same integration', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + const subscriber1: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [ + { + _integrationId: '1', + providerId: ChatProviderIdEnum.Discord, + credentials: { deviceTokens: ['token_1', 'token_2'] }, + }, + ], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + const firstCreatedSubscriber = await subscriberRepository.create(subscriber1); + + const subscriber2: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [ + { + _integrationId: '1', + providerId: ChatProviderIdEnum.Discord, + credentials: { deviceTokens: ['token_2', 'token_3', 'token_3'] }, + }, + ], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + await subscriberRepository.create(subscriber2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0]._id).to.equal(firstCreatedSubscriber._id); + expect(remainingDuplicates[0]._organizationId).to.equal(firstCreatedSubscriber._organizationId); + expect(remainingDuplicates[0]._environmentId).to.equal(firstCreatedSubscriber._environmentId); + expect(remainingDuplicates[0].email).to.equal('email_1'); + expect(remainingDuplicates[0].firstName).to.equal('first_name_1'); + expect(remainingDuplicates[0].lastName).to.equal('last_name_1'); + expect(remainingDuplicates[0].createdAt).to.equal('2021-01-01T00:00:00.000Z'); + + const firstChannel: IChannelSettings | undefined = remainingDuplicates[0].channels?.find( + (channel) => channel._integrationId === '1' + ); + expect(firstChannel?._integrationId).to.equal('1'); + expect(firstChannel?.providerId).to.equal(ChatProviderIdEnum.Discord); + expect(firstChannel?.credentials.deviceTokens).to.deep.equal(['token_1', 'token_2', 'token_3']); + }); + + it('should merge 2 channel same integration', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + const subscriber1: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [ + { + _integrationId: '1', + providerId: ChatProviderIdEnum.Slack, + credentials: { webhookUrl: 'old_url_1' }, + }, + ], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + const firstCreatedSubscriber = await subscriberRepository.create(subscriber1); + + const subscriber2: ISubscriber = { + email: 'email_1', + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_1', + lastName: 'last_name_1', + channels: [ + { + _integrationId: '1', + providerId: ChatProviderIdEnum.Slack, + credentials: { webhookUrl: 'new_url_1' }, + }, + ], + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + deleted: false, + createdAt: '2021-01-01T00:00:00.000Z', + updatedAt: '2021-01-01T00:00:00.000Z', + }; + await subscriberRepository.create(subscriber2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0]._id).to.equal(firstCreatedSubscriber._id); + expect(remainingDuplicates[0]._organizationId).to.equal(firstCreatedSubscriber._organizationId); + expect(remainingDuplicates[0]._environmentId).to.equal(firstCreatedSubscriber._environmentId); + expect(remainingDuplicates[0].email).to.equal('email_1'); + expect(remainingDuplicates[0].firstName).to.equal('first_name_1'); + expect(remainingDuplicates[0].lastName).to.equal('last_name_1'); + expect(remainingDuplicates[0].createdAt).to.equal('2021-01-01T00:00:00.000Z'); + + const firstChannel: IChannelSettings | undefined = remainingDuplicates[0].channels?.find( + (channel) => channel._integrationId === '1' + ); + expect(firstChannel?._integrationId).to.equal('1'); + expect(firstChannel?.providerId).to.equal(ChatProviderIdEnum.Slack); + expect(firstChannel?.credentials.webhookUrl).to.be.equal('new_url_1'); + }); + + it('should keep the first created subscriber after merge', async () => { + const duplicatedSubscriberId = '123'; + const firstEnvironmentId = session.environment._id; + + const firstCreatedSubscriber = await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + await subscriberRepository.create({ + subscriberId: duplicatedSubscriberId, + firstName: 'first_name_2', + _environmentId: firstEnvironmentId, + _organizationId: session.organization._id, + }); + + const duplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + expect(duplicates.length).to.equal(2); + + await removeDuplicatedSubscribers(); + + const remainingDuplicates = await subscriberRepository.find({ + subscriberId: duplicatedSubscriberId, + _environmentId: session.environment._id, + }); + + expect(remainingDuplicates.length).to.equal(1); + expect(remainingDuplicates[0]._id).to.equal(firstCreatedSubscriber._id); + }); +}); diff --git a/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.ts b/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.ts new file mode 100644 index 00000000000..67d503fda0b --- /dev/null +++ b/apps/api/migrations/subscribers/remove-duplicated-subscribers/remove-duplicated-subscribers.migration.ts @@ -0,0 +1,193 @@ +import '../../../src/config'; +import { NestFactory } from '@nestjs/core'; +import { SubscriberRepository } from '@novu/dal'; +import { AppModule } from '../../../src/app.module'; +import { IChannelSettings, ISubscriber } from '@novu/shared'; + +export async function removeDuplicatedSubscribers() { + // eslint-disable-next-line no-console + console.log('start migration - remove duplicated subscribers'); + + const app = await NestFactory.create(AppModule, { + logger: false, + }); + + const batchSize = 1000; + const subscriberRepository = app.get(SubscriberRepository); + + const pipeline = [ + // Group by subscriberId and _environmentId + { + $group: { + _id: { subscriberId: '$subscriberId', environmentId: '$_environmentId' }, + count: { $sum: 1 }, + subscribers: { $push: '$$ROOT' }, // Store all documents of each group + }, + }, + // Filter groups having more than one document (duplicates) + { + $match: { + count: { $gt: 1 }, + }, + }, + ]; + + const cursor = await subscriberRepository._model.aggregate(pipeline, { + batchSize: batchSize, + readPreference: 'secondaryPreferred', + allowDiskUse: true, + }); + + for (const group of cursor) { + const { subscriberId, environmentId } = group._id; + const subscribers = group.subscribers; + + if (subscribers.length <= 1) { + continue; + } + + // sort oldest subscriber first + const sortedSubscribers = subscribers.sort((a, b) => a.updatedAt - b.updatedAt); + const mergedSubscriber = mergeSubscribers(sortedSubscribers); + const subscribersToRemove = sortedSubscribers.filter((subscriber) => subscriber._id !== mergedSubscriber._id); + + // eslint-disable-next-line no-console + console.log( + 'Merged subscriber:', + mergedSubscriber._id.toString(), + 'subscriberId:', + subscriberId, + 'environmentId:', + environmentId.toString() + ); + + try { + await subscriberRepository.update( + { + _id: mergedSubscriber._id, + subscriberId: subscriberId, + _environmentId: environmentId, + }, + { + $set: mergedSubscriber, + } + ); + + // eslint-disable-next-line no-console + console.log( + 'Remaining subscriber updated with merged data for subscriberId:', + subscriberId, + 'subscriberId:', + mergedSubscriber._id.toString(), + 'environmentId:', + environmentId.toString() + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error updating remaining subscribers:', err); + } + + try { + // Delete all duplicates except the merged one + await subscriberRepository.deleteMany({ + _id: { $in: subscribersToRemove.map((subscriber) => subscriber._id) }, + subscriberId: subscriberId, + _environmentId: environmentId, + }); + // eslint-disable-next-line no-console + console.log( + 'Duplicates deleted for subscriberId:', + subscriberId, + 'environmentId:', + environmentId.toString(), + 'ids:', + subscribersToRemove.map((subscriber) => subscriber._id).join() + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Error deleting duplicates:', err); + } + } + + // eslint-disable-next-line no-console + console.log('end migration - remove duplicated subscribers'); + + app.close(); +} + +// Function to merge subscriber information +function mergeSubscribers(subscribers) { + const mergedSubscriber = { ...subscribers[0] }; // Start with the first subscriber + + // Initialize a map to store merged channels + const mergedChannelsMap = new Map(); + + // Merge information from other subscribers + for (const subscriber of subscribers) { + const currentSubscriber = subscriber; + for (const key in currentSubscriber) { + // Skip internal and irrelevant fields + if ( + [ + '_id', + '_organizationId', + '_environmentId', + 'deleted', + 'createdAt', + 'updatedAt', + '__v', + 'isOnline', + 'lastOnlineAt', + ].includes(key) + ) { + continue; + } + + // Update with non-null/undefined values from subsequent subscribers + if (currentSubscriber[key] !== null && currentSubscriber[key] !== undefined) { + if (key === 'channels') { + mergeSubscriberChannels(currentSubscriber, mergedChannelsMap); + } else { + // For other keys, update directly + mergedSubscriber[key] = currentSubscriber[key]; + } + } + } + } + + // Convert merged channels map back to array + mergedSubscriber.channels = [...mergedChannelsMap.values()]; + + return mergedSubscriber; +} + +function mergeChannels(existingChannel: IChannelSettings, newChannel: IChannelSettings) { + const result = { ...existingChannel }; + + // Merge deviceTokens + const allTokens = [ + ...(existingChannel?.credentials?.deviceTokens || []), + ...(newChannel?.credentials?.deviceTokens || []), + ]; + result.credentials.deviceTokens = [...new Set(allTokens)]; + + if (newChannel.credentials.webhookUrl) { + existingChannel.credentials.webhookUrl = newChannel.credentials.webhookUrl; + } + + return existingChannel; +} + +function mergeSubscriberChannels(subscriber: ISubscriber, mergedChannelsMap) { + for (const channel of subscriber.channels || []) { + const integrationId = channel._integrationId; + if (!mergedChannelsMap.has(integrationId)) { + // merging the same channel as a workaround just to make sure we always remove token duplications + mergedChannelsMap.set(integrationId, mergeChannels(channel, channel)); + } else { + // If the integration ID exists, merge device tokens + const existingChannel = mergedChannelsMap.get(integrationId); + mergedChannelsMap.set(integrationId, mergeChannels(existingChannel, channel)); + } + } +} diff --git a/apps/api/package.json b/apps/api/package.json index 9b700d5b105..5d5f995a608 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "@novu/api", - "version": "0.24.0", + "version": "0.24.1", "description": "description", "author": "", "private": "true", @@ -41,12 +41,12 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/terminus": "^10.0.1", "@nestjs/throttler": "^5.0.1", - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/node": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/node": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/testing": "^0.24.1", "@sendgrid/mail": "^8.1.0", "@sentry/hub": "^7.40.0", "@sentry/node": "^7.40.0", @@ -117,10 +117,10 @@ "typescript": "4.9.5" }, "optionalDependencies": { - "@novu/ee-chimera": "^0.24.0", - "@novu/ee-auth": "^0.24.0", - "@novu/ee-billing": "^0.24.0", - "@novu/ee-translation": "^0.24.0" + "@novu/ee-chimera": "^0.24.1", + "@novu/ee-auth": "^0.24.1", + "@novu/ee-billing": "^0.24.1", + "@novu/ee-translation": "^0.24.1" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index cf8dbe81abf..05a3c93a914 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -36,6 +36,7 @@ import { IdempotencyInterceptor } from './app/shared/framework/idempotency.inter import { WorkflowOverridesModule } from './app/workflow-overrides/workflow-overrides.module'; import { ApiRateLimitInterceptor } from './app/rate-limiting/guards'; import { RateLimitingModule } from './app/rate-limiting/rate-limiting.module'; +import { ProductFeatureInterceptor } from './app/shared/interceptors/product-feature.interceptor'; const enterpriseImports = (): Array | ForwardReference> => { const modules: Array | ForwardReference> = []; @@ -90,8 +91,8 @@ const baseModules: Array | Forward TenantModule, WorkflowOverridesModule, RateLimitingModule, - TracingModule.register(packageJson.name), ProfilingModule.register(packageJson.name), + TracingModule.register(packageJson.name, packageJson.version), ]; const enterpriseModules = enterpriseImports(); @@ -99,6 +100,10 @@ const enterpriseModules = enterpriseImports(); const modules = baseModules.concat(enterpriseModules); const providers: Provider[] = [ + { + provide: APP_INTERCEPTOR, + useClass: ProductFeatureInterceptor, + }, { provide: APP_INTERCEPTOR, useClass: ApiRateLimitInterceptor, diff --git a/apps/api/src/app/auth/e2e/login.e2e.ts b/apps/api/src/app/auth/e2e/login.e2e.ts index 68a8a884cb6..b7e8a9722f5 100644 --- a/apps/api/src/app/auth/e2e/login.e2e.ts +++ b/apps/api/src/app/auth/e2e/login.e2e.ts @@ -13,154 +13,179 @@ describe('User login - /auth/login (POST)', async () => { password: '123Qwerty@', }; - before(async () => { - session = new UserSession(); - await session.initialize(); + context('with email/password', async () => { + before(async () => { + session = new UserSession(); + await session.initialize(); + + const { body } = await session.testAgent + .post('/v1/auth/register') + .send({ + email: userCredentials.email, + password: userCredentials.password, + firstName: 'Test', + lastName: 'User', + }) + .expect(201); + }); - const { body } = await session.testAgent - .post('/v1/auth/register') - .send({ + it('should login the user correctly', async () => { + const { body } = await session.testAgent.post('/v1/auth/login').send({ email: userCredentials.email, password: userCredentials.password, - firstName: 'Test', - lastName: 'User', - }) - .expect(201); - }); + }); - it('should login the user correctly', async () => { - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: userCredentials.password, + const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; + + expect(jwtContent.firstName).to.equal('test'); + expect(jwtContent.lastName).to.equal('user'); + expect(jwtContent.email).to.equal('testytest22@gmail.com'); }); - const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; + it('should login the user correctly with uppercase email', async () => { + const { body } = await session.testAgent.post('/v1/auth/login').send({ + email: userCredentials.email.toUpperCase(), + password: userCredentials.password, + }); - expect(jwtContent.firstName).to.equal('test'); - expect(jwtContent.lastName).to.equal('user'); - expect(jwtContent.email).to.equal('testytest22@gmail.com'); - }); + const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; - it('should login the user correctly with uppercase email', async () => { - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email.toUpperCase(), - password: userCredentials.password, + expect(jwtContent.firstName).to.equal('test'); + expect(jwtContent.lastName).to.equal('user'); + expect(jwtContent.email).to.equal('testytest22@gmail.com'); }); - const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; - - expect(jwtContent.firstName).to.equal('test'); - expect(jwtContent.lastName).to.equal('user'); - expect(jwtContent.email).to.equal('testytest22@gmail.com'); - }); + it('should throw error on trying to login non-existing user', async () => { + const { body } = await session.testAgent.post('/v1/auth/login').send({ + email: 'nonExistingUser@email.com', + password: '123123213123', + }); - it('should throw error on trying to login non-existing user', async () => { - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: 'nonExistingUser@email.com', - password: '123123213123', + expect(body.statusCode).to.equal(401); + expect(body.message).to.contain('Incorrect email or password provided.'); }); - expect(body.statusCode).to.equal(401); - expect(body.message).to.contain('Incorrect email or password provided.'); - }); + it('should fail on bad password', async () => { + const { body } = await session.testAgent.post('/v1/auth/login').send({ + email: userCredentials.email, + password: '123123213123', + }); - it('should fail on bad password', async () => { - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: '123123213123', + expect(body.statusCode).to.equal(401); + expect(body.message).to.contain('Incorrect email or password provided.'); }); - expect(body.statusCode).to.equal(401); - expect(body.message).to.contain('Incorrect email or password provided.'); - }); + it('should allow user to log in and reset the failed attempts counter after less than 5 failed attempts within 5 minutes', async () => { + const SAFE_FAILED_LOGIN_ATTEMPTS = 3; - it('should allow user to log in and reset the failed attempts counter after less than 5 failed attempts within 5 minutes', async () => { - const SAFE_FAILED_LOGIN_ATTEMPTS = 3; + for (let i = 0; i < SAFE_FAILED_LOGIN_ATTEMPTS; i++) { + await session.testAgent.post('/v1/auth/login').send({ + email: userCredentials.email, + password: 'wrong-password', + }); + } - for (let i = 0; i < SAFE_FAILED_LOGIN_ATTEMPTS; i++) { - await session.testAgent.post('/v1/auth/login').send({ + const { body } = await session.testAgent.post('/v1/auth/login').send({ email: userCredentials.email, - password: 'wrong-password', + password: userCredentials.password, }); - } - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: userCredentials.password, - }); + const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; - const jwtContent = (await jwt.decode(body.data.token)) as IJwtPayload; + expect(jwtContent.firstName).to.equal('test'); + expect(jwtContent.lastName).to.equal('user'); + expect(jwtContent.email).to.equal('testytest22@gmail.com'); - expect(jwtContent.firstName).to.equal('test'); - expect(jwtContent.lastName).to.equal('user'); - expect(jwtContent.email).to.equal('testytest22@gmail.com'); + const { body: wrongCredsBody } = await session.testAgent.post('/v1/auth/login').send({ + email: userCredentials.email, + password: 'wrong-password', + }); - const { body: wrongCredsBody } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: 'wrong-password', + expect(wrongCredsBody.statusCode).to.equal(401); + expect(wrongCredsBody.message).to.contain('Incorrect email or password provided.'); }); - expect(wrongCredsBody.statusCode).to.equal(401); - expect(wrongCredsBody.message).to.contain('Incorrect email or password provided.'); - }); + it('should block the user account after 5 unsuccessful attempts within 5 minutes', async () => { + const MAX_LOGIN_ATTEMPTS = 5; - it('should block the user account after 5 unsuccessful attempts within 5 minutes', async () => { - const MAX_LOGIN_ATTEMPTS = 5; + for (let i = 0; i < MAX_LOGIN_ATTEMPTS; i++) { + await session.testAgent.post('/v1/auth/login').send({ + email: userCredentials.email, + password: 'wrong-password', + }); + } - for (let i = 0; i < MAX_LOGIN_ATTEMPTS; i++) { - await session.testAgent.post('/v1/auth/login').send({ + const { body } = await session.testAgent.post('/v1/auth/login').send({ email: userCredentials.email, - password: 'wrong-password', + password: userCredentials.password, }); - } - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: userCredentials.password, + expect(body.statusCode).to.equal(401); + expect(body.message).to.contain('Account blocked'); }); - expect(body.statusCode).to.equal(401); - expect(body.message).to.contain('Account blocked'); - }); - - it('should reset the account blocked error after 5 minutes and allow for more 5 failed attempts', async () => { - const MAX_LOGIN_ATTEMPTS = 5; - const BLOCKED_PERIOD_IN_MINUTES = 5; + it('should reset the account blocked error after 5 minutes and allow for more 5 failed attempts', async () => { + const MAX_LOGIN_ATTEMPTS = 5; + const BLOCKED_PERIOD_IN_MINUTES = 5; - const lastFailedAttempt = subMinutes(new Date(), BLOCKED_PERIOD_IN_MINUTES); + const lastFailedAttempt = subMinutes(new Date(), BLOCKED_PERIOD_IN_MINUTES); - const failedLogin = { - lastFailedAttempt: lastFailedAttempt.toISOString(), - times: MAX_LOGIN_ATTEMPTS, - }; + const failedLogin = { + lastFailedAttempt: lastFailedAttempt.toISOString(), + times: MAX_LOGIN_ATTEMPTS, + }; - await userRepository.update( - { - _id: session.user._id, - }, - { - $set: { - failedLogin, + await userRepository.update( + { + _id: session.user._id, }, + { + $set: { + failedLogin, + }, + } + ); + + for (let i = 0; i < MAX_LOGIN_ATTEMPTS - 1; i++) { + const { body } = await session.testAgent.post('/v1/auth/login').send({ + email: session.user.email, + password: 'wrong-password', + }); + + expect(body.message).to.contain('Incorrect email or password provided.'); + expect(body.statusCode).to.equal(401); } - ); - for (let i = 0; i < MAX_LOGIN_ATTEMPTS - 1; i++) { const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: session.user.email, - password: 'wrong-password', + email: userCredentials.email, + password: userCredentials.password, }); - expect(body.message).to.contain('Incorrect email or password provided.'); expect(body.statusCode).to.equal(401); - } + expect(body.message).to.contain('Account blocked'); + }); + }); - const { body } = await session.testAgent.post('/v1/auth/login').send({ - email: userCredentials.email, - password: userCredentials.password, + context('with OAuth', async () => { + const userEmail = 'testoauth@gmail.com'; + + before(async () => { + // Create a mock OAuth user without a password + await userRepository.create({ + email: userEmail, + firstName: 'Testy', + lastName: 'Oauth', + }); }); - expect(body.statusCode).to.equal(401); - expect(body.message).to.contain('Account blocked'); + it('should throw an error informing the user to use OAuth instead', async () => { + const { body } = await session.testAgent.post('/v1/auth/login').send({ + email: userEmail, + password: 'whatever', + }); + + expect(body.statusCode).to.equal(400); + expect(body.message).to.contain('Please sign in using Github.'); + }); }); }); diff --git a/apps/api/src/app/auth/usecases/login/login.usecase.ts b/apps/api/src/app/auth/usecases/login/login.usecase.ts index 4d4d1a9e220..09bf96f5eda 100644 --- a/apps/api/src/app/auth/usecases/login/login.usecase.ts +++ b/apps/api/src/app/auth/usecases/login/login.usecase.ts @@ -41,7 +41,8 @@ export class Login { throw new UnauthorizedException(`Account blocked, Please try again after ${blockedMinutesLeft} minutes`); } - if (!user.password) throw new ApiException('OAuth user login attempt'); + // TODO: Trigger a password reset flow automatically for existing OAuth users instead of throwing an error + if (!user.password) throw new ApiException('Please sign in using Github.'); const isMatching = await bcrypt.compare(command.password, user.password); if (!isMatching) { diff --git a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts index 252ccfaa04b..091c57e34a3 100644 --- a/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts +++ b/apps/api/src/app/blueprint/e2e/get-grouped-blueprints.e2e.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; import { UserSession } from '@novu/testing'; -import { NotificationTemplateRepository, EnvironmentRepository } from '@novu/dal'; +import { NotificationTemplateRepository, EnvironmentRepository, EnvironmentEntity } from '@novu/dal'; import { EmailBlockTypeEnum, FieldLogicalOperatorEnum, @@ -52,6 +52,7 @@ describe('Get grouped notification template blueprints - /blueprints/group-by-ca it('should get the grouped blueprints', async function () { const prodEnv = await getProductionEnvironment(); + if (!prodEnv) throw new Error('production environment was not found'); await createTemplateFromBlueprint({ session, notificationTemplateRepository, prodEnv }); @@ -82,6 +83,7 @@ describe('Get grouped notification template blueprints - /blueprints/group-by-ca it('should get the updated grouped blueprints (after invalidation)', async function () { const prodEnv = await getProductionEnvironment(); + if (!prodEnv) throw new Error('production environment was not found'); await createTemplateFromBlueprint({ session, @@ -167,7 +169,7 @@ export async function createTemplateFromBlueprint({ }: { session: UserSession; notificationTemplateRepository: NotificationTemplateRepository; - prodEnv; + prodEnv: EnvironmentEntity; }) { const testTemplateRequestDto: Partial = { name: 'test email template', @@ -215,8 +217,6 @@ export async function createTemplateFromBlueprint({ enabled: false, }); - if (!prodEnv) throw new Error('production environment was not found'); - const blueprintId = ( await notificationTemplateRepository.findOne({ _environmentId: prodEnv._id, diff --git a/apps/api/src/app/blueprint/usecases/get-blueprint/get-blueprint.usecase.ts b/apps/api/src/app/blueprint/usecases/get-blueprint/get-blueprint.usecase.ts index 79cca048a0c..dc2bd0a5d0d 100644 --- a/apps/api/src/app/blueprint/usecases/get-blueprint/get-blueprint.usecase.ts +++ b/apps/api/src/app/blueprint/usecases/get-blueprint/get-blueprint.usecase.ts @@ -1,10 +1,9 @@ import { Injectable, NotFoundException } from '@nestjs/common'; -import { NotificationTemplateRepository } from '@novu/dal'; +import { NotificationTemplateRepository, NotificationTemplateEntity } from '@novu/dal'; import { GetBlueprintCommand } from './get-blueprint.command'; import { GetBlueprintResponse } from '../../dto/get-blueprint.response.dto'; -import { NotificationTemplateEntity } from '@novu/dal/src'; @Injectable() export class GetBlueprint { diff --git a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts index 95bb8599bde..e0ca5dd1a20 100644 --- a/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts +++ b/apps/api/src/app/blueprint/usecases/get-grouped-blueprints/get-grouped-blueprints.usecase.ts @@ -1,7 +1,7 @@ import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { NotificationTemplateRepository, NotificationTemplateEntity } from '@novu/dal'; import { buildGroupedBlueprintsKey, CachedEntity } from '@novu/application-generic'; -import { INotificationTemplate, IGroupedBlueprint } from '@novu/shared'; +import { IGroupedBlueprint } from '@novu/shared'; import { GroupedBlueprintResponse } from '../../dto/grouped-blueprint.response.dto'; import { GetGroupedBlueprintsCommand, POPULAR_GROUPED_NAME, POPULAR_TEMPLATES_ID_LIST } from './index'; @@ -17,13 +17,13 @@ export class GetGroupedBlueprints { options: { ttl: WEEK_IN_SECONDS }, }) async execute(command: GetGroupedBlueprintsCommand): Promise { - const groups = await this.fetchGroupedBlueprints(); + const generalGroups = await this.fetchGroupedBlueprints(); - const updatePopularBlueprints = this.updatePopularBlueprints(groups); + const updatePopularBlueprints = this.getPopularGroupBlueprints(generalGroups); - const popular = { name: POPULAR_GROUPED_NAME, blueprints: updatePopularBlueprints }; + const popularGroup = { name: POPULAR_GROUPED_NAME, blueprints: updatePopularBlueprints }; - return { general: groups as IGroupedBlueprint[], popular }; + return { general: generalGroups as IGroupedBlueprint[], popular: popularGroup as IGroupedBlueprint }; } private async fetchGroupedBlueprints() { @@ -41,27 +41,28 @@ export class GetGroupedBlueprints { return groups.map((group) => group.blueprints).flat(); } - private updatePopularBlueprints( + private getPopularGroupBlueprints( groups: { name: string; blueprints: NotificationTemplateEntity[] }[] - ): INotificationTemplate[] { + ): NotificationTemplateEntity[] { const storedBlueprints = this.groupedToBlueprintsArray(groups); const localPopularIds = [...POPULAR_TEMPLATES_ID_LIST]; - const result: INotificationTemplate[] = []; + const result: NotificationTemplateEntity[] = []; for (const localPopularId of localPopularIds) { const storedBlueprint = storedBlueprints.find((blueprint) => blueprint._id === localPopularId); if (!storedBlueprint) { Logger.warn( - `Could not find stored popular blueprint id: ${localPopularId}, BLUEPRINT_CREATOR: ${NotificationTemplateRepository.getBlueprintOrganizationId()}` + `Could not find stored popular blueprint id: ${localPopularId}, BLUEPRINT_CREATOR: + ${NotificationTemplateRepository.getBlueprintOrganizationId()}` ); continue; } - result.push(storedBlueprint as INotificationTemplate); + result.push(storedBlueprint); } return result; diff --git a/apps/api/src/app/change/usecases/index.ts b/apps/api/src/app/change/usecases/index.ts index fa07f1263b9..a4c17fe7de5 100644 --- a/apps/api/src/app/change/usecases/index.ts +++ b/apps/api/src/app/change/usecases/index.ts @@ -11,6 +11,7 @@ import { PromoteFeedChange } from './promote-feed-change/promote-feed-change'; import { PromoteLayoutChange } from './promote-layout-change/promote-layout-change.use-case'; import { CreateChange } from '@novu/application-generic'; import { PromoteTranslationChange } from './promote-translation-change'; +import { PromoteTranslationGroupChange } from './promote-translation-group-change'; export * from './apply-change'; export * from './promote-change-to-environment'; @@ -30,4 +31,5 @@ export const USE_CASES = [ CountChanges, UpdateChange, PromoteTranslationChange, + PromoteTranslationGroupChange, ]; diff --git a/apps/api/src/app/change/usecases/promote-change-to-environment/promote-change-to-environment.usecase.ts b/apps/api/src/app/change/usecases/promote-change-to-environment/promote-change-to-environment.usecase.ts index 60f69d37b87..6ad8ae53109 100644 --- a/apps/api/src/app/change/usecases/promote-change-to-environment/promote-change-to-environment.usecase.ts +++ b/apps/api/src/app/change/usecases/promote-change-to-environment/promote-change-to-environment.usecase.ts @@ -1,8 +1,8 @@ -import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import { forwardRef, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common'; import { ChangeRepository, EnvironmentRepository } from '@novu/dal'; import { ChangeEntityTypeEnum } from '@novu/shared'; -import { applyDiff } from 'recursive-diff'; +import { applyDiff } from 'recursive-diff'; import { PromoteChangeToEnvironmentCommand } from './promote-change-to-environment.command'; import { PromoteTypeChangeCommand } from '../promote-type-change.command'; import { PromoteLayoutChange } from '../promote-layout-change'; @@ -10,8 +10,8 @@ import { PromoteNotificationTemplateChange } from '../promote-notification-templ import { PromoteMessageTemplateChange } from '../promote-message-template-change/promote-message-template-change'; import { PromoteNotificationGroupChange } from '../promote-notification-group-change/promote-notification-group-change'; import { PromoteFeedChange } from '../promote-feed-change/promote-feed-change'; -import { ModuleRef } from '@nestjs/core'; -import { PromoteTranslationChange } from '../promote-translation-change/promote-translation-change.usecase'; +import { PromoteTranslationChange } from '../promote-translation-change'; +import { PromoteTranslationGroupChange } from '../promote-translation-group-change'; @Injectable() export class PromoteChangeToEnvironment { @@ -25,7 +25,7 @@ export class PromoteChangeToEnvironment { private promoteNotificationGroupChange: PromoteNotificationGroupChange, private promoteFeedChange: PromoteFeedChange, private promoteTranslationChange: PromoteTranslationChange, - private moduleRef: ModuleRef + private promoteTranslationGroupChange: PromoteTranslationGroupChange ) {} async execute(command: PromoteChangeToEnvironmentCommand) { @@ -69,19 +69,7 @@ export class PromoteChangeToEnvironment { await this.promoteTranslationChange.execute(typeCommand); break; case ChangeEntityTypeEnum.TRANSLATION_GROUP: - try { - if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') { - if (!require('@novu/ee-translation')?.PromoteTranslationGroupChange) { - throw new BadRequestException('Translation module is not loaded'); - } - const usecase = this.moduleRef.get(require('@novu/ee-translation')?.PromoteTranslationGroupChange, { - strict: false, - }); - await usecase.execute(typeCommand); - } - } catch (e) { - Logger.error(e, `Unexpected error while importing enterprise modules`, 'PromoteChangeToEnvironment'); - } + await this.promoteTranslationGroupChange.execute(typeCommand); break; default: Logger.error(`Change with type ${command.type} could not be enabled from environment ${command.environmentId}`); diff --git a/apps/api/src/app/change/usecases/promote-translation-group-change/index.ts b/apps/api/src/app/change/usecases/promote-translation-group-change/index.ts new file mode 100644 index 00000000000..d7cc3cf2e5a --- /dev/null +++ b/apps/api/src/app/change/usecases/promote-translation-group-change/index.ts @@ -0,0 +1 @@ +export * from './promote-translation-group-change.usecase'; diff --git a/apps/api/src/app/change/usecases/promote-translation-group-change/promote-translation-group-change.usecase.ts b/apps/api/src/app/change/usecases/promote-translation-group-change/promote-translation-group-change.usecase.ts new file mode 100644 index 00000000000..19d848571c4 --- /dev/null +++ b/apps/api/src/app/change/usecases/promote-translation-group-change/promote-translation-group-change.usecase.ts @@ -0,0 +1,52 @@ +import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; + +import { ChangeRepository } from '@novu/dal'; +import { ChangeEntityTypeEnum } from '@novu/shared'; + +import { ApplyChange, ApplyChangeCommand } from '../apply-change'; +import { PromoteTypeChangeCommand } from '../promote-type-change.command'; + +@Injectable() +export class PromoteTranslationGroupChange { + constructor( + private moduleRef: ModuleRef, + @Inject(forwardRef(() => ApplyChange)) private applyChange: ApplyChange, + private changeRepository: ChangeRepository + ) {} + + async execute(command: PromoteTypeChangeCommand) { + try { + if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') { + if (!require('@novu/ee-translation')?.PromoteTranslationGroupChange) { + throw new BadRequestException('Translation module is not loaded'); + } + const usecase = this.moduleRef.get(require('@novu/ee-translation')?.PromoteTranslationGroupChange, { + strict: false, + }); + await usecase.execute(command, this.applyDefaultTranslationChange.bind(this)); + } + } catch (e) { + Logger.error(e, `Unexpected error while importing enterprise modules`, 'PromoteTranslationGroupChange'); + } + } + + private async applyDefaultTranslationChange(command: PromoteTypeChangeCommand, translationId: string) { + const changes = await this.changeRepository.getEntityChanges( + command.organizationId, + ChangeEntityTypeEnum.TRANSLATION, + translationId + ); + + for (const change of changes) { + await this.applyChange.execute( + ApplyChangeCommand.create({ + changeId: change._id, + environmentId: change._environmentId, + organizationId: change._organizationId, + userId: command.userId, + }) + ); + } + } +} diff --git a/apps/api/src/app/events/e2e/digest-events.e2e.ts b/apps/api/src/app/events/e2e/digest-events.e2e.ts index 67eea2bf746..c73169e52ae 100644 --- a/apps/api/src/app/events/e2e/digest-events.e2e.ts +++ b/apps/api/src/app/events/e2e/digest-events.e2e.ts @@ -380,7 +380,7 @@ describe('Trigger event - Digest triggered events - /v1/events/trigger (POST)', expect(jobs && jobs.length).to.equal(0); }); - it('should digest with backoff strategy', async function () { + it.skip('should digest with backoff strategy', async function () { template = await session.createTemplate({ steps: [ { diff --git a/apps/api/src/app/health/health.controller.ts b/apps/api/src/app/health/health.controller.ts index 2d20bae16b5..6f652090917 100644 --- a/apps/api/src/app/health/health.controller.ts +++ b/apps/api/src/app/health/health.controller.ts @@ -1,6 +1,12 @@ import { Controller, Get } from '@nestjs/common'; import { ApiExcludeController } from '@nestjs/swagger'; -import { HealthCheck, HealthCheckResult, HealthCheckService, HealthIndicatorFunction } from '@nestjs/terminus'; +import { + HealthCheck, + HealthCheckResult, + HealthCheckService, + HealthIndicatorFunction, + HealthCheckError, +} from '@nestjs/terminus'; import { CacheServiceHealthIndicator, DalServiceHealthIndicator, @@ -25,14 +31,12 @@ export class HealthController { const checks: HealthIndicatorFunction[] = [ async () => this.dalHealthIndicator.isHealthy(), async () => this.workflowQueueHealthIndicator.isHealthy(), - async () => { - return { - apiVersion: { - version, - status: 'up', - }, - }; - }, + async () => ({ + apiVersion: { + version, + status: 'up', + }, + }), ]; if (process.env.ELASTICACHE_CLUSTER_SERVICE_HOST) { diff --git a/apps/api/src/app/integrations/e2e/update-integration.e2e.ts b/apps/api/src/app/integrations/e2e/update-integration.e2e.ts index 57464cddec7..23e23fa12d3 100644 --- a/apps/api/src/app/integrations/e2e/update-integration.e2e.ts +++ b/apps/api/src/app/integrations/e2e/update-integration.e2e.ts @@ -55,7 +55,9 @@ describe('Update Integration - /integrations/:integrationId (PUT)', function () // update integration await session.testAgent.put(`/v1/integrations/${integrationId}`).send(payload); - const integration = (await session.testAgent.get(`/v1/integrations`)).body.data[0]; + const integration = (await session.testAgent.get(`/v1/integrations`)).body.data.find( + (fetchedIntegration) => fetchedIntegration._id === integrationId + ); expect(integration.credentials.apiKey).to.equal(payload.credentials.apiKey); expect(integration.credentials.secretKey).to.equal(payload.credentials.secretKey); diff --git a/apps/api/src/app/message-template/shared/sanitizer.service.spec.ts b/apps/api/src/app/message-template/shared/sanitizer.service.spec.ts index e4b7b9d6a4f..d9e2bde87fc 100644 --- a/apps/api/src/app/message-template/shared/sanitizer.service.spec.ts +++ b/apps/api/src/app/message-template/shared/sanitizer.service.spec.ts @@ -60,4 +60,16 @@ describe('HTML Sanitizer', function () { expect(result[0].content).to.equal('

Red Text

'); }); + + it('should NOT sanitize img tags', function () { + const result = sanitizeMessageContent([ + { + type: EmailBlockTypeEnum.TEXT, + content: 'Example Image', + url: '', + }, + ]); + + expect(result[0].content).to.equal('Example Image'); + }); }); diff --git a/apps/api/src/app/message-template/shared/sanitizer.service.ts b/apps/api/src/app/message-template/shared/sanitizer.service.ts index ea2651d5798..a3bafe14aa8 100644 --- a/apps/api/src/app/message-template/shared/sanitizer.service.ts +++ b/apps/api/src/app/message-template/shared/sanitizer.service.ts @@ -10,13 +10,14 @@ const sanitizeOptions: sanitize.IOptions = { /** * Additional tags to allow. */ - allowedTags: sanitize.defaults.allowedTags.concat(['style']), + allowedTags: sanitize.defaults.allowedTags.concat(['style', 'img']), allowedAttributes: { ...sanitize.defaults.allowedAttributes, /** * Additional attributes to allow on all tags. */ '*': ['style'], + img: ['src', 'srcset', 'alt', 'title', 'width', 'height', 'loading'], }, /** * Required to disable console warnings when allowing style tags. diff --git a/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts b/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts index a3c4d91516e..f1398f9a3c6 100644 --- a/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts +++ b/apps/api/src/app/message-template/usecases/update-message-template/update-message-template.usecase.ts @@ -33,7 +33,7 @@ export class UpdateMessageTemplate { updatePayload.name = command.name; } - if (command.content !== null) { + if (command.content !== null || command.content !== undefined) { updatePayload.content = command.contentType === 'editor' ? sanitizeMessageContent(command.content) : command.content; } diff --git a/apps/api/src/app/organization/organization.module.ts b/apps/api/src/app/organization/organization.module.ts index 9e058638c81..b9764c53d60 100644 --- a/apps/api/src/app/organization/organization.module.ts +++ b/apps/api/src/app/organization/organization.module.ts @@ -1,4 +1,13 @@ -import { MiddlewareConsumer, Module, NestModule, RequestMethod, forwardRef } from '@nestjs/common'; +import { + MiddlewareConsumer, + Module, + DynamicModule, + NestModule, + RequestMethod, + forwardRef, + Logger, + ForwardReference, +} from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { EnvironmentsModule } from '../environments/environments.module'; import { IntegrationModule } from '../integrations/integrations.module'; @@ -7,9 +16,32 @@ import { UserModule } from '../user/user.module'; import { OrganizationController } from './organization.controller'; import { USE_CASES } from './usecases'; import { AuthModule } from '../auth/auth.module'; +import { Type } from '@nestjs/common/interfaces/type.interface'; + +const enterpriseImports = (): Array | ForwardReference> => { + const modules: Array | ForwardReference> = []; + try { + if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') { + if (require('@novu/ee-billing')?.BillingModule) { + modules.push(require('@novu/ee-billing')?.BillingModule.forRoot()); + } + } + } catch (e) { + Logger.error(e, `Unexpected error while importing enterprise modules`, 'EnterpriseImport'); + } + + return modules; +}; @Module({ - imports: [SharedModule, UserModule, EnvironmentsModule, IntegrationModule, forwardRef(() => AuthModule)], + imports: [ + SharedModule, + UserModule, + EnvironmentsModule, + IntegrationModule, + forwardRef(() => AuthModule), + ...enterpriseImports(), + ], controllers: [OrganizationController], providers: [...USE_CASES], exports: [...USE_CASES], diff --git a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts index f78b81d41d4..266cea069f8 100644 --- a/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts +++ b/apps/api/src/app/organization/usecases/create-organization/create-organization.usecase.ts @@ -1,4 +1,4 @@ -import { Inject, Injectable, Scope } from '@nestjs/common'; +import { BadRequestException, Inject, Injectable, Logger, Scope } from '@nestjs/common'; import { OrganizationEntity, OrganizationRepository, UserRepository } from '@novu/dal'; import { ApiServiceLevelEnum, JobTitleEnum, MemberRoleEnum } from '@novu/shared'; import { AnalyticsService } from '@novu/application-generic'; @@ -14,6 +14,7 @@ import { CreateOrganizationCommand } from './create-organization.command'; import { ApiException } from '../../../shared/exceptions/api.exception'; import { CreateNovuIntegrations } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.usecase'; import { CreateNovuIntegrationsCommand } from '../../../integrations/usecases/create-novu-integrations/create-novu-integrations.command'; +import { ModuleRef } from '@nestjs/core'; @Injectable({ scope: Scope.REQUEST, @@ -26,7 +27,8 @@ export class CreateOrganization { private readonly userRepository: UserRepository, private readonly createEnvironmentUsecase: CreateEnvironment, private readonly createNovuIntegrations: CreateNovuIntegrations, - private analyticsService: AnalyticsService + private analyticsService: AnalyticsService, + private moduleRef: ModuleRef ) {} async execute(command: CreateOrganizationCommand): Promise { @@ -99,6 +101,10 @@ export class CreateOrganization { }) ); + if (organizationAfterChanges !== null) { + await this.startFreeTrial(user._id, organizationAfterChanges._id); + } + return organizationAfterChanges as OrganizationEntity; } @@ -116,4 +122,23 @@ export class CreateOrganization { this.analyticsService.setValue(user._id, 'jobTitle', jobTitle); } + + private async startFreeTrial(userId: string, organizationId: string) { + try { + if (process.env.NOVU_ENTERPRISE === 'true' || process.env.CI_EE_TEST === 'true') { + if (!require('@novu/ee-billing')?.StartReverseFreeTrial) { + throw new BadRequestException('Billing module is not loaded'); + } + const usecase = this.moduleRef.get(require('@novu/ee-billing')?.StartReverseFreeTrial, { + strict: false, + }); + await usecase.execute({ + userId, + organizationId, + }); + } + } catch (e) { + Logger.error(e, `Unexpected error while importing enterprise modules`, 'StartReverseFreeTrial'); + } + } } diff --git a/apps/api/src/app/shared/decorators/product-feature.decorator.ts b/apps/api/src/app/shared/decorators/product-feature.decorator.ts new file mode 100644 index 00000000000..945c513671a --- /dev/null +++ b/apps/api/src/app/shared/decorators/product-feature.decorator.ts @@ -0,0 +1,5 @@ +import { Reflector } from '@nestjs/core'; +import { ProductFeatureKeyEnum } from '@novu/shared'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export const ProductFeature = Reflector.createDecorator(); diff --git a/apps/api/src/app/shared/helpers/content.service.spec.ts b/apps/api/src/app/shared/helpers/content.service.spec.ts index 4482495d79e..4c3e9c0e2fd 100644 --- a/apps/api/src/app/shared/helpers/content.service.spec.ts +++ b/apps/api/src/app/shared/helpers/content.service.spec.ts @@ -256,6 +256,24 @@ describe('ContentService', function () { expect(variables[0].name).to.include('customVariables'); }); + it('should extract i18n content variables', function () { + const contentService = new ContentService(); + const { variables } = contentService.extractMessageVariables([ + { + template: { + type: StepTypeEnum.IN_APP, + content: '{{i18n "group.key" var=customVar.subVar var2=secVar}}', + }, + }, + ]); + + expect(variables.length).to.equal(2); + + const variablesNames = variables.map((variable) => variable.name); + expect(variablesNames).to.include('customVar.subVar'); + expect(variablesNames).to.include('secVar'); + }); + it('should extract action steps variables', function () { const contentService = new ContentService(); const { variables } = contentService.extractMessageVariables([ diff --git a/apps/api/src/app/shared/interceptors/product-feature.interceptor.ts b/apps/api/src/app/shared/interceptors/product-feature.interceptor.ts new file mode 100644 index 00000000000..acfb0a328b1 --- /dev/null +++ b/apps/api/src/app/shared/interceptors/product-feature.interceptor.ts @@ -0,0 +1,67 @@ +import { + CallHandler, + ExecutionContext, + HttpException, + Injectable, + NestInterceptor, + NotFoundException, + UnauthorizedException, +} from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { OrganizationRepository } from '@novu/dal'; +import { + ApiServiceLevelEnum, + IJwtPayload, + productFeatureEnabledForServiceLevel, + ProductFeatureKeyEnum, +} from '@novu/shared'; +import { Observable } from 'rxjs'; +import { ProductFeature } from '../decorators/product-feature.decorator'; + +@Injectable() +export class ProductFeatureInterceptor implements NestInterceptor { + constructor(private reflector: Reflector, private organizationRepository: OrganizationRepository) {} + + async intercept(context: ExecutionContext, next: CallHandler): Promise> { + try { + const handler = context.getHandler(); + const classRef = context.getClass(); + const requestedFeature: ProductFeatureKeyEnum | undefined = this.reflector.getAllAndOverride(ProductFeature, [ + handler, + classRef, + ]); + + if (requestedFeature === undefined) { + return next.handle(); + } + + const user = this.getReqUser(context); + + if (!user) { + throw new UnauthorizedException(); + } + + const { organizationId } = user; + + const organization = await this.organizationRepository.findById(organizationId); + + const enabled = productFeatureEnabledForServiceLevel[requestedFeature].includes( + organization?.apiServiceLevel as ApiServiceLevelEnum + ); + + if (!enabled) { + throw new HttpException('Payment Required', 402); + } + + return next.handle(); + } catch (error) { + throw error; + } + } + + private getReqUser(context: ExecutionContext): IJwtPayload { + const req = context.switchToHttp().getRequest(); + + return req.user; + } +} diff --git a/apps/api/src/app/testing/billing/create-setup-intent.e2e-ee.ts b/apps/api/src/app/testing/billing/create-setup-intent.e2e-ee.ts deleted file mode 100644 index 5a3bf9ef43c..00000000000 --- a/apps/api/src/app/testing/billing/create-setup-intent.e2e-ee.ts +++ /dev/null @@ -1,151 +0,0 @@ -import * as sinon from 'sinon'; -import { expect } from 'chai'; -import { ApiServiceLevelEnum } from '@novu/shared'; - -describe('Create setup intent', () => { - const eeBilling = require('@novu/ee-billing'); - if (!eeBilling.CreateSetupIntent) { - throw new Error("CreateSetupIntent doesn't exist"); - } - // eslint-disable-next-line @typescript-eslint/naming-convention - const { CreateSetupIntent } = eeBilling; - - const stubObject = { - setupIntents: { - list: () => {}, - create: () => {}, - }, - }; - - const getCustomerUsecase = { - execute: () => Promise.resolve({ id: 'customer_id' }), - }; - - const userRepository = { - findById: () => Promise.resolve({ email: 'user_email' }), - }; - - let spyGetCustomer: sinon.SinonSpy; - let stubCreateSetupIntent: sinon.SinonStub; - let stubListSetupIntents: sinon.SinonStub; - let stubGetUser: sinon.SinonStub; - - beforeEach(() => { - spyGetCustomer = sinon.spy(getCustomerUsecase, 'execute'); - stubCreateSetupIntent = sinon.stub(stubObject.setupIntents, 'create').resolves({ client_secret: 'client_secret' }); - stubListSetupIntents = sinon.stub(stubObject.setupIntents, 'list').resolves({ - data: [ - { - client_secret: 'client_secret', - status: 'succeeded', - metadata: { - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }, - }, - ], - }); - stubGetUser = sinon.stub(userRepository, 'findById').resolves({ email: 'user_email' }); - }); - - afterEach(() => { - spyGetCustomer.resetHistory(); - stubCreateSetupIntent.reset(); - stubListSetupIntents.reset(); - }); - - const createUseCase = () => { - const useCase = new CreateSetupIntent(stubObject, getCustomerUsecase, userRepository); - - return useCase; - }; - - it('should create a new setup intent', async () => { - const useCase = createUseCase(); - const result = await useCase.execute({ - organizationId: 'organization_id', - userId: 'user_id', - }); - - expect(stubCreateSetupIntent.lastCall.args.at(0)).to.deep.equal({ - customer: 'customer_id', - metadata: { - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }, - }); - - expect(spyGetCustomer.lastCall.args.at(0)).to.deep.equal({ - organizationId: 'organization_id', - email: 'user_email', - }); - - expect(stubListSetupIntents.lastCall.args.at(0)).to.deep.equal({ - customer: 'customer_id', - limit: 1, - }); - - expect(result).to.equal('client_secret'); - }); - - it('should setup intent with existing intent', async () => { - stubListSetupIntents.resolves({ - data: [ - { - client_secret: 'client_secret', - status: 'requires_payment_method', - metadata: { - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }, - }, - ], - }); - - const useCase = createUseCase(); - const result = await useCase.execute({ - organizationId: 'organization_id', - userId: 'user_id', - }); - - expect(stubCreateSetupIntent.callCount).to.equal(0); - expect(spyGetCustomer.lastCall.args.at(0)).to.deep.equal({ - organizationId: 'organization_id', - email: 'user_email', - }); - - expect(stubListSetupIntents.lastCall.args.at(0)).to.deep.equal({ - customer: 'customer_id', - limit: 1, - }); - - expect(result).to.equal('client_secret'); - }); - - it('should throw an error if user is not found', async () => { - stubGetUser.rejects(new Error('User not found: user_id')); - - const useCase = createUseCase(); - - try { - await useCase.execute({ - organizationId: 'organization_id', - userId: 'user_id', - }); - throw new Error('Should not reach here'); - } catch (e) { - expect(e.message).to.equal('User not found: user_id'); - } - }); - - it('should use the email from the user to get the customer', async () => { - stubGetUser.resolves({ email: 'user_email' }); - const useCase = createUseCase(); - await useCase.execute({ - organizationId: 'organization_id', - userId: 'user_id', - }); - - expect(spyGetCustomer.lastCall.args.at(0)).to.deep.equal({ - organizationId: 'organization_id', - email: 'user_email', - }); - }); -}); diff --git a/apps/api/src/app/testing/billing/create-usage-records.e2e-ee.ts b/apps/api/src/app/testing/billing/create-usage-records.e2e-ee.ts index ab83de0bbc5..b1fe9837776 100644 --- a/apps/api/src/app/testing/billing/create-usage-records.e2e-ee.ts +++ b/apps/api/src/app/testing/billing/create-usage-records.e2e-ee.ts @@ -3,13 +3,20 @@ import { Logger } from '@nestjs/common'; import * as sinon from 'sinon'; import { expect } from 'chai'; import { ApiServiceLevelEnum } from '@novu/shared'; +import { StripeBillingIntervalEnum, StripeUsageTypeEnum } from '@novu/ee-billing/src/stripe/types'; -const mockBusinessSubscription = { +const mockMonthlyBusinessSubscription = { id: 'subscription_id', items: { data: [ - { id: 'item_id_usage_notifications', price: { lookup_key: 'business_usage_notifications' } }, - { id: 'item_id_flat', price: { lookup_key: 'business_flat_monthly' } }, + { + id: 'item_id_usage_notifications', + price: { lookup_key: 'business_usage_notifications', recurring: { usage_type: StripeUsageTypeEnum.METERED } }, + }, + { + id: 'item_id_flat', + price: { lookup_key: 'business_flat_monthly', recurring: { usage_type: StripeUsageTypeEnum.LICENSED } }, + }, ], }, }; @@ -53,7 +60,10 @@ describe('CreateUsageRecords', () => { }, ] as any); getFeatureFlagStub = sinon.stub(getFeatureFlagUsecase, 'execute').resolves(true); - upsertSubscriptionStub = sinon.stub(upsertSubscriptionUsecase, 'execute').resolves(mockBusinessSubscription as any); + upsertSubscriptionStub = sinon.stub(upsertSubscriptionUsecase, 'execute').resolves({ + licensed: mockMonthlyBusinessSubscription, + metered: mockMonthlyBusinessSubscription, + } as any); getCustomerStub = sinon.stub(getCustomerUsecase, 'execute').resolves({ id: 'customer_id', deleted: false, @@ -61,7 +71,7 @@ describe('CreateUsageRecords', () => { organizationId: 'organization_id', }, subscriptions: { - data: [mockBusinessSubscription], + data: [mockMonthlyBusinessSubscription], }, } as any); }); @@ -141,20 +151,21 @@ describe('CreateUsageRecords', () => { { customer: mockNoSubscriptionsCustomer, apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, }, ]); }); - it('should set the usage timestamp to the subscription start date if the subscription is new', async () => { + it('should set the usage timestamp to the subscription current period start if the subscription is new', async () => { const mockSubscriptionStartDate = new Date('2021-02-01T00:00:00Z'); - const mockSubscriptionCreated = mockSubscriptionStartDate.getTime() / 1000; + const mockSubscriptionCurrentPeriodStart = mockSubscriptionStartDate.getTime() / 1000; const mockUsageStartDate = new Date('2021-01-15T00:00:00Z'); getCustomerStub.resolves({ subscriptions: { data: [ { - created: mockSubscriptionCreated, - ...mockBusinessSubscription, + ...mockMonthlyBusinessSubscription, + current_period_start: mockSubscriptionCurrentPeriodStart, }, ], }, @@ -167,7 +178,7 @@ describe('CreateUsageRecords', () => { }) ); - expect(createUsageRecordStub.lastCall.args[1].timestamp).to.equal(mockSubscriptionCreated); + expect(createUsageRecordStub.lastCall.args[1].timestamp).to.equal(mockSubscriptionCurrentPeriodStart); }); it('should set the usage timestamp to the usage start date if the subscription is not new', async () => { @@ -178,8 +189,8 @@ describe('CreateUsageRecords', () => { subscriptions: { data: [ { - created: mockSubscriptionCreated, - ...mockBusinessSubscription, + ...mockMonthlyBusinessSubscription, + current_period_start: mockSubscriptionCreated, }, ], }, diff --git a/apps/api/src/app/testing/billing/get-prices.e2e-ee.ts b/apps/api/src/app/testing/billing/get-prices.e2e-ee.ts index def35474021..900311442e9 100644 --- a/apps/api/src/app/testing/billing/get-prices.e2e-ee.ts +++ b/apps/api/src/app/testing/billing/get-prices.e2e-ee.ts @@ -1,6 +1,7 @@ import * as sinon from 'sinon'; import { expect } from 'chai'; import { ApiServiceLevelEnum } from '@novu/shared'; +import { StripeBillingIntervalEnum } from '@novu/ee-billing/src/stripe/types'; describe('GetPrices', () => { const eeBilling = require('@novu/ee-billing'); @@ -12,21 +13,18 @@ describe('GetPrices', () => { const stripeStub = { prices: { - list: () => {}, + list: sinon.stub(), }, }; let listPricesStub: sinon.SinonStub; beforeEach(() => { - listPricesStub = sinon.stub(stripeStub.prices, 'list').resolves({ - data: [ - { - id: 'price_id_1', - }, - { - id: 'price_id_2', - }, - ], + listPricesStub = stripeStub.prices.list; + listPricesStub.onFirstCall().resolves({ + data: [{ id: 'licensed_price_id_1' }], + }); + listPricesStub.onSecondCall().resolves({ + data: [{ id: 'metered_price_id_1' }], }); }); @@ -34,56 +32,94 @@ describe('GetPrices', () => { listPricesStub.reset(); }); - const createUseCase = () => { - const useCase = new GetPrices(stripeStub as any); - - return useCase; - }; + const createUseCase = () => new GetPrices(stripeStub as any); const expectedPrices = [ { apiServiceLevel: ApiServiceLevelEnum.FREE, - prices: ['free_usage_notifications'], + billingInterval: StripeBillingIntervalEnum.MONTH, + prices: { + licensed: ['free_flat_monthly'], + metered: ['free_usage_notifications'], + }, }, { apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - prices: ['business_flat_monthly', 'business_usage_notifications'], + billingInterval: StripeBillingIntervalEnum.MONTH, + prices: { + licensed: ['business_flat_monthly'], + metered: ['business_usage_notifications'], + }, + }, + { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.YEAR, + prices: { + licensed: ['business_flat_annually'], + metered: ['business_usage_notifications'], + }, + }, + { + apiServiceLevel: ApiServiceLevelEnum.ENTERPRISE, + billingInterval: StripeBillingIntervalEnum.MONTH, + prices: { + licensed: ['enterprise_flat_monthly'], + metered: ['enterprise_usage_notifications'], + }, + }, + { + apiServiceLevel: ApiServiceLevelEnum.ENTERPRISE, + billingInterval: StripeBillingIntervalEnum.YEAR, + prices: { + licensed: ['enterprise_flat_annually'], + metered: ['enterprise_usage_notifications'], + }, }, ]; + expectedPrices - .map(({ apiServiceLevel, prices }) => { + .map(({ apiServiceLevel, billingInterval, prices }) => { return () => { - describe(`apiServiceLevel of ${apiServiceLevel}`, () => { - it(`should fetch the prices list with the expected values`, async () => { + describe(`apiServiceLevel of ${apiServiceLevel} and billingInterval of ${billingInterval}`, () => { + it(`should fetch the prices list with the expected lookup keys`, async () => { const useCase = createUseCase(); await useCase.execute( GetPricesCommand.create({ - apiServiceLevel: apiServiceLevel, + apiServiceLevel, + billingInterval, }) ); - expect(listPricesStub.lastCall.args[0].lookup_keys).to.contain.members(prices); - }); - - it(`should throw an error if no prices are found`, async () => { - listPricesStub.resolves({ data: [] }); - const useCase = createUseCase(); - - try { - await useCase.execute( - GetPricesCommand.create({ - apiServiceLevel: apiServiceLevel, - }) - ); - } catch (e) { - expect(e.message).to.equal( - `No price found for apiServiceLevel: '${apiServiceLevel}' and lookup_keys: '${prices}'` - ); - } + const allCallsArgs = listPricesStub.getCalls().map((call) => call.args[0]); + expect(allCallsArgs).to.deep.equal([ + { + lookup_keys: prices.licensed, + }, + { + lookup_keys: prices.metered, + }, + ]); }); }); }; }) .forEach((test) => test()); + + it(`should throw an error if no prices are found`, async () => { + listPricesStub.onFirstCall().resolves({ data: [] }); + listPricesStub.onSecondCall().resolves({ data: [] }); + const useCase = createUseCase(); + + try { + await useCase.execute( + GetPricesCommand.create({ + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + } catch (e) { + expect(e.message).to.include(`No prices found for apiServiceLevel: '${ApiServiceLevelEnum.BUSINESS}'`); + } + }); }); diff --git a/apps/api/src/app/testing/billing/upsert-setup-intent.e2e-ee.ts b/apps/api/src/app/testing/billing/upsert-setup-intent.e2e-ee.ts new file mode 100644 index 00000000000..411a5d2c6cd --- /dev/null +++ b/apps/api/src/app/testing/billing/upsert-setup-intent.e2e-ee.ts @@ -0,0 +1,213 @@ +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { StripeBillingIntervalEnum } from '@novu/ee-billing/src/stripe/types'; + +describe('Upsert setup intent', () => { + const eeBilling = require('@novu/ee-billing'); + if (!eeBilling.UpsertSetupIntent) { + throw new Error("UpsertSetupIntent doesn't exist"); + } + // eslint-disable-next-line @typescript-eslint/naming-convention + const { UpsertSetupIntent } = eeBilling; + + const stubObject = { + setupIntents: { + list: () => {}, + create: () => {}, + update: () => {}, + }, + }; + + const getCustomerUsecase = { + execute: () => Promise.resolve({ id: 'customer_id' }), + }; + + const userRepository = { + findById: () => Promise.resolve({ email: 'user_email' }), + }; + + let spyGetCustomer: sinon.SinonSpy; + let stubCreateSetupIntent: sinon.SinonStub; + let stubListSetupIntents: sinon.SinonStub; + let stubUpdateSetupIntent: sinon.SinonStub; + let stubGetUser: sinon.SinonStub; + + beforeEach(() => { + spyGetCustomer = sinon.spy(getCustomerUsecase, 'execute'); + stubCreateSetupIntent = sinon.stub(stubObject.setupIntents, 'create').resolves({ client_secret: 'client_secret' }); + stubListSetupIntents = sinon.stub(stubObject.setupIntents, 'list').resolves({ + data: [ + { + client_secret: 'client_secret', + status: 'succeeded', + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }, + }, + ], + }); + stubUpdateSetupIntent = sinon.stub(stubObject.setupIntents, 'update').resolves({}); + stubGetUser = sinon.stub(userRepository, 'findById').resolves({ email: 'user_email' }); + }); + + afterEach(() => { + spyGetCustomer.resetHistory(); + stubCreateSetupIntent.reset(); + stubListSetupIntents.reset(); + stubUpdateSetupIntent.reset(); + }); + + const createUseCase = () => { + const useCase = new UpsertSetupIntent(stubObject, getCustomerUsecase, userRepository); + + return useCase; + }; + + it('should create a new setup intent', async () => { + const useCase = createUseCase(); + const result = await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + + expect(stubCreateSetupIntent.lastCall.args.at(0)).to.deep.equal({ + customer: 'customer_id', + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }, + }); + + expect(result).to.deep.equal({ clientSecret: 'client_secret' }); + }); + + it('should setup intent with existing intent requiring update', async () => { + stubListSetupIntents.resolves({ + data: [ + { + id: 'intent_id', + client_secret: 'client_secret', + status: 'requires_payment_method', + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.YEAR, + }, + }, + ], + }); + + const useCase = createUseCase(); + const result = await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + + expect( + stubUpdateSetupIntent.calledWith('intent_id', { + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }, + }) + ).to.be.true; + + expect(result).to.deep.equal({ clientSecret: 'client_secret' }); + }); + + it('should not create or update setup intent if existing intent matches', async () => { + stubListSetupIntents.resolves({ + data: [ + { + client_secret: 'client_secret', + status: 'requires_payment_method', + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }, + }, + ], + }); + + const useCase = createUseCase(); + const result = await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + + expect(stubCreateSetupIntent.callCount).to.equal(0); + expect(stubUpdateSetupIntent.callCount).to.equal(0); + expect(result).to.deep.equal({ clientSecret: 'client_secret' }); + }); + + it('should throw an error if user is not found', async () => { + stubGetUser.rejects(new Error('User not found: user_id')); + + const useCase = createUseCase(); + + try { + await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal('User not found: user_id'); + } + }); + + it('should use the email from the user to get the customer', async () => { + stubGetUser.resolves({ email: 'user_email' }); + const useCase = createUseCase(); + await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + + expect(spyGetCustomer.lastCall.args.at(0)).to.deep.equal({ + organizationId: 'organization_id', + email: 'user_email', + }); + }); + + it('should throw an error when the apiServiceLevel is not BUSINESS', async () => { + const useCase = createUseCase(); + try { + await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`API service level not allowed: ${ApiServiceLevelEnum.FREE}`); + } + }); + + it('should throw an error when given an invalid billing interval', async () => { + const useCase = createUseCase(); + try { + await useCase.execute({ + organizationId: 'organization_id', + userId: 'user_id', + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: 'invalid', + }); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`Invalid billing interval: 'invalid'`); + } + }); +}); diff --git a/apps/api/src/app/testing/billing/upsert-subscription.e2e-ee.ts b/apps/api/src/app/testing/billing/upsert-subscription.e2e-ee.ts index a2d9b91ccb0..2877cc3faa5 100644 --- a/apps/api/src/app/testing/billing/upsert-subscription.e2e-ee.ts +++ b/apps/api/src/app/testing/billing/upsert-subscription.e2e-ee.ts @@ -2,6 +2,7 @@ import * as sinon from 'sinon'; import { OrganizationRepository } from '@novu/dal'; import { expect } from 'chai'; import { ApiServiceLevelEnum } from '@novu/shared'; +import { StripeBillingIntervalEnum, StripeUsageTypeEnum } from '@novu/ee-billing/src/stripe/types'; describe('UpsertSubscription', () => { const eeBilling = require('@novu/ee-billing'); @@ -15,16 +16,18 @@ describe('UpsertSubscription', () => { subscriptions: { create: () => {}, update: () => {}, + del: () => {}, }, }; let updateSubscriptionStub: sinon.SinonStub; let createSubscriptionStub: sinon.SinonStub; + let deleteSubscriptionStub: sinon.SinonStub; let getPricesStub: sinon.SinonStub; const repo = new OrganizationRepository(); let updateOrgStub: sinon.SinonStub; - const mockCustomer = { + const mockCustomerBase = { id: 'customer_id', deleted: false, metadata: { @@ -34,24 +37,40 @@ describe('UpsertSubscription', () => { data: [ { id: 'subscription_id', - items: { data: [{ id: 'item_id_usage_notifications' }, { id: 'item_id_flat' }] }, + billing_cycle_anchor: 123456789, + items: { + data: [ + { + id: 'item_id_usage_notifications', + price: { recurring: { usage_type: StripeUsageTypeEnum.METERED } }, + }, + { id: 'item_id_flat', price: { recurring: { usage_type: StripeUsageTypeEnum.LICENSED } } }, + ], + }, }, ], }, }; beforeEach(() => { - getPricesStub = sinon.stub(GetPrices.prototype, 'execute').resolves([ - { - id: 'price_id_1', - }, - { - id: 'price_id_2', - }, - ] as any); + getPricesStub = sinon.stub(GetPrices.prototype, 'execute').resolves({ + metered: [ + { + id: 'price_id_notifications', + recurring: { usage_type: StripeUsageTypeEnum.METERED }, + }, + ], + licensed: [ + { + id: 'price_id_flat', + recurring: { usage_type: StripeUsageTypeEnum.LICENSED }, + }, + ], + } as any); updateOrgStub = sinon.stub(repo, 'update').resolves({ matched: 1, modified: 1 }); createSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'create'); updateSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'update'); + deleteSubscriptionStub = sinon.stub(stripeStub.subscriptions, 'del'); }); afterEach(() => { @@ -59,6 +78,7 @@ describe('UpsertSubscription', () => { updateOrgStub.reset(); createSubscriptionStub.reset(); updateSubscriptionStub.reset(); + deleteSubscriptionStub.reset(); }); const createUseCase = () => { @@ -68,90 +88,536 @@ describe('UpsertSubscription', () => { }; describe('Subscription upserting', () => { - it('should create a new subscription if the customer has no subscriptions', async () => { - const useCase = createUseCase(); - const customer = { ...mockCustomer, subscriptions: { data: [] } }; + describe('ZERO active subscriptions', () => { + const mockCustomerNoSubscriptions = { + ...mockCustomerBase, + subscriptions: { data: [] }, + }; - await useCase.execute( - UpsertSubscriptionCommand.create({ - customer: customer as any, - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }) - ); + describe('Monthly Billing Interval', () => { + it('should create a single subscription with monthly prices', async () => { + const useCase = createUseCase(); - expect(createSubscriptionStub.lastCall.args).to.deep.equal([ - { - customer: 'customer_id', - items: [ + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerNoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + + expect(createSubscriptionStub.lastCall.args).to.deep.equal([ { - price: 'price_id_1', + customer: 'customer_id', + items: [ + { + price: 'price_id_notifications', + }, + { + price: 'price_id_flat', + }, + ], }, + ]); + }); + + it('should set the trial configuration for the subscription when trial days are provided', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerNoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, + trialPeriodDays: 10, + }) + ); + + expect(createSubscriptionStub.lastCall.args).to.deep.equal([ + { + customer: 'customer_id', + trial_period_days: 10, + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + items: [ + { + price: 'price_id_notifications', + }, + { + price: 'price_id_flat', + }, + ], + }, + ]); + }); + }); + + describe('Annual Billing Interval', () => { + it('should create two subscriptions, one with monthly prices and one with annual prices', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerNoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.YEAR, + }) + ); + + expect(createSubscriptionStub.callCount).to.equal(2); + expect(createSubscriptionStub.getCalls().map((call) => call.args)).to.deep.equal([ + [ + { + customer: 'customer_id', + items: [ + { + price: 'price_id_flat', + }, + ], + }, + ], + [ + { + customer: 'customer_id', + items: [ + { + price: 'price_id_notifications', + }, + ], + }, + ], + ]); + }); + + it('should set the trial configuration for both subscriptions when trial days are provided', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerNoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.YEAR, + trialPeriodDays: 10, + }) + ); + + expect(createSubscriptionStub.callCount).to.equal(2); + expect(createSubscriptionStub.getCalls().map((call) => call.args)).to.deep.equal([ + [ + { + customer: 'customer_id', + trial_period_days: 10, + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + items: [ + { + price: 'price_id_flat', + }, + ], + }, + ], + [ + { + customer: 'customer_id', + trial_period_days: 10, + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + items: [ + { + price: 'price_id_notifications', + }, + ], + }, + ], + ]); + }); + }); + }); + + describe('ONE active subscription', () => { + const mockCustomerOneSubscription = { + ...mockCustomerBase, + subscriptions: { + data: [ { - price: 'price_id_2', + id: 'subscription_id', + items: { + data: [ + { + id: 'item_id_usage_notifications', + price: { recurring: { usage_type: StripeUsageTypeEnum.METERED } }, + }, + { id: 'item_id_flat', price: { recurring: { usage_type: StripeUsageTypeEnum.LICENSED } } }, + ], + }, }, ], }, - ]); - }); + }; - it('should update the existing subscription if the customer has a subscription', async () => { - const useCase = createUseCase(); + describe('Monthly Billing Interval', () => { + it('should update the existing subscription', async () => { + const useCase = createUseCase(); - await useCase.execute( - UpsertSubscriptionCommand.create({ - customer: mockCustomer as any, - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }) - ); + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerOneSubscription as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); - expect(updateSubscriptionStub.lastCall.args).to.deep.equal([ - 'subscription_id', - { - items: [ + expect(updateSubscriptionStub.lastCall.args).to.deep.equal([ + 'subscription_id', + { + items: [ + { + id: 'item_id_usage_notifications', + price: 'price_id_notifications', + }, + { + id: 'item_id_flat', + price: 'price_id_flat', + }, + ], + }, + ]); + }); + }); + + describe('Annual Billing Interval', () => { + it('should create a new annual subscription and update the existing subscription', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerOneSubscription as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.YEAR, + }) + ); + + expect(createSubscriptionStub.lastCall.args).to.deep.equal([ + { + customer: 'customer_id', + items: [ + { + price: 'price_id_flat', + }, + ], + }, + ]); + + expect(updateSubscriptionStub.lastCall.args).to.deep.equal([ + 'subscription_id', + { + items: [ + { + id: 'item_id_usage_notifications', + price: 'price_id_notifications', + }, + { + id: 'item_id_flat', + deleted: true, + }, + ], + }, + ]); + }); + + it('should set the trial configuration for the newly created annual subscription from the existing licensed subscription', async () => { + const useCase = createUseCase(); + const customer = { + ...mockCustomerBase, + subscriptions: { + data: [ + { + id: 'subscription_id', + trial_end: 123456789, + items: { + data: [ + { + id: 'item_id_usage_notifications', + price: { recurring: { usage_type: StripeUsageTypeEnum.METERED } }, + }, + { id: 'item_id_flat', price: { recurring: { usage_type: StripeUsageTypeEnum.LICENSED } } }, + ], + }, + }, + ], + }, + }; + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.YEAR, + }) + ); + + expect(createSubscriptionStub.lastCall.args).to.deep.equal([ { - price: 'price_id_1', + customer: 'customer_id', + trial_end: 123456789, + trial_settings: { + end_behavior: { + missing_payment_method: 'cancel', + }, + }, + items: [ + { + price: 'price_id_flat', + }, + ], }, + ]); + }); + }); + + it('should throw an error if the licensed subscription is not found', async () => { + const useCase = createUseCase(); + const customer = { + ...mockCustomerBase, + subscriptions: { + data: [{ items: { data: [{ price: { recurring: { usage_type: 'invalid' } } }] } }], + }, + }; + + try { + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: customer as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`No licensed subscription found for customer with id: 'customer_id'`); + } + }); + }); + + describe('TWO active subscriptions', () => { + const mockCustomerTwoSubscriptions = { + ...mockCustomerBase, + subscriptions: { + data: [ { - price: 'price_id_2', + id: 'subscription_id_1', + items: { + data: [{ id: 'item_id_flat', price: { recurring: { usage_type: StripeUsageTypeEnum.LICENSED } } }], + }, + }, + { + id: 'subscription_id_2', + items: { + data: [ + { + id: 'item_id_usage_notifications', + price: { recurring: { usage_type: StripeUsageTypeEnum.METERED } }, + }, + ], + }, }, ], }, - ]); + }; + + describe('Monthly Billing Interval', () => { + it('should delete the licensed subscription and update the metered subscription', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerTwoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + + expect(deleteSubscriptionStub.lastCall.args).to.deep.equal(['subscription_id_1', { prorate: true }]); + + expect(updateSubscriptionStub.lastCall.args).to.deep.equal([ + 'subscription_id_2', + { + items: [ + { + id: 'item_id_flat', + price: 'price_id_flat', + }, + { + id: 'item_id_usage_notifications', + price: 'price_id_notifications', + }, + ], + }, + ]); + }); + }); + + describe('Annual Billing Interval', () => { + it('should update the existing subscriptions', async () => { + const useCase = createUseCase(); + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerTwoSubscriptions as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.YEAR, + }) + ); + + expect(updateSubscriptionStub.getCalls().map((call) => call.args)).to.deep.equal([ + [ + 'subscription_id_1', + { + items: [ + { + id: 'item_id_flat', + price: 'price_id_flat', + }, + ], + }, + ], + [ + 'subscription_id_2', + { + items: [ + { + id: 'item_id_flat', + deleted: true, + }, + { + id: 'item_id_usage_notifications', + price: 'price_id_notifications', + }, + ], + }, + ], + ]); + }); + }); + + it('should throw an error if the licensed subscription is not found', async () => { + const useCase = createUseCase(); + const customer = { + ...mockCustomerBase, + subscriptions: { + data: [ + { items: { data: [{ price: { recurring: { usage_type: StripeUsageTypeEnum.METERED } } }] } }, + { items: { data: [{ price: { recurring: { usage_type: 'invalid' } } }] } }, + ], + }, + }; + + try { + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: customer as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`No licensed subscription found for customer with id: 'customer_id'`); + } + }); + + it('should throw an error if the metered subscription is not found', async () => { + const useCase = createUseCase(); + const customer = { + ...mockCustomerBase, + subscriptions: { + data: [ + { items: { data: [{ price: { recurring: { usage_type: StripeUsageTypeEnum.LICENSED } } }] } }, + { items: { data: [{ price: { recurring: { usage_type: 'invalid' } } }] } }, + ], + }, + }; + + try { + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: customer as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`No metered subscription found for customer with id: 'customer_id'`); + } + }); }); - it('should throw an error if the customer has more than one subscription', async () => { + describe('More than TWO active subscriptions', () => { + it('should throw an error if the customer has more than two subscription', async () => { + const useCase = createUseCase(); + const customer = { ...mockCustomerBase, subscriptions: { data: [{}, {}, {}] } }; + + try { + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: customer as any, + apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: StripeBillingIntervalEnum.MONTH, + }) + ); + throw new Error('Should not reach here'); + } catch (e) { + expect(e.message).to.equal(`Customer with id: 'customer_id' has more than two subscriptions`); + } + }); + }); + + it('should throw an error if the billing interval is not supported', async () => { const useCase = createUseCase(); - const customer = { ...mockCustomer, subscriptions: { data: [{}, {}] } }; + const customer = { ...mockCustomerBase, subscriptions: { data: [{}, {}] } }; try { await useCase.execute( UpsertSubscriptionCommand.create({ customer: customer as any, apiServiceLevel: ApiServiceLevelEnum.FREE, + billingInterval: 'invalid', }) ); throw new Error('Should not reach here'); } catch (e) { - expect(e.message).to.equal(`Customer with id: 'customer_id' has more than one subscription`); + expect(e.message).to.equal(`Invalid billing interval: 'invalid'`); } }); - }); - describe('Organization entity update', () => { - it('should update the organization with the new apiServiceLevel', async () => { - const useCase = createUseCase(); + describe('Organization entity update', () => { + it('should update the organization with the new apiServiceLevel', async () => { + const useCase = createUseCase(); + const customer = { ...mockCustomerBase, subscriptions: { data: [{}, {}] } }; + + await useCase.execute( + UpsertSubscriptionCommand.create({ + customer: mockCustomerBase as any, + billingInterval: StripeBillingIntervalEnum.MONTH, + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + }) + ); - await useCase.execute( - UpsertSubscriptionCommand.create({ - customer: mockCustomer as any, - apiServiceLevel: ApiServiceLevelEnum.BUSINESS, - }) - ); - - expect(updateOrgStub.lastCall.args).to.deep.equal([ - { _id: 'organization_id' }, - { apiServiceLevel: ApiServiceLevelEnum.BUSINESS }, - ]); + expect(updateOrgStub.lastCall.args).to.deep.equal([ + { _id: 'organization_id' }, + { apiServiceLevel: ApiServiceLevelEnum.BUSINESS }, + ]); + }); }); }); }); diff --git a/apps/api/src/app/testing/billing/webhook.e2e-ee.ts b/apps/api/src/app/testing/billing/webhook.e2e-ee.ts index 2a1e6a0a4a0..084207b3ab8 100644 --- a/apps/api/src/app/testing/billing/webhook.e2e-ee.ts +++ b/apps/api/src/app/testing/billing/webhook.e2e-ee.ts @@ -1,6 +1,7 @@ import * as sinon from 'sinon'; import { expect } from 'chai'; import { ApiServiceLevelEnum } from '@novu/shared'; +import { StripeBillingIntervalEnum } from '@novu/ee-billing/src/stripe/types'; const mockSetupIntentSucceededEvent = { type: 'setup_intent.succeeded', @@ -13,6 +14,7 @@ const mockSetupIntentSucceededEvent = { livemode: false, metadata: { apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + billingInterval: StripeBillingIntervalEnum.MONTH, }, payment_method_options: null, payment_method_types: [] as string[], @@ -47,6 +49,29 @@ describe('Stripe webhooks', () => { customers: { update: () => {}, }, + subscriptions: { + retrieve: () => + Promise.resolve({ + items: { + data: [ + { + id: 'subscription_id', + items: { data: [{ id: 'item_id_usage_notifications' }, { id: 'item_id_flat' }] }, + price: { + recurring: { + usage_type: 'licensed', + }, + product: { + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + }, + }, + }, + }, + ], + }, + }), + }, }; const eeBilling = require('@novu/ee-billing'); @@ -54,13 +79,17 @@ describe('Stripe webhooks', () => { throw new Error('ee-billing does not exist'); } // eslint-disable-next-line @typescript-eslint/naming-convention - const { SetupIntentSucceededHandler, UpsertSubscription, VerifyCustomer } = eeBilling; + const { SetupIntentSucceededHandler, CustomerSubscriptionCreatedHandler, UpsertSubscription, VerifyCustomer } = + eeBilling; describe('setup_intent.succeeded', () => { let updateCustomerStub: sinon.SinonStub; let verifyCustomerStub: sinon.SinonStub; let upsertSubscriptionStub: sinon.SinonStub; + const analyticsServiceStub = { + track: sinon.stub(), + }; beforeEach(() => { verifyCustomerStub = sinon.stub(VerifyCustomer.prototype, 'execute').resolves({ @@ -82,7 +111,8 @@ describe('Stripe webhooks', () => { organization: { _id: 'organization_id', apiServiceLevel: ApiServiceLevelEnum.FREE }, } as any); upsertSubscriptionStub = sinon.stub(UpsertSubscription.prototype, 'execute').resolves({ - id: 'subscription_id', + licensed: { id: 'licensed_subscription_id' }, + metered: { id: 'metered_subscription_id' }, } as any); updateCustomerStub = sinon.stub(stripeStub.customers, 'update').resolves({}); }); @@ -91,7 +121,8 @@ describe('Stripe webhooks', () => { const handler = new SetupIntentSucceededHandler( stripeStub as any, { execute: verifyCustomerStub } as any, - { execute: upsertSubscriptionStub } as any + { execute: upsertSubscriptionStub } as any, + analyticsServiceStub as any ); return handler; @@ -141,14 +172,247 @@ describe('Stripe webhooks', () => { }); describe('customer.subscription.created', () => { - it('Should handle customer.subscription.created event with known organization', async () => { - // @TODO: Implement test - expect(true).to.equal(true); + let verifyCustomerStub: sinon.SinonStub; + const organizationRepositoryStub = { + update: sinon.stub().resolves({ matched: 1, modified: 1 }), + }; + const analyticsServiceStub = { + track: sinon.stub(), + }; + + beforeEach(() => { + verifyCustomerStub = sinon.stub(VerifyCustomer.prototype, 'execute').resolves({ + customer: { + id: 'customer_id', + deleted: false, + metadata: { + organizationId: 'organization_id', + }, + }, + organization: { _id: 'organization_id', apiServiceLevel: ApiServiceLevelEnum.FREE }, + } as any); }); - it('Should exit early with unknown organization', async () => { - // @TODO: Implement test - expect(true).to.equal(true); + afterEach(() => { + organizationRepositoryStub.update.reset(); + }); + + const createHandler = () => { + const handler = new CustomerSubscriptionCreatedHandler( + stripeStub as any, + { execute: verifyCustomerStub } as any, + organizationRepositoryStub, + analyticsServiceStub as any + ); + + return handler; + }; + + it('should handle event with known organization', async () => { + const event = { + data: { + object: { + id: 'sub_123', + customer: 'cus_123', + items: [ + { + id: 'si_123', + data: { + plan: { + interval: StripeBillingIntervalEnum.MONTH, + }, + }, + }, + ], + }, + }, + created: 1234567890, + }; + + const handler = createHandler(); + await handler.handle(event); + + expect( + organizationRepositoryStub.update.calledWith( + { _id: 'organization_id' }, + { apiServiceLevel: ApiServiceLevelEnum.BUSINESS } + ) + ).to.be.true; + }); + + it('should exit early with unknown organization', async () => { + const event = { + data: { + object: { + id: 'sub_123', + customer: 'cus_123', + items: [ + { + id: 'si_123', + data: { + plan: { + interval: StripeBillingIntervalEnum.MONTH, + }, + }, + }, + ], + }, + }, + created: 1234567890, + }; + + verifyCustomerStub.resolves({ + organization: null, + customer: { id: 'customer_id', metadata: { organizationId: 'org_id' } }, + }); + + const handler = createHandler(); + await handler.handle(event); + + expect(organizationRepositoryStub.update.called).to.be.false; + }); + + it('should handle event with known organization and licensed subscription', async () => { + const event = { + data: { + object: { + id: 'sub_123', + customer: 'cus_123', + items: [ + { + id: 'si_123', + data: { + plan: { + interval: StripeBillingIntervalEnum.MONTH, + }, + price: { + recurring: { + usage_type: 'licensed', + }, + product: { + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + }, + }, + }, + }, + }, + ], + }, + }, + created: 1234567890, + }; + + const handler = createHandler(); + await handler.handle(event); + + expect( + organizationRepositoryStub.update.calledWith( + { _id: 'organization_id' }, + { apiServiceLevel: ApiServiceLevelEnum.BUSINESS } + ) + ).to.be.true; + }); + + it('should exit early with known organization and metered subscription', async () => { + const event = { + data: { + object: { + id: 'sub_123', + customer: 'cus_123', + items: [ + { + id: 'si_123', + data: { + plan: { + interval: StripeBillingIntervalEnum.MONTH, + }, + price: { + recurring: { + usage_type: 'metered', + }, + product: { + metadata: { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + }, + }, + }, + }, + }, + ], + }, + }, + created: 1234567890, + }; + + const handler = createHandler(); + await handler.handle(event); + + expect( + organizationRepositoryStub.update.calledWith( + { _id: 'organization_id' }, + { apiServiceLevel: ApiServiceLevelEnum.BUSINESS } + ) + ).to.be.true; + }); + + it('should exit early with known organization and invalid apiServiceLevel', async () => { + const event = { + data: { + object: { + id: 'sub_123', + customer: 'cus_123', + items: [ + { + id: 'si_123', + data: { + plan: { + interval: StripeBillingIntervalEnum.MONTH, + }, + price: { + recurring: { + usage_type: 'licensed', + }, + product: { + metadata: { + apiServiceLevel: 'invalid', + }, + }, + }, + }, + }, + ], + }, + }, + created: 1234567890, + }; + + stripeStub.subscriptions.retrieve = () => + Promise.resolve({ + items: { + data: [ + { + id: 'subscription_id', + items: { data: [{ id: 'item_id_usage_notifications' }, { id: 'item_id_flat' }] }, + price: { + recurring: { + usage_type: 'licensed', + }, + product: { + metadata: { + apiServiceLevel: 'invalid' as any, + }, + }, + }, + }, + ], + }, + }); + + const handler = createHandler(); + await handler.handle(event); + + expect(organizationRepositoryStub.update.called).to.be.false; }); }); }); diff --git a/apps/api/src/app/testing/product-feature.e2e.ts b/apps/api/src/app/testing/product-feature.e2e.ts new file mode 100644 index 00000000000..9e5d413cb85 --- /dev/null +++ b/apps/api/src/app/testing/product-feature.e2e.ts @@ -0,0 +1,32 @@ +import { OrganizationRepository } from '@novu/dal'; +import { ApiServiceLevelEnum } from '@novu/shared'; +import { UserSession } from '@novu/testing'; +import { expect } from 'chai'; + +describe('Product feature Test', async () => { + let session: UserSession; + const path = '/v1/testing/product-feature'; + let organizationRepository: OrganizationRepository; + + beforeEach(async () => { + session = new UserSession(); + await session.initialize(); + organizationRepository = new OrganizationRepository(); + }); + + it('should return a number as response when required api service level exists on organization for feature', async () => { + await organizationRepository.update( + { _id: session.organization._id }, + { + apiServiceLevel: ApiServiceLevelEnum.BUSINESS, + } + ); + const { body } = await session.testAgent.get(path).set('authorization', `ApiKey ${session.apiKey}`).expect(200); + expect(typeof body.data.number === 'number').to.be.true; + }); + + it('should return a 402 response when required api service level does not exists on organization for feature', async () => { + const { body } = await session.testAgent.get(path).set('authorization', `ApiKey ${session.apiKey}`).expect(402); + expect(body).to.deep.equal({ statusCode: 402, message: 'Payment Required' }); + }); +}); diff --git a/apps/api/src/app/testing/testing.controller.ts b/apps/api/src/app/testing/testing.controller.ts index d46e4b06851..ee802b935d9 100644 --- a/apps/api/src/app/testing/testing.controller.ts +++ b/apps/api/src/app/testing/testing.controller.ts @@ -1,6 +1,6 @@ import { Body, Controller, Get, HttpException, NotFoundException, Post, UseGuards } from '@nestjs/common'; import { DalService } from '@novu/dal'; -import { IUserEntity } from '@novu/shared'; +import { IUserEntity, ProductFeatureKeyEnum } from '@novu/shared'; import { ISeedDataResponseDto, SeedDataBodyDto } from './dtos/seed-data.dto'; import { IdempotencyBodyDto } from './dtos/idempotency.dto'; @@ -11,6 +11,7 @@ import { CreateSessionCommand } from './usecases/create-session/create-session.c import { ApiExcludeController } from '@nestjs/swagger'; import { UserAuthGuard } from '../auth/framework/user.auth.guard'; import { ExternalApiAccessible } from '../auth/framework/external-api.decorator'; +import { ProductFeature } from '../shared/decorators/product-feature.decorator'; @Controller('/testing') @ApiExcludeController() @@ -75,4 +76,14 @@ export class TestingController { return { number: Math.random() }; } + + @ExternalApiAccessible() + @UseGuards(UserAuthGuard) + @Get('/product-feature') + @ProductFeature(ProductFeatureKeyEnum.TRANSLATIONS) + async productFeatureGet(): Promise<{ number: number }> { + if (process.env.NODE_ENV !== 'test') throw new NotFoundException(); + + return { number: Math.random() }; + } } diff --git a/apps/api/src/app/testing/translations/create-translation.e2e-ee.ts b/apps/api/src/app/testing/translations/create-translation.e2e-ee.ts index e5209f38a32..621f3310c69 100644 --- a/apps/api/src/app/testing/translations/create-translation.e2e-ee.ts +++ b/apps/api/src/app/testing/translations/create-translation.e2e-ee.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; describe('Create translation group - /translations/groups (POST)', async () => { let session: UserSession; - before(async () => { + beforeEach(async () => { session = new UserSession(); await session.initialize(); await session.testAgent.put(`/v1/organizations/language`).send({ @@ -45,6 +45,41 @@ describe('Create translation group - /translations/groups (POST)', async () => { expect(group.identifier).to.eq('test'); expect(locales).to.deep.eq(['en_US']); }); + it('should promote creation of default locale translation after translation group promotion', async () => { + const result = await session.testAgent.post(`/v1/translations/groups`).send({ + name: 'test', + identifier: 'test', + locales: ['en_US', 'sv_SE'], + }); + + let group = result.body.data; + const id = group.id; + + expect(group.name).to.eq('test'); + expect(group.identifier).to.eq('test'); + + let data = await session.testAgent.get(`/v1/translations/groups/test`).send(); + group = data.body.data; + let locales = group.translations.map((t) => t.isoLanguage); + + expect(group.name).to.eq('test'); + expect(group.identifier).to.eq('test'); + expect(locales).to.deep.eq(['en_US', 'sv_SE']); + expect(id).to.equal(group.id); + + await session.applyChanges({ + enabled: false, + _entityId: group.id, + }); + await session.switchToProdEnvironment(); + + data = await session.testAgent.get(`/v1/translations/groups/test`).send(); + group = data.body.data; + locales = group.translations.map((t) => t.isoLanguage); + expect(group.name).to.eq('test'); + expect(group.identifier).to.eq('test'); + expect(locales).to.deep.eq(['en_US']); + }); it('should check that default locale is included in group else add it', async () => { const result = await session.testAgent.post(`/v1/translations/groups`).send({ diff --git a/apps/api/src/app/widgets/usecases/get-organization-data/get-organization-data.usecase.ts b/apps/api/src/app/widgets/usecases/get-organization-data/get-organization-data.usecase.ts index a95009c0f29..7688a2c6c0f 100644 --- a/apps/api/src/app/widgets/usecases/get-organization-data/get-organization-data.usecase.ts +++ b/apps/api/src/app/widgets/usecases/get-organization-data/get-organization-data.usecase.ts @@ -8,7 +8,7 @@ export class GetOrganizationData { constructor(private organizationRepository: OrganizationRepository) {} async execute(command: GetOrganizationDataCommand): Promise { - const organization = await this.organizationRepository.findOrganizationById(command.organizationId); + const organization = await this.organizationRepository.findById(command.organizationId); if (!organization) { throw new NotFoundException(`Organization with id ${command.organizationId} not found`); } diff --git a/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts b/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts index 67911890fb4..dcd7885392b 100644 --- a/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts +++ b/apps/api/src/app/workflows/dto/create-workflow.request.dto.ts @@ -1,6 +1,11 @@ import { IsArray, IsBoolean, IsDefined, IsOptional, IsString, MaxLength, ValidateNested } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ICreateWorkflowDto, IPreferenceChannels, NotificationTemplateCustomData } from '@novu/shared'; +import { + ICreateWorkflowDto, + INotificationGroup, + IPreferenceChannels, + NotificationTemplateCustomData, +} from '@novu/shared'; import { PreferenceChannels } from '../../shared/dtos/preference-channels'; import { NotificationStep } from '../../shared/dtos/notification-step'; @@ -18,6 +23,10 @@ export class CreateWorkflowRequestDto implements ICreateWorkflowDto { }) notificationGroupId: string; + @ApiProperty() + @IsOptional() + notificationGroup?: INotificationGroup; + @ApiPropertyOptional() @IsOptional() @IsArray() diff --git a/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts b/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts index d3e02c6443c..d7edff9a121 100644 --- a/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts +++ b/apps/api/src/app/workflows/e2e/create-notification-templates.e2e.ts @@ -21,8 +21,8 @@ import { MessageTemplateRepository, EnvironmentRepository, SubscriberEntity, - NotificationGroupRepository, OrganizationRepository, + NotificationTemplateEntity, } from '@novu/dal'; import { isSameDay } from 'date-fns'; import { CreateWorkflowRequestDto } from '../dto'; @@ -509,7 +509,6 @@ describe('Create Notification template from blueprint - /notification-templates let session: UserSession; const notificationTemplateRepository: NotificationTemplateRepository = new NotificationTemplateRepository(); const environmentRepository: EnvironmentRepository = new EnvironmentRepository(); - const notificationGroupRepository: NotificationGroupRepository = new NotificationGroupRepository(); const organizationRepository: OrganizationRepository = new OrganizationRepository(); before(async () => { @@ -548,11 +547,8 @@ describe('Create Notification template from blueprint - /notification-templates const blueprint = (await session.testAgent.get(`/v1/blueprints/${blueprintId}`).send()).body.data; const blueprintOrg = await organizationRepository.create({ name: 'Blueprint Org' }); process.env.BLUEPRINT_CREATOR = blueprintOrg._id; - const group = await notificationGroupRepository.create({ - _organizationId: blueprintOrg._id, - name: 'Test name', - }); - blueprint.notificationGroupId = group._id; + blueprint.notificationGroupId = blueprint._notificationGroupId; + blueprint.notificationGroup.name = 'New Group Name'; blueprint.blueprintId = blueprint._id; const noChanges = (await session.testAgent.get(`/v1/changes?promoted=false`)).body.data; @@ -564,6 +560,22 @@ describe('Create Notification template from blueprint - /notification-templates expect(newWorkflowChanges[1].type).to.equal(ChangeEntityTypeEnum.NOTIFICATION_GROUP); }); + it('should create workflow from blueprint (full blueprint mock)', async function () { + const createdTemplate: NotificationTemplateEntity = ( + await session.testAgent.post(`/v1/workflows`).send(blueprintTemplateMock) + ).body.data; + + expect(createdTemplate.blueprintId).to.equal(blueprintTemplateMock.blueprintId); + expect(createdTemplate.isBlueprint).to.equal(false); + expect(createdTemplate.name).to.equal(blueprintTemplateMock.name); + expect(createdTemplate.steps.length).to.equal(blueprintTemplateMock.steps.length); + expect(createdTemplate._notificationGroupId).to.not.equal(blueprintTemplateMock.notificationGroupId); + + const inAppStep = createdTemplate.steps.find((step) => step.template?.type === StepTypeEnum.IN_APP); + + expect(inAppStep?.template?._feedId).to.be.equal(null); + }); + async function getProductionEnvironment() { return await environmentRepository.findOne({ _parentId: session.environment._id, @@ -663,3 +675,277 @@ export async function createTemplateFromBlueprint({ createdTemplate, }; } + +const blueprintTemplateMock = { + // _id: '64731d4e1084f5a48293ceab', + blueprintId: '64731d4e1084f5a48293ceab', + name: 'Mention in a comment', + active: true, + draft: false, + critical: false, + isBlueprint: true, + notificationGroupId: '64731d4e1084f5a48293ce85', + tags: [], + triggers: [ + { + type: 'event', + identifier: 'fa-solid-fa-comment-mention-in-a-comment', + variables: [ + { + name: 'commenterName', + type: 'String', + _id: '65ee069a319fc6a92cf436d5', + }, + { + name: 'commentSnippet', + type: 'String', + _id: '65ee069a319fc6a92cf436d6', + }, + { + name: 'commentLink', + type: 'String', + _id: '65ee069a319fc6a92cf436d7', + }, + ], + reservedVariables: [], + subscriberVariables: [ + { + name: 'email', + _id: '65ee069a319fc6a92cf436d4', + }, + ], + _id: '64731d1c1084f5a48293cd4a', + }, + ], + steps: [ + { + active: true, + shouldStopOnFail: false, + uuid: 'b6944995-a283-46bd-b55a-18625fd1d4fd', + name: 'In-App', + type: 'REGULAR', + filters: [ + { + children: [], + _id: '6485b9052a50bb49867584a0', + }, + ], + _templateId: '6485b92e2a50bb4986758656', + _parentId: null, + metadata: { + timed: { + weekDays: [], + monthDays: [], + }, + }, + variants: [], + _id: '6485b9052a50bb498675846d', + template: { + _id: '6485b92e2a50bb4986758656', + type: 'in_app', + active: true, + subject: '', + variables: [ + { + name: 'commenterName', + type: 'String', + required: false, + _id: '6485b9052a50bb498675846e', + }, + { + name: 'commentSnippet', + type: 'String', + required: false, + _id: '6485b9052a50bb498675846f', + }, + ], + content: '{{commenterName}} has mentioned you in "{{commentSnippet}}" ', + contentType: 'editor', + cta: { + data: { + url: '', + }, + type: 'redirect', + }, + _environmentId: '64731b391084f5a48293cb87', + _organizationId: '64731b391084f5a48293cb5b', + _creatorId: '64731b331084f5a48293cb52', + _parentId: '6485b9052a50bb498675846d', + _layoutId: null, + _feedId: '64731b331084f5a48293cb52', + feedId: '64731b331084f5a48293cb52', + deleted: false, + createdAt: '2023-06-11T12:08:14.446Z', + updatedAt: '2024-03-10T19:14:45.347Z', + __v: 0, + actor: { + type: 'none', + data: null, + }, + }, + }, + { + active: true, + shouldStopOnFail: false, + uuid: '642e42b5-51e6-4d3b-8a91-067c29e902d4', + name: 'Digest', + type: 'REGULAR', + filters: [], + _templateId: '6485b92e2a50bb4986758662', + _parentId: '6485b9052a50bb498675846d', + metadata: { + amount: 30, + unit: 'minutes', + type: 'regular', + backoffUnit: 'minutes', + backoffAmount: 5, + backoff: true, + timed: { + weekDays: [], + monthDays: [], + }, + }, + variants: [], + _id: '6485b9052a50bb4986758479', + template: { + _id: '6485b92e2a50bb4986758662', + type: 'digest', + active: true, + subject: '', + variables: [], + content: '', + contentType: 'editor', + _environmentId: '64731b391084f5a48293cb87', + _organizationId: '64731b391084f5a48293cb5b', + _creatorId: '64731b331084f5a48293cb52', + _parentId: '6485b9052a50bb4986758479', + _layoutId: null, + deleted: false, + createdAt: '2023-06-11T12:08:14.520Z', + updatedAt: '2024-03-10T19:14:45.377Z', + __v: 0, + }, + }, + { + active: true, + replyCallback: { + active: true, + url: 'https://webhook.com/reply-callback', + }, + shouldStopOnFail: false, + uuid: '671d86ec-dc27-413c-a666-ec4aeb191691', + name: 'Email', + type: 'REGULAR', + filters: [ + { + value: 'AND', + children: [ + { + operator: 'EQUAL', + on: 'previousStep', + step: 'b6944995-a283-46bd-b55a-18625fd1d4fd', + stepType: 'unseen', + _id: '6485b9052a50bb49867584a4', + }, + ], + _id: '6485b9052a50bb49867584a3', + }, + ], + _templateId: '6485b92e2a50bb4986758671', + _parentId: '6485b9052a50bb4986758479', + metadata: { + timed: { + weekDays: [], + monthDays: [], + }, + }, + variants: [], + _id: '6485b9052a50bb4986758481', + template: { + _id: '6485b92e2a50bb4986758671', + type: 'email', + active: true, + subject: '{{mentionedUser}} mention you in {{resourceName}}', + variables: [ + { + name: 'mentionedUser', + type: 'String', + required: false, + _id: '6485b9052a50bb4986758482', + }, + { + name: 'resourceName', + type: 'String', + required: false, + _id: '6485b9052a50bb4986758483', + }, + { + name: 'commentLink', + type: 'String', + required: false, + _id: '6485b9052a50bb4986758484', + }, + { + name: 'step.digest', + type: 'Boolean', + required: false, + defaultValue: true, + _id: '6485b9052a50bb4986758485', + }, + { + name: 'step.events.0.mentionedUser', + type: 'String', + required: false, + _id: '6485b9052a50bb4986758486', + }, + { + name: 'step.total_count', + type: 'String', + required: false, + _id: '6485b9052a50bb4986758487', + }, + ], + content: + '{{#if step.digest}}\n {{step.events.0.mentionedUser}} and {{step.total_count}} others mentioned you in a comment. \n{{else}}\n {{mentionedUser}} mentioned you in a comment. \n{{/if}}\n \n

\n\n\n\n
\n\n{{#unless step.digest}}\n You can reply to this email, and the email contents will be posted as a comment reply to this post.\n{{/unless}}\n', + contentType: 'customHtml', + _environmentId: '64731b391084f5a48293cb87', + _organizationId: '64731b391084f5a48293cb5b', + _creatorId: '64731b331084f5a48293cb52', + _parentId: '6485b9052a50bb4986758481', + _layoutId: '64731d4e1084f5a48293ce8f', + deleted: false, + createdAt: '2023-06-11T12:08:14.551Z', + updatedAt: '2024-03-10T19:14:45.409Z', + __v: 0, + preheader: '', + senderName: '', + }, + }, + ], + preferenceSettings: { + email: true, + sms: true, + in_app: true, + chat: true, + push: true, + }, + _environmentId: '64731b391084f5a48293cb87', + _organizationId: '64731b391084f5a48293cb5b', + _creatorId: '64731b331084f5a48293cb52', + _parentId: '64731d1c1084f5a48293cd49', + deleted: false, + createdAt: '2023-05-28T09:22:22.586Z', + updatedAt: '2024-03-10T19:14:45.442Z', + __v: 0, + deletedAt: '2023-05-30T12:55:34.842Z', + notificationGroup: { + _id: '64731d4e1084f5a48293ce85', + name: 'General', + _organizationId: '64731b391084f5a48293cb5b', + _environmentId: '64731b391084f5a48293cb87', + _parentId: '64731b391084f5a48293cb65', + createdAt: '2023-05-28T09:22:22.381Z', + updatedAt: '2023-05-28T09:22:22.381Z', + __v: 0, + }, +}; diff --git a/apps/api/src/app/workflows/notification-template.controller.ts b/apps/api/src/app/workflows/notification-template.controller.ts index 444d3ddb1a5..2a5f49622ea 100644 --- a/apps/api/src/app/workflows/notification-template.controller.ts +++ b/apps/api/src/app/workflows/notification-template.controller.ts @@ -178,6 +178,7 @@ export class NotificationTemplateController { description: body.description, steps: body.steps, notificationGroupId: body.notificationGroupId, + notificationGroup: body.notificationGroup, active: body.active ?? false, draft: !body.active, critical: body.critical ?? false, diff --git a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.command.ts b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.command.ts index a264390d2b6..7f18fc51c5d 100644 --- a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.command.ts +++ b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.command.ts @@ -17,6 +17,7 @@ import { FilterParts, IWorkflowStepMetadata, NotificationTemplateCustomData, + INotificationGroup, } from '@novu/shared'; import { EnvironmentWithUserCommand } from '../../../shared/commands/project.command'; @@ -32,6 +33,9 @@ export class CreateNotificationTemplateCommand extends EnvironmentWithUserComman @IsDefined() notificationGroupId: string; + @IsOptional() + notificationGroup?: INotificationGroup; + @IsOptional() @IsArray() tags: string[]; diff --git a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts index d28447eab7e..d688aebef52 100644 --- a/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts +++ b/apps/api/src/app/workflows/usecases/create-notification-template/create-notification-template.usecase.ts @@ -15,11 +15,14 @@ import { INotificationTrigger, TriggerTypeEnum, IStepVariant, - StepTypeEnum, } from '@novu/shared'; import { AnalyticsService, CreateChange, CreateChangeCommand, PlatformException } from '@novu/application-generic'; -import { CreateNotificationTemplateCommand, NotificationStepVariant } from './create-notification-template.command'; +import { + CreateNotificationTemplateCommand, + NotificationStep, + NotificationStepVariant, +} from './create-notification-template.command'; import { ContentService } from '../../../shared/helpers/content.service'; import { CreateMessageTemplate, CreateMessageTemplateCommand } from '../../../message-template/usecases'; import { ApiException } from '../../../shared/exceptions/api.exception'; @@ -325,8 +328,8 @@ export class CreateNotificationTemplate { private async processBlueprint(command: CreateNotificationTemplateCommand) { if (!command.blueprintId) return null; - const group: NotificationGroupEntity = await this.handleGroup(command.notificationGroupId, command); - const steps: NotificationStepEntity[] = await this.handleFeeds(command.steps as any, command); + const group: NotificationGroupEntity = await this.handleGroup(command); + const steps: NotificationStep[] = this.normalizeSteps(command.steps); return CreateNotificationTemplateCommand.create({ organizationId: command.organizationId, @@ -346,6 +349,22 @@ export class CreateNotificationTemplate { }); } + private normalizeSteps(commandSteps: NotificationStep[]): NotificationStep[] { + const steps = JSON.parse(JSON.stringify(commandSteps)) as NotificationStep[]; + + return steps.map((step) => { + const template = step.template; + if (template) { + template.feedId = undefined; + } + + return { + ...step, + ...(template ? { template } : {}), + }; + }); + } + private async handleFeeds( steps: NotificationStepEntity[], command: CreateNotificationTemplateCommand @@ -400,34 +419,25 @@ export class CreateNotificationTemplate { return steps; } - private async handleGroup( - notificationGroupId: string, - command: CreateNotificationTemplateCommand - ): Promise { - const blueprintNotificationGroup = await this.notificationGroupRepository.findOne({ - _id: notificationGroupId, - _organizationId: this.getBlueprintOrganizationId, - }); - - if (!blueprintNotificationGroup) - throw new NotFoundException(`Blueprint workflow group with id ${notificationGroupId} is not found`); + private async handleGroup(command: CreateNotificationTemplateCommand): Promise { + if (!command.notificationGroup?.name) throw new NotFoundException(`Notification group was not provided`); - let group = await this.notificationGroupRepository.findOne({ - name: blueprintNotificationGroup.name, + let notificationGroup = await this.notificationGroupRepository.findOne({ + name: command.notificationGroup.name, _environmentId: command.environmentId, _organizationId: command.organizationId, }); - if (!group) { - group = await this.notificationGroupRepository.create({ + if (!notificationGroup) { + notificationGroup = await this.notificationGroupRepository.create({ _environmentId: command.environmentId, _organizationId: command.organizationId, - name: blueprintNotificationGroup.name, + name: command.notificationGroup.name, }); await this.createChange.execute( CreateChangeCommand.create({ - item: group, + item: notificationGroup, environmentId: command.environmentId, organizationId: command.organizationId, userId: command.userId, @@ -437,7 +447,7 @@ export class CreateNotificationTemplate { ); } - return group; + return notificationGroup; } private get getBlueprintOrganizationId(): string { return NotificationTemplateRepository.getBlueprintOrganizationId() as string; diff --git a/apps/api/src/app/workflows/workflow.controller.ts b/apps/api/src/app/workflows/workflow.controller.ts index cd43cdb3a13..33d6df134d7 100644 --- a/apps/api/src/app/workflows/workflow.controller.ts +++ b/apps/api/src/app/workflows/workflow.controller.ts @@ -198,6 +198,7 @@ export class WorkflowController { description: body.description, steps: body.steps, notificationGroupId: body.notificationGroupId, + notificationGroup: body.notificationGroup, active: body.active ?? false, draft: !body.active, critical: body.critical ?? false, diff --git a/apps/api/src/config/cors.spec.ts b/apps/api/src/config/cors.spec.ts index b820d802f87..422c4cee7fd 100644 --- a/apps/api/src/config/cors.spec.ts +++ b/apps/api/src/config/cors.spec.ts @@ -71,7 +71,7 @@ describe('CORS Configuration', () => { { url: '/v1/test', headers: { - host: 'https://test--' + process.env.PR_PREVIEW_ROOT_URL, + origin: 'https://test--' + process.env.PR_PREVIEW_ROOT_URL, }, }, callbackSpy @@ -79,10 +79,7 @@ describe('CORS Configuration', () => { expect(callbackSpy.calledOnce).to.be.ok; expect(callbackSpy.firstCall.firstArg).to.be.null; - expect(callbackSpy.firstCall.lastArg.origin.length).to.equal(3); - expect(callbackSpy.firstCall.lastArg.origin[0]).to.equal('https://test.com'); - expect(callbackSpy.firstCall.lastArg.origin[1]).to.equal('https://widget.com'); - expect(callbackSpy.firstCall.lastArg.origin[2]).to.equal('*'); + expect(callbackSpy.firstCall.lastArg.origin).to.equal('*'); }); } }); diff --git a/apps/api/src/config/cors.ts b/apps/api/src/config/cors.ts index 0ff6af7cea6..6d16e5eeb82 100644 --- a/apps/api/src/config/cors.ts +++ b/apps/api/src/config/cors.ts @@ -1,4 +1,4 @@ -import { INestApplication, Request } from '@nestjs/common'; +import { INestApplication, Logger } from '@nestjs/common'; import { HttpRequestHeaderKeysEnum } from '../app/shared/framework/types'; export const corsOptionsDelegate: Parameters[0] = function (req: Request, callback) { @@ -10,24 +10,20 @@ export const corsOptionsDelegate: Parameters[0] methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], }; - const host = (req.headers as any)?.host || ''; + const origin = (req.headers as any)?.origin || ''; - if (['test', 'local'].includes(process.env.NODE_ENV) || isWidgetRoute(req.url) || isBlueprintRoute(req.url)) { + if ( + ['test', 'local'].includes(process.env.NODE_ENV) || + isWidgetRoute(req.url) || + isBlueprintRoute(req.url) || + hasPermittedDeployPreviewOrigin(origin) + ) { corsOptions.origin = '*'; } else { corsOptions.origin = [process.env.FRONT_BASE_URL]; if (process.env.WIDGET_BASE_URL) { corsOptions.origin.push(process.env.WIDGET_BASE_URL); } - - const shouldDisableCorsForPreviewUrls = - process.env.PR_PREVIEW_ROOT_URL && - process.env.NODE_ENV === 'dev' && - host.includes(process.env.PR_PREVIEW_ROOT_URL); - - if (shouldDisableCorsForPreviewUrls) { - corsOptions.origin.push('*'); - } } callback(null as unknown as Error, corsOptions); @@ -40,3 +36,18 @@ function isWidgetRoute(url: string) { function isBlueprintRoute(url: string) { return url.startsWith('/v1/blueprints'); } + +function hasPermittedDeployPreviewOrigin(origin: string) { + const shouldAllowOrigin = + process.env.PR_PREVIEW_ROOT_URL && + process.env.NODE_ENV === 'dev' && + origin.includes(process.env.PR_PREVIEW_ROOT_URL); + + Logger.verbose(`Should allow deploy preview? ${shouldAllowOrigin ? 'Yes' : 'No'}.`, { + curEnv: process.env.NODE_ENV, + previewUrlRoot: process.env.PR_PREVIEW_ROOT_URL, + origin, + }); + + return shouldAllowOrigin; +} diff --git a/apps/inbound-mail/package.json b/apps/inbound-mail/package.json index 16ddd077d4c..f5320150ab8 100644 --- a/apps/inbound-mail/package.json +++ b/apps/inbound-mail/package.json @@ -1,6 +1,6 @@ { "name": "@novu/inbound-mail", - "version": "0.24.0", + "version": "0.24.1", "description": "", "author": "", "private": true, @@ -19,8 +19,8 @@ "test": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --trace-warnings --timeout 10000 --require ts-node/register --exit --file e2e/setup.ts src/**/**/*.spec.ts" }, "dependencies": { - "@novu/application-generic": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/shared": "^0.24.1", "@sentry/node": "^7.12.1", "bluebird": "^2.9.30", "dotenv": "^8.6.0", @@ -39,7 +39,7 @@ "winston": "^3.9.0" }, "devDependencies": { - "@novu/testing": "^0.24.0", + "@novu/testing": "^0.24.1", "@types/chai": "^4.2.11", "@types/express": "^4.17.8", "@types/html-to-text": "^9.0.1", diff --git a/apps/web/.env.playwirght.test b/apps/web/.env.playwirght.test new file mode 100644 index 00000000000..ba5e5ef43c5 --- /dev/null +++ b/apps/web/.env.playwirght.test @@ -0,0 +1,3 @@ +NODE_ENV=test +MONGODB_URL=mongodb://127.0.0.1:27017/novu-test +API_URL=http://127.0.0.1:1336 diff --git a/apps/web/.eslintrc.js b/apps/web/.eslintrc.js index a882dc23378..4417ceb2f7a 100644 --- a/apps/web/.eslintrc.js +++ b/apps/web/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { env: { 'cypress/globals': true, }, - ignorePatterns: ['craco.config.js', 'cypress/*', '**/styled-system/**/*'], + ignorePatterns: ['craco.config.js', 'cypress/*', '**/styled-system/**/*', 'tests/*'], extends: ['plugin:cypress/recommended', '../../.eslintrc.js'], plugins: ['cypress', 'react-hooks'], parserOptions: { diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 44f85e781ec..3267e0dcdb1 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -29,4 +29,8 @@ cypress/screenshots ## Panda styled-system -styled-system-studio \ No newline at end of file +styled-system-studio +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/apps/web/config-overrides.js b/apps/web/config-overrides.js index 83cb5c10185..41c615a9c59 100644 --- a/apps/web/config-overrides.js +++ b/apps/web/config-overrides.js @@ -17,7 +17,7 @@ function overrideConfig(config, env) { plugins, ignoreWarnings: [ { - message: /Module not found: Error: Can't resolve \'@novu\/ee-translation-web\' .*/, + message: /Module not found: Error: Can't resolve \'@novu\/ee-.*\' .*/, }, ], }; diff --git a/apps/web/cypress/tests/activity-graph.spec.ts b/apps/web/cypress/tests/activity-graph.spec.ts index 09ac09f78f5..e9a2aa20d70 100644 --- a/apps/web/cypress/tests/activity-graph.spec.ts +++ b/apps/web/cypress/tests/activity-graph.spec.ts @@ -1,4 +1,8 @@ -describe('Activity page', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/activity-graph.spec.ts. + * @deprecated + */ +describe.skip('Activity page', function () { beforeEach(function () { // @ts-expect-error cy.initializeSession() diff --git a/apps/web/cypress/tests/auth.spec.ts b/apps/web/cypress/tests/auth.spec.ts index 2fd50d3207b..2c7b61b8577 100644 --- a/apps/web/cypress/tests/auth.spec.ts +++ b/apps/web/cypress/tests/auth.spec.ts @@ -162,7 +162,7 @@ describe('User Sign-up and Login', function () { cy.getByTestId('email').type('test-user-1@example.com'); cy.getByTestId('password').type('123456'); cy.getByTestId('submit-btn').click(); - cy.getByTestId('error-alert-banner').contains('Incorrect email or password provided'); + cy.get('.mantine-TextInput-error').contains('Incorrect email or password provided'); }); it('should show invalid email error when authenticating with invalid email', function () { @@ -180,7 +180,7 @@ describe('User Sign-up and Login', function () { cy.getByTestId('email').type('test-user-1@example.de'); cy.getByTestId('password').type('123456'); cy.getByTestId('submit-btn').click(); - cy.getByTestId('error-alert-banner').contains('Incorrect email or password provided'); + cy.get('.mantine-TextInput-error').contains('Incorrect email or password provided'); }); }); diff --git a/apps/web/cypress/tests/billing/annual-subscription.spec-ee.ts b/apps/web/cypress/tests/billing/annual-subscription.spec-ee.ts new file mode 100644 index 00000000000..8e42a807493 --- /dev/null +++ b/apps/web/cypress/tests/billing/annual-subscription.spec-ee.ts @@ -0,0 +1,88 @@ +/** cspell:disable */ +describe('Billing - Annual subscription', function () { + beforeEach(function () { + cy.initializeSession().as('session'); + + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: null, + trialEnd: null, + hasPaymentMethod: false, + status: 'active', + }, + }).as('getSubscription'); + }); + + it('should display monthly in modal as default', function () { + cy.intercept('POST', '**/billing/checkout', { + data: { + clientSecret: 'seti_1Mm8s8LkdIwHu7ix0OXBfTRG_secret_NXDICkPqPeiBTAFqWmkbff09lRmSVXe', + }, + }).as('checkout'); + + cy.visit('/settings/billing'); + + cy.getByTestId('plan-business-upgrade').click(); + cy.wait(['@checkout']); + + cy.getByTestId('billing-interval-control-monthly') + .last() + .parent() + .should('have.class', 'mantine-SegmentedControl-labelActive'); + + cy.getByTestId('modal-monthly-pricing').should('exist'); + }); + + it('should display annually if it is selected before open modal', function () { + cy.intercept('POST', '**/billing/checkout', { + data: { + clientSecret: 'seti_1Mm8s8LkdIwHu7ix0OXBfTRG_secret_NXDICkPqPeiBTAFqWmkbff09lRmSVXe', + }, + }).as('checkout'); + + cy.visit('/settings/billing'); + + cy.getByTestId('billing-interval-control-annually').click(); + + cy.getByTestId('plan-business-upgrade').click(); + cy.wait(['@checkout']); + + cy.getByTestId('billing-interval-control-annually') + .last() + .parent() + .should('have.class', 'mantine-SegmentedControl-labelActive'); + + cy.getByTestId('modal-anually-pricing').should('exist'); + }); + + it('should display billing page with billing interval control', function () { + cy.intercept('GET', '**/v1/organizations', (request) => { + request.reply((response) => { + if (!response.body.data) { + return response; + } + + response.body['data'] = [ + { + ...response.body.data[0], + apiServiceLevel: 'free', + }, + ]; + return response; + }); + }).as('organizations'); + + cy.visit('/settings/billing'); + + cy.getByTestId('billing-interval-control').should('exist'); + cy.getByTestId('billing-interval-price').should('have.text', '$250 month package / billed monthly'); + cy.getByTestId('billing-interval-control-annually').click(); + cy.getByTestId('billing-interval-control-annually') + .parent() + .should('have.class', 'mantine-SegmentedControl-labelActive'); + cy.getByTestId('billing-interval-price').should( + 'have.text', + `$${(2700).toLocaleString()} year package / billed annually` + ); + }); +}); diff --git a/apps/web/cypress/tests/billing/billing.spec-ee.ts b/apps/web/cypress/tests/billing/billing.spec-ee.ts new file mode 100644 index 00000000000..0a6d8582a1f --- /dev/null +++ b/apps/web/cypress/tests/billing/billing.spec-ee.ts @@ -0,0 +1,262 @@ +import { addDays, subDays, startOfDay, endOfDay } from 'date-fns'; + +describe('Billing', function () { + beforeEach(function () { + cy.initializeSession().as('session'); + }); + + it('should display billing page', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: null, + trialEnd: null, + hasPaymentMethod: false, + status: 'active', + }, + }).as('getSubscription'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + + cy.getByTestId('plan-title').should('have.text', 'Plans'); + }); + + it('should display free trial widget', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: startOfDay(new Date()), + trialEnd: addDays(endOfDay(new Date()), 30), + hasPaymentMethod: false, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.visit('/workflows'); + + cy.wait(['@getSubscription']); + cy.getByTestId('free-trial-widget-text').should('have.text', '30 days left on your Business trial'); + cy.getByTestId('free-trial-widget-button').should('have.text', 'Upgrade to Business'); + cy.getByTestId('free-trial-widget-progress').find('.mantine-Progress-bar').should('have.css', 'width', '0px'); + }); + + it('should display free trial widget after 10 days', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: subDays(startOfDay(new Date()), 10), + trialEnd: addDays(endOfDay(new Date()), 20), + hasPaymentMethod: false, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.visit('/workflows'); + + cy.wait(['@getSubscription']); + cy.getByTestId('free-trial-widget-text').should('have.text', '20 days left on your Business trial'); + cy.getByTestId('free-trial-widget-button').should('have.text', 'Upgrade to Business'); + cy.getByTestId('free-trial-widget-progress') + .find('.mantine-Progress-bar') + .should('have.css', 'background-color', 'rgb(77, 153, 128)'); + }); + + it('should display free trial widget after 20 days', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: subDays(startOfDay(new Date()), 20), + trialEnd: addDays(endOfDay(new Date()), 10), + hasPaymentMethod: false, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.visit('/workflows'); + + cy.wait(['@getSubscription']); + cy.getByTestId('free-trial-widget-text').should('have.text', '10 days left on your Business trial'); + cy.getByTestId('free-trial-widget-button').should('have.text', 'Upgrade to Business'); + cy.getByTestId('free-trial-widget-progress') + .find('.mantine-Progress-bar') + .should('have.css', 'background-color', 'rgb(253, 224, 68)'); + cy.getByTestId('free-trial-banner').should('exist'); + cy.getByTestId('free-trial-banner-upgrade').should('have.text', 'Upgrade'); + cy.getByTestId('free-trial-banner-contact-sales').should('have.text', 'Contact sales'); + }); + + it('should not display free trial widget after 30 days', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: null, + trialEnd: null, + hasPaymentMethod: false, + status: 'active', + }, + }).as('getSubscription'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + cy.getByTestId('free-trial-widget-text').should('not.exist'); + cy.getByTestId('free-trial-plan-widget').should('not.exist'); + }); + + it('should display free trail info on billing page', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: startOfDay(new Date()), + trialEnd: addDays(endOfDay(new Date()), 30), + hasPaymentMethod: false, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.intercept('GET', '**/v1/organizations', (request) => { + request.reply((response) => { + if (!response.body.data) { + return response; + } + + response.body['data'] = [ + { + ...response.body.data[0], + apiServiceLevel: 'business', + }, + ]; + return response; + }); + }).as('organizations'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + + cy.getByTestId('plan-title').should('have.text', 'Plans'); + cy.getByTestId('free-trial-plan-widget').should('have.text', '30 days left on your trial'); + cy.getByTestId('plan-business-current').should('exist'); + cy.getByTestId('plan-business-add-payment').should('exist'); + }); + + it('should be able to manage subscription', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: null, + trialEnd: null, + hasPaymentMethod: true, + status: 'active', + }, + }).as('getSubscription'); + + cy.intercept('GET', '**/v1/organizations', (request) => { + request.reply((response) => { + if (!response.body.data) { + return response; + } + + response.body['data'] = [ + { + ...response.body.data[0], + apiServiceLevel: 'business', + }, + ]; + return response; + }); + }).as('organizations'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + + cy.getByTestId('plan-title').should('have.text', 'Plans'); + cy.getByTestId('plan-business-current').should('exist'); + cy.getByTestId('plan-business-manage').should('exist'); + }); + + it('should be able to upgrade from free', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: null, + trialEnd: null, + hasPaymentMethod: null, + status: 'active', + }, + }).as('getSubscription'); + + cy.intercept('GET', '**/v1/organizations', (request) => { + request.reply((response) => { + if (!response.body.data) { + return response; + } + + response.body['data'] = [ + { + ...response.body.data[0], + apiServiceLevel: 'free', + }, + ]; + return response; + }); + }).as('organizations'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + + cy.getByTestId('plan-title').should('have.text', 'Plans'); + cy.getByTestId('plan-free-current').should('exist'); + cy.getByTestId('plan-business-upgrade').should('exist'); + }); + + it('should display billing page', function () { + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: subDays(startOfDay(new Date()), 20), + trialEnd: addDays(endOfDay(new Date()), 10), + hasPaymentMethod: false, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.intercept('GET', '**/v1/organizations', (request) => { + request.reply((response) => { + if (!response.body.data) { + return response; + } + + response.body['data'] = [ + { + ...response.body.data[0], + apiServiceLevel: 'business', + }, + ]; + return response; + }); + }).as('organizations'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + + cy.getByTestId('plan-title').should('have.text', 'Plans'); + cy.getByTestId('plan-business-current').should('exist'); + cy.getByTestId('plan-business-add-payment').should('exist'); + cy.getByTestId('free-trial-plan-widget').should('have.text', '10 days left on your trial'); + cy.getByTestId('free-trial-widget-text').should('have.text', '10 days left on your Business trial'); + cy.getByTestId('free-trial-banner').should('exist'); + + cy.intercept('GET', '**/v1/billing/subscription', { + data: { + trialStart: startOfDay(new Date()), + trialEnd: addDays(endOfDay(new Date()), 30), + hasPaymentMethod: true, + status: 'trialing', + }, + }).as('getSubscription'); + + cy.visit('/settings/billing'); + + cy.wait(['@getSubscription']); + cy.getByTestId('free-trial-plan-widget').should('not.exist'); + cy.getByTestId('free-trial-widget-text').should('not.exist'); + cy.getByTestId('free-trial-banner').should('not.exist'); + }); +}); diff --git a/apps/web/cypress/tests/digest-playground.spec.ts b/apps/web/cypress/tests/digest-playground.spec.ts index 58fbeca2cfe..5ffff1d51bd 100644 --- a/apps/web/cypress/tests/digest-playground.spec.ts +++ b/apps/web/cypress/tests/digest-playground.spec.ts @@ -1,4 +1,8 @@ -describe('Digest Playground Workflow Page', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/digest-playground.spec.ts. + * @deprecated + */ +describe.skip('Digest Playground Workflow Page', function () { beforeEach(function () { cy.initializeSession({ noTemplates: true }).as('session'); }); @@ -16,7 +20,7 @@ describe('Digest Playground Workflow Page', function () { cy.url().should('include', '/digest-playground'); cy.contains('Digest Workflow Playground'); - cy.get('a[href="https://docs.novu.co/workflows/digest?utm_campaign=in-app"]').contains('Learn more in docs'); + cy.get('a[href^="https://docs.novu.co/workflows/digest"]').contains('Learn more in docs'); }); it('the set up digest workflow should redirect to template edit page', function () { diff --git a/apps/web/cypress/tests/integrations-list-modal.spec.ts b/apps/web/cypress/tests/integrations-list-modal.spec.ts index e423c9b615b..fe8c2b4e0d5 100644 --- a/apps/web/cypress/tests/integrations-list-modal.spec.ts +++ b/apps/web/cypress/tests/integrations-list-modal.spec.ts @@ -18,7 +18,11 @@ Cypress.on('window:before:load', (win) => { win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); -describe('Integrations List Modal', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/integrations-list-modal.spec.ts. + * @deprecated + */ +describe.skip('Integrations List Modal', function () { let session: any; beforeEach(function () { @@ -773,7 +777,7 @@ describe('Integrations List Modal', function () { 'Select a framework to set up credentials to start sending notifications.' ); cy.getByTestId('update-provider-sidebar') - .find('a[href="https://docs.novu.co/notification-center/introduction?utm_campaign=in-app"]') + .find('a[href^="https://docs.novu.co/notification-center/introduction"]') .contains('Explore set-up guide'); cy.getByTestId('is_active_id').should('have.value', 'true'); cy.window().then((win) => { diff --git a/apps/web/cypress/tests/integrations-list-page.spec.ts b/apps/web/cypress/tests/integrations-list-page.spec.ts index 0d0fe14524a..57a31fa54bb 100644 --- a/apps/web/cypress/tests/integrations-list-page.spec.ts +++ b/apps/web/cypress/tests/integrations-list-page.spec.ts @@ -18,7 +18,11 @@ Cypress.on('window:before:load', (win) => { win.isDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; }); -describe('Integrations List Page', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/integrations-list-page.spec.ts. + * @deprecated + */ +describe.skip('Integrations List Page', function () { let session: any; beforeEach(function () { @@ -924,7 +928,7 @@ describe('Integrations List Page', function () { 'Select a framework to set up credentials to start sending notifications.' ); cy.getByTestId('update-provider-sidebar') - .find('a[href="https://docs.novu.co/notification-center/introduction?utm_campaign=in-app"]') + .find('a[href^="https://docs.novu.co/notification-center/introduction"]') .contains('Explore set-up guide'); cy.getByTestId('is_active_id').should('have.value', 'true'); cy.window().then((win) => { diff --git a/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts b/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts index 4a6856c83fa..12a5af166fd 100644 --- a/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts +++ b/apps/web/cypress/tests/notification-editor/main-functionality.spec.ts @@ -1,6 +1,10 @@ import { addAndEditChannel, clickWorkflow, dragAndDrop, editChannel, fillBasicNotificationDetails, goBack } from '.'; -describe('Workflow Editor - Main Functionality', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/main-functionality.spec.ts. + * @deprecated + */ +describe.skip('Workflow Editor - Main Functionality', function () { beforeEach(function () { cy.initializeSession().as('session'); }); diff --git a/apps/web/cypress/tests/start-from-scratch-tour.spec.ts b/apps/web/cypress/tests/start-from-scratch-tour.spec.ts index dbcdd76d6a7..ace6acd53fd 100644 --- a/apps/web/cypress/tests/start-from-scratch-tour.spec.ts +++ b/apps/web/cypress/tests/start-from-scratch-tour.spec.ts @@ -1,4 +1,8 @@ -describe('Start from scratch tour hints', function () { +/** + * The tests from this file were moved to the corresponding Playwright file apps/web/tests/start-from-scratch-tour.spec.ts. + * @deprecated + */ +describe.skip('Start from scratch tour hints', function () { beforeEach(function () { cy.initializeSession({ showOnBoardingTour: true }).as('session'); }); diff --git a/apps/web/cypress/tests/templates-store.spec.ts b/apps/web/cypress/tests/templates-store.spec.ts index 5c03d09100b..6cf7397eb6f 100644 --- a/apps/web/cypress/tests/templates-store.spec.ts +++ b/apps/web/cypress/tests/templates-store.spec.ts @@ -12,6 +12,7 @@ describe('Templates Store', function () { }; beforeEach(function () { + cy.mockFeatureFlags({ IS_TEMPLATE_STORE_ENABLED: true }); cy.initializeSession({ noTemplates: true }).as('session'); indexedDB.deleteDatabase('localforage'); }); diff --git a/apps/web/package.json b/apps/web/package.json index 6b195f7d5be..c2770d7b481 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,9 +1,9 @@ { "name": "@novu/web", - "version": "0.24.0", + "version": "0.24.1", "private": true, "scripts": { - "start": "pnpm panda --watch & cross-env PORT=4200 react-app-rewired start", + "start": "pnpm panda --watch & cross-env NODE_OPTIONS=--max_old_space_size=8192 PORT=4200 react-app-rewired start", "prebuild": "rimraf build", "build": "pnpm panda && cross-env NODE_OPTIONS=--max_old_space_size=4096 GENERATE_SOURCEMAP=false react-app-rewired --max_old_space_size=4096 build", "precommit": "lint-staged", @@ -19,6 +19,12 @@ "cypress:install": "cypress install", "cypress:open": "cross-env NODE_ENV=test cypress open", "cypress:run:components": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component", + "playwright:install": "playwright install --with-deps", + "playwright:test": "playwright test", + "playwright:test-ui": "playwright test --ui", + "playwright:codegen": "playwright codegen", + "playwright:show-report": "npx playwright show-report", + "playwright:merge-report": "playwright merge-reports --reporter html", "start:api": "cd ../../ && pnpm start:api:test", "storybook": "storybook dev -p 6006 -s public", "build-storybook": "storybook build -s public", @@ -53,10 +59,10 @@ "@mantine/prism": "^5.7.1", "@mantine/spotlight": "^5.7.1", "@monaco-editor/react": "^4.6.0", - "@novu/design-system": "^0.24.0", - "@novu/notification-center": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/shared-web": "^0.24.0", + "@novu/design-system": "^0.24.1", + "@novu/notification-center": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/shared-web": "^0.24.1", "@rive-app/react-canvas": "^4.8.1", "@segment/analytics-next": "^1.48.0", "@sentry/react": "^7.40.0", @@ -125,8 +131,9 @@ "zod": "^3.22.4" }, "optionalDependencies": { - "@novu/ee-billing-web": "^0.24.0", - "@novu/ee-translation-web": "^0.24.0" + "@novu/ee-billing-web": "^0.24.1", + "@novu/ee-echo-web": "^0.24.1", + "@novu/ee-translation-web": "^0.24.1" }, "devDependencies": { "@babel/polyfill": "^7.12.1", @@ -134,9 +141,10 @@ "@babel/preset-react": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "@babel/runtime": "^7.20.13", - "@novu/dal": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/dal": "^0.24.1", + "@novu/testing": "^0.24.1", "@pandacss/dev": "^0.34.0", + "@playwright/test": "^1.42.1", "@storybook/addon-actions": "^7.4.2", "@storybook/addon-essentials": "^7.4.2", "@storybook/addon-links": "^7.4.2", @@ -164,8 +172,8 @@ "storybook": "^7.4.2", "typescript": "4.9.5", "webpack": "5.78.0", - "webpack-dev-server": "4.11.1", - "webpack-bundle-analyzer": "^4.9.0" + "webpack-bundle-analyzer": "^4.9.0", + "webpack-dev-server": "4.11.1" }, "browserslist": { "production": [ diff --git a/apps/web/panda.config.ts b/apps/web/panda.config.ts index d3db549f984..8c6d171c36e 100644 --- a/apps/web/panda.config.ts +++ b/apps/web/panda.config.ts @@ -38,6 +38,9 @@ export default defineConfig({ // The output directory for your css system outdir: './src/styled-system', + // Recommended by panda maintainer due to potential bug with nesting styled-system in src + importMap: 'styled-system', + // Enables JSX util generation! jsxFramework: 'react', }); diff --git a/apps/web/playwright.config.ts b/apps/web/playwright.config.ts new file mode 100644 index 00000000000..c6872da1b35 --- /dev/null +++ b/apps/web/playwright.config.ts @@ -0,0 +1,87 @@ +import { defineConfig, devices } from '@playwright/test'; +import dotenv from 'dotenv'; +import path from 'path'; + +dotenv.config({ path: path.resolve(__dirname, '.env.playwirght.test') }); + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default 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, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 4 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: process.env.CI ? 'blob' : 'html', + /* 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: 'http://127.0.0.1:4200', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + timeout: 60_000, + /* 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/apps/web/src/api/hooks/notification-templates/useCreateTemplateFromBlueprint.ts b/apps/web/src/api/hooks/notification-templates/useCreateTemplateFromBlueprint.ts index 3e08ce5a0a1..0bbf465e1eb 100644 --- a/apps/web/src/api/hooks/notification-templates/useCreateTemplateFromBlueprint.ts +++ b/apps/web/src/api/hooks/notification-templates/useCreateTemplateFromBlueprint.ts @@ -1,14 +1,20 @@ import { useMutation, UseMutationOptions } from '@tanstack/react-query'; -import { ICreateNotificationTemplateDto, INotificationTemplate, INotificationTemplateStep } from '@novu/shared'; +import { + IBlueprint, + ICreateNotificationTemplateDto, + INotificationTemplate, + INotificationTemplateStep, +} from '@novu/shared'; import { createTemplate } from '../../notification-templates'; -const mapBlueprintToTemplate = (blueprint: INotificationTemplate): ICreateNotificationTemplateDto => ({ +const mapBlueprintToTemplate = (blueprint: IBlueprint): ICreateNotificationTemplateDto => ({ name: blueprint.name, tags: blueprint.tags, description: blueprint.description, steps: blueprint.steps as INotificationTemplateStep[], notificationGroupId: blueprint._notificationGroupId, + notificationGroup: blueprint.notificationGroup, active: blueprint.active, draft: blueprint.draft, critical: blueprint.critical, @@ -20,13 +26,13 @@ export const useCreateTemplateFromBlueprint = ( options: UseMutationOptions< INotificationTemplate & { __source?: string }, any, - { blueprint: INotificationTemplate; params: { __source?: string } } + { blueprint: IBlueprint; params: { __source?: string } } > = {} ) => { const { mutate, ...rest } = useMutation< INotificationTemplate, any, - { blueprint: INotificationTemplate; params: { __source?: string } } + { blueprint: IBlueprint; params: { __source?: string } } >((data) => createTemplate(mapBlueprintToTemplate(data.blueprint), data.params), { ...options, }); diff --git a/apps/web/src/api/notification-templates.ts b/apps/web/src/api/notification-templates.ts index 35c7ace8cd2..9cb5a6fd055 100644 --- a/apps/web/src/api/notification-templates.ts +++ b/apps/web/src/api/notification-templates.ts @@ -3,6 +3,7 @@ import { INotificationTemplate, IGroupedBlueprint, IPaginationWithQueryParams, + IBlueprint, } from '@novu/shared'; import { api } from './api.client'; @@ -40,7 +41,7 @@ export async function getBlueprintsGroupedByCategory(): Promise<{ return api.get(`${BLUEPRINTS_API_URL}/v1/blueprints/group-by-category`, { absoluteUrl: true }); } -export async function getBlueprintTemplateById(id: string): Promise { +export async function getBlueprintTemplateById(id: string): Promise { return api.get(`${BLUEPRINTS_API_URL}/v1/blueprints/${id}`, { absoluteUrl: true }); } diff --git a/apps/web/src/api/types.ts b/apps/web/src/api/types.ts index e1944e1b3bc..41e9067b446 100644 --- a/apps/web/src/api/types.ts +++ b/apps/web/src/api/types.ts @@ -1,6 +1,6 @@ import { IconName } from '@fortawesome/fontawesome-svg-core'; -import { INotificationTemplate } from '@novu/shared'; +import { IBlueprint } from '@novu/shared'; -export interface IBlueprintTemplate extends INotificationTemplate { +export interface IBlueprintTemplate extends IBlueprint { iconName: IconName; } diff --git a/apps/web/src/components/layout/AppLayout.tsx b/apps/web/src/components/layout/AppLayout.tsx index 1218d80b116..3b1160ff138 100644 --- a/apps/web/src/components/layout/AppLayout.tsx +++ b/apps/web/src/components/layout/AppLayout.tsx @@ -11,8 +11,9 @@ import { INTERCOM_APP_ID } from '../../config'; import { RequiredAuth } from './RequiredAuth'; import { SpotLight } from '../utils/Spotlight'; import { SpotLightProvider } from '../providers/SpotlightProvider'; +import { FreeTrialBanner } from './components/FreeTrialBanner'; -const AppShellNew = styled.div` +const AppShell = styled.div` display: flex; width: 100vw; height: 100vh; @@ -42,7 +43,7 @@ export function AppLayout() { fallback={({ error, resetError, eventId }) => ( <> Sorry, but something went wrong.
- Our team been notified about it and we will look at it asap. + Our team has been notified and we are investigating.
@@ -55,13 +56,14 @@ export function AppLayout() { )} > - + + - + diff --git a/apps/web/src/components/layout/components/EchoStatus.tsx b/apps/web/src/components/layout/components/EchoStatus.tsx index 3982bf6ea30..2905a3cbf11 100644 --- a/apps/web/src/components/layout/components/EchoStatus.tsx +++ b/apps/web/src/components/layout/components/EchoStatus.tsx @@ -31,9 +31,13 @@ export function EchoStatus() { return null; } - if (!echoEnabled || isInitialLoading) return null; + if (!echoEnabled) return null; const status = data?.status === 'ok' && !error ? 'ok' : 'down'; + let color = status === 'ok' ? 'green' : 'red'; + if (isInitialLoading) { + color = 'yellow'; + } return ( + Echo } diff --git a/apps/web/src/components/layout/components/FreeTrialBanner.tsx b/apps/web/src/components/layout/components/FreeTrialBanner.tsx new file mode 100644 index 00000000000..1135ff84ba5 --- /dev/null +++ b/apps/web/src/components/layout/components/FreeTrialBanner.tsx @@ -0,0 +1,23 @@ +import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { IS_DOCKER_HOSTED, useFeatureFlag } from '@novu/shared-web'; + +export function FreeTrialBanner() { + const isEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_BILLING_REVERSE_TRIAL_ENABLED); + + if (IS_DOCKER_HOSTED) { + return null; + } + + if (!isEnabled) { + return null; + } + + try { + const module = require('@novu/ee-billing-web'); + const Component = module.FreeTrialBanner; + + return ; + } catch (e) {} + + return null; +} diff --git a/apps/web/src/components/layout/components/FreeTrialSidebarWidget.tsx b/apps/web/src/components/layout/components/FreeTrialSidebarWidget.tsx new file mode 100644 index 00000000000..9c455e9ae63 --- /dev/null +++ b/apps/web/src/components/layout/components/FreeTrialSidebarWidget.tsx @@ -0,0 +1,23 @@ +import { FeatureFlagsKeysEnum } from '@novu/shared'; +import { IS_DOCKER_HOSTED, useFeatureFlag } from '@novu/shared-web'; + +export const FreeTrialSidebarWidget = () => { + const isEnabled = useFeatureFlag(FeatureFlagsKeysEnum.IS_BILLING_REVERSE_TRIAL_ENABLED); + + if (IS_DOCKER_HOSTED) { + return null; + } + + if (!isEnabled) { + return null; + } + + try { + const module = require('@novu/ee-billing-web'); + const Component = module.FreeTrialSidebarWidget; + + return ; + } catch (e) {} + + return null; +}; diff --git a/apps/web/src/components/layout/components/OrganizationSelect.tsx b/apps/web/src/components/layout/components/OrganizationSelect.tsx index 22f824d3161..8fd6a6d5a29 100644 --- a/apps/web/src/components/layout/components/OrganizationSelect.tsx +++ b/apps/web/src/components/layout/components/OrganizationSelect.tsx @@ -4,7 +4,7 @@ import * as capitalize from 'lodash.capitalize'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import type { IResponseError, IOrganizationEntity } from '@novu/shared'; -import { Select } from '@novu/design-system'; +import { Select, successMessage } from '@novu/design-system'; import { addOrganization, switchOrganization } from '../../../api/organization'; import { useAuthContext } from '../../providers/AuthProvider'; @@ -25,7 +25,11 @@ export default function OrganizationSelect() { IOrganizationEntity, IResponseError, string - >((name) => addOrganization(name)); + >((name) => addOrganization(name), { + onSuccess: () => { + successMessage('Your Business trial has started'); + }, + }); const { mutateAsync: changeOrganization } = useMutation((id) => switchOrganization(id) diff --git a/apps/web/src/components/layout/components/SideNav.tsx b/apps/web/src/components/layout/components/SideNav.tsx index 083757e78e2..f46a9062046 100644 --- a/apps/web/src/components/layout/components/SideNav.tsx +++ b/apps/web/src/components/layout/components/SideNav.tsx @@ -34,6 +34,7 @@ import { useSpotlightContext } from '../../providers/SpotlightProvider'; import { ChangesCountBadge } from './ChangesCountBadge'; import OrganizationSelect from './OrganizationSelect'; import { FeatureFlagsKeysEnum, UTM_CAMPAIGN_QUERY_PARAM } from '@novu/shared'; +import { FreeTrialSidebarWidget } from './FreeTrialSidebarWidget'; import { useUserOnboardingStatus } from '../../../api/hooks/useUserOnboardingStatus'; import { VisibilityOff } from './VisibilityOff'; import { useSegment } from '../../providers/SegmentProvider'; @@ -171,19 +172,19 @@ export function SideNav({}: Props) { borderRight: 'none', width: '300px', minHeight: '100vh', - padding: '16px 24px', + padding: '16px 0', paddingBottom: '0px', '@media (max-width: 768px)': { width: '100%', }, }} > - + - + + { }), ], environment: ENV, - + ignoreErrors: [ + 'Network Error', + 'network error (Error)', + 'ResizeObserver loop limit exceeded', + 'ResizeObserver loop completed with undelivered notifications', + 'Non-Error exception captured', + 'Non-Error promise rejection captured', + ], /* * This sets the sample rate to be 10%. You may want this to be 100% while * in development and sample at a lower rate in production diff --git a/apps/web/src/pages/auth/components/HubspotSignupForm.tsx b/apps/web/src/pages/auth/components/HubspotSignupForm.tsx index c25cad97f3c..9d1f7c8d29c 100644 --- a/apps/web/src/pages/auth/components/HubspotSignupForm.tsx +++ b/apps/web/src/pages/auth/components/HubspotSignupForm.tsx @@ -14,6 +14,7 @@ import { useVercelIntegration, useVercelParams } from '../../../hooks'; import { ROUTES } from '../../../constants/routes.enum'; import { HUBSPOT_FORM_IDS } from '../../../constants/hubspotForms'; import SetupLoader from './SetupLoader'; +import { successMessage } from '@novu/design-system'; export function HubspotSignupForm() { const [loading, setLoading] = useState(); @@ -51,6 +52,9 @@ export function HubspotSignupForm() { const { organizationName, jobTitle, ...rest } = data; const createDto: ICreateOrganizationDto = { ...rest, name: organizationName, jobTitle }; const organization = await createOrganizationMutation(createDto); + + successMessage('Your Business trial has started'); + const organizationResponseToken = await api.post(`/v1/auth/organizations/${organization._id}/switch`, {}); setToken(organizationResponseToken); diff --git a/apps/web/src/pages/auth/components/LoginForm.tsx b/apps/web/src/pages/auth/components/LoginForm.tsx index ffcc4448dde..b545fdf9ad8 100644 --- a/apps/web/src/pages/auth/components/LoginForm.tsx +++ b/apps/web/src/pages/auth/components/LoginForm.tsx @@ -13,6 +13,7 @@ import { useVercelParams } from '../../../hooks'; import { useAcceptInvite } from './useAcceptInvite'; import { ROUTES } from '../../../constants/routes.enum'; import { OAuth } from './OAuth'; +import { parseServerErrorMessage } from '../../../utils/errors'; type LoginFormProps = { invitationToken?: string; @@ -78,29 +79,28 @@ export function LoginForm({ email, invitationToken }: LoginFormProps) { } }; - const serverErrorString = useMemo(() => { - return Array.isArray(error?.message) ? error?.message[0] : error?.message; - }, [error]); + const emailClientError = errors.email?.message; + let emailServerError = parseServerErrorMessage(error); - const emailServerError = useMemo(() => { - if (serverErrorString === 'email must be an email') return 'Please provide a valid email'; - - return ''; - }, [serverErrorString]); + // TODO: Use a more human-friendly message in the IsEmail validator and remove this patch + if (emailServerError === 'email must be an email') { + emailServerError = 'Please provide a valid email address'; + } return ( <>
- {isError && !emailServerError && ( - - {' '} - {error?.message} - - )} ); } diff --git a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx index d698ba91821..c8c59eb87c1 100644 --- a/apps/web/src/pages/auth/components/QuestionnaireForm.tsx +++ b/apps/web/src/pages/auth/components/QuestionnaireForm.tsx @@ -9,7 +9,6 @@ import { JobTitleEnum, jobTitleToLabelMapper, ProductUseCasesEnum } from '@novu/ import type { ProductUseCases, IResponseError, ICreateOrganizationDto, IJwtPayload } from '@novu/shared'; import { Button, - colors, Digest, HalfClock, Input, @@ -26,6 +25,7 @@ import { useVercelIntegration, useVercelParams } from '../../../hooks'; import { ROUTES } from '../../../constants/routes.enum'; import { DynamicCheckBox } from './dynamic-checkbox/DynamicCheckBox'; import styled from '@emotion/styled/macro'; +import { useDomainParser } from './useDomainHook'; import { OnboardingExperimentV2ModalKey } from '../../../constants/experimentsConstants'; export function QuestionnaireForm() { @@ -34,11 +34,13 @@ export function QuestionnaireForm() { handleSubmit, formState: { errors }, control, + setError, } = useForm({}); const navigate = useNavigate(); const { setToken, token } = useAuthContext(); const { startVercelSetup } = useVercelIntegration(); const { isFromVercel } = useVercelParams(); + const { parse } = useDomainParser(); const { mutateAsync: createOrganizationMutation } = useMutation< { _id: string }, @@ -162,9 +164,14 @@ export function QuestionnaireForm() { name="domain" control={control} rules={{ - pattern: { - value: /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/, - message: 'Please make sure you specified a valid domain', + validate: { + isValiDomain: (value) => { + const val = parse(value as string); + + if (value && !val.isIcann) { + return 'Please provide a valid domain'; + } + }, }, }} render={({ field }) => { diff --git a/apps/web/src/pages/auth/components/SignUpForm.tsx b/apps/web/src/pages/auth/components/SignUpForm.tsx index 5ae9274d156..ca81b0e2b56 100644 --- a/apps/web/src/pages/auth/components/SignUpForm.tsx +++ b/apps/web/src/pages/auth/components/SignUpForm.tsx @@ -3,7 +3,6 @@ import { Link, useNavigate } from 'react-router-dom'; import { useMutation } from '@tanstack/react-query'; import { useForm } from 'react-hook-form'; import { Center } from '@mantine/core'; -import { showNotification } from '@mantine/notifications'; import { passwordConstraints, UTM_CAMPAIGN_QUERY_PARAM } from '@novu/shared'; import type { IResponseError } from '@novu/shared'; import { PasswordInput, Button, colors, Input, Text, Checkbox } from '@novu/design-system'; @@ -13,7 +12,6 @@ import { api } from '../../../api/api.client'; import { applyToken, useVercelParams } from '../../../hooks'; import { useAcceptInvite } from './useAcceptInvite'; import { PasswordRequirementPopover } from './PasswordRequirementPopover'; -import { buildGithubLink, buildVercelGithubLink } from './gitHubUtils'; import { ROUTES } from '../../../constants/routes.enum'; import { OAuth } from './OAuth'; @@ -36,9 +34,6 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { const { isFromVercel, code, next, configurationId } = useVercelParams(); const vercelQueryParams = `code=${code}&next=${next}&configurationId=${configurationId}`; const loginLink = isFromVercel ? `/auth/login?${vercelQueryParams}` : ROUTES.AUTH_LOGIN; - const githubLink = isFromVercel - ? buildVercelGithubLink({ code, next, configurationId }) - : buildGithubLink({ invitationToken }); const { isLoading, mutateAsync, isError, error } = useMutation< { token: string }, @@ -52,21 +47,14 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { >((data) => api.post('/v1/auth/register', data)); const onSubmit = async (data) => { + const [firstName, lastName] = data?.fullName.trim().split(' '); const itemData = { - firstName: data.fullName.split(' ')[0], - lastName: data.fullName.split(' ')[1], + firstName, + lastName, email: data.email, password: data.password, }; - if (!itemData.lastName) { - showNotification({ - message: 'Please write your full name including last name', - color: 'red', - }); - - return; - } const response = await mutateAsync(itemData); /** @@ -80,10 +68,9 @@ export function SignUpForm({ invitationToken, email }: SignUpFormProps) { submitToken(token, invitationToken); return true; - } else { - setToken(token); } + setToken(token); navigate(isFromVercel ? `/auth/application?${vercelQueryParams}` : ROUTES.AUTH_APPLICATION); return true; diff --git a/apps/web/src/pages/auth/components/useDomainHook.ts b/apps/web/src/pages/auth/components/useDomainHook.ts new file mode 100644 index 00000000000..68deb7fb0d5 --- /dev/null +++ b/apps/web/src/pages/auth/components/useDomainHook.ts @@ -0,0 +1,62 @@ +import { useCallback, useEffect, useRef } from 'react'; + +interface DomainInfo { + domain: string; + domainWithoutSuffix: string; + hostname: string; + isIcann: boolean; + isIp: boolean; + isPrivate: boolean; + publicSuffix: string; + subdomain: string; +} + +function stripProtocol(url: string): string { + return (url || '').replace(/(https?)?(:\/+)?/, ''); +} + +function fallbackParser(url: string) { + const parts = stripProtocol(url).split('.'); + + if (parts.length > 2) { + const [subdomain, ...domainParts] = parts; + + return { + subdomain: subdomain || '', + domain: domainParts.join('.'), + }; + } + + return { + subdomain: '', + domain: url, + }; +} + +export function useDomainParser(): { parse: (url: string) => Partial } { + const tldParser = useRef(null); + + useEffect(() => { + import( + /* webpackIgnore: true */ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line import/extensions + 'https://unpkg.com/tldts/dist/es6/index.js?module' + ) + .then((mod) => (tldParser.current = mod)) + .catch(() => (tldParser.current = null)); + }, []); + + const parse = useCallback((url: string) => { + url = stripProtocol(url); + + if (tldParser.current) { + return (tldParser.current as any).parse(url, { allowPrivateDomains: true }) as DomainInfo; + } + + return fallbackParser(url); + }, []); + + return { parse }; +} diff --git a/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx b/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx index 4bff312c356..c50a3232694 100644 --- a/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/DelayUseCase.const.tsx @@ -9,7 +9,7 @@ import { OnboardingUseCasesTabsEnum } from './OnboardingUseCasesTabsEnum'; const USECASE_BLUEPRINT_IDENTIFIER = 'get-started-delay'; export const DelayUseCaseConst: OnboardingUseCase = { - useCaseLink: 'https://docs.novu.co/workflows/delay-action', + useCaseLink: 'https://docs.novu.co/workflows/delay-action?utm_campaign=inapp-usecase-delay', type: OnboardingUseCasesTabsEnum.DELAY, title: 'Delay step execution', description: 'Introduces a specified time delay between workflow steps, ensuring a well-paced progression of events.', diff --git a/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx b/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx index c2aed0be20d..92ffd13cbfc 100644 --- a/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/DigestUseCase.const.tsx @@ -15,7 +15,7 @@ export const DigestUseCaseConst: OnboardingUseCase = { type: OnboardingUseCasesTabsEnum.DIGEST, description: 'Aggregates multiple events into a single, concise message, preventing user overload with excessive notifications.', - useCaseLink: 'https://docs.novu.co/workflows/digest', + useCaseLink: 'https://docs.novu.co/workflows/digest?utm_campaign=inapp-usecase-digest', steps: [ { title: 'Configure providers', diff --git a/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx b/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx index dcb0fcba3d7..0f3afbf2010 100644 --- a/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/InAppUseCase.const.tsx @@ -16,7 +16,7 @@ export const InAppUseCaseConst: OnboardingUseCase = { description: "Utilize Novu's pre-built customizable in-app component. " + 'Or opt for the headless library to create your own in-app notification center.', - useCaseLink: 'https://docs.novu.co/channels-and-providers/in-app/introduction', + useCaseLink: 'https://docs.novu.co/channels-and-providers/in-app/introduction?utm_campaign=inapp-usecase-inapp', steps: [ { title: 'Configure In-App provider', diff --git a/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx b/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx index 1db41a28ed7..97803d5c069 100644 --- a/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/MultiChannelUseCase.const.tsx @@ -15,7 +15,7 @@ export const MultiChannelUseCaseConst: OnboardingUseCase = { 'Notifies subscribers using a wide range of channels: In-App, Email, Chat, Push, and SMS.\n' + '\n' + 'Configure as many providers as you like to Customize notification experience.', - useCaseLink: 'https://docs.novu.co/channels-and-providers/introduction', + useCaseLink: 'https://docs.novu.co/channels-and-providers/introduction?utm_campaign=inapp-usecase-multichannel', steps: [ { title: 'Configure providers', diff --git a/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx b/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx index ac9a5f6dffd..1b6f84f7387 100644 --- a/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx +++ b/apps/web/src/pages/get-started/consts/TranslationUseCase.const.tsx @@ -11,7 +11,7 @@ export const TranslationUseCaseConst: OnboardingUseCase = { description: 'Upload translations to use them as variables or for auto-upload in the editor in a workflow. ' + 'This feature is available for business and enterprise plan.', - useCaseLink: 'https://docs.novu.co/content-creation-design/translations', + useCaseLink: 'https://docs.novu.co/content-creation-design/translations?utm_campaign=inapp-usecase-translation', steps: [ { title: 'Configure providers', diff --git a/apps/web/src/pages/integrations/components/NovuInAppForm.tsx b/apps/web/src/pages/integrations/components/NovuInAppForm.tsx index 1c8a44139ef..50a5101cac9 100644 --- a/apps/web/src/pages/integrations/components/NovuInAppForm.tsx +++ b/apps/web/src/pages/integrations/components/NovuInAppForm.tsx @@ -131,7 +131,7 @@ export const NovuInAppForm = ({ { window.open( - `https://docs.novu.co/notification-center/client/iframe${UTM_CAMPAIGN_QUERY_PARAM}#enabling-hmac-encryption` + `https://docs.novu.co/notification-center/client/iframe#enabling-hmac-encryption${UTM_CAMPAIGN_QUERY_PARAM}` ); }} /> diff --git a/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx index e02139bcea6..b1791aeef8d 100644 --- a/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx +++ b/apps/web/src/pages/quick-start/components/OnboardingExperimentModal.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Modal, useMantineTheme, Grid } from '@mantine/core'; import styled from '@emotion/styled'; @@ -22,6 +22,13 @@ export function OnboardingExperimentModal() { setOpened(true); }; + useEffect(() => { + segment.track('Welcome modal open - [Onboarding]', { + experiment_id: '2024-w15-onb', + _organization: currentOrganization?._id, + }); + }, [currentOrganization?._id, segment]); + return ( ({ open: false }); - const isOnboardingModalEnabled = localStorage.getItem(OnboardingExperimentV2ModalKey) === 'true'; + const isOnboardingModalEnabled = + localStorage.getItem(OnboardingExperimentV2ModalKey) === 'true' && window.innerWidth > 768; const onIntegrationModalClose = () => setClickedChannel({ open: false }); useEffect(() => { segment.track(OnBoardingAnalyticsEnum.CONFIGURE_PROVIDER_VISIT); - if (isOnboardingModalEnabled) { - segment.track('Welcome modal open - [Onboarding]', { - experiment_id: '2024-w15-onb', - _organization: currentOrganization?._id, - }); - } }, [currentOrganization?._id, isOnboardingModalEnabled, segment]); function handleOnClick() { diff --git a/apps/web/src/pages/templates/WorkflowListPage.tsx b/apps/web/src/pages/templates/WorkflowListPage.tsx index 97e6abc4375..927deb23c10 100644 --- a/apps/web/src/pages/templates/WorkflowListPage.tsx +++ b/apps/web/src/pages/templates/WorkflowListPage.tsx @@ -52,7 +52,7 @@ const columns: IExtendedColumn[] = [ - +
diff --git a/apps/web/src/pages/templates/components/InputVariables.tsx b/apps/web/src/pages/templates/components/InputVariables.tsx index 45365e41f18..00bfc1bcc32 100644 --- a/apps/web/src/pages/templates/components/InputVariables.tsx +++ b/apps/web/src/pages/templates/components/InputVariables.tsx @@ -18,11 +18,11 @@ export const InputVariables = ({ } try { - const module = require('@novu/ee-billing-web'); + const module = require('@novu/ee-echo-web'); const InputVariablesComponent = module.InputVariables; return ; - } catch (e) {} - - return null; + } catch (e) { + throw e; + } }; diff --git a/apps/web/src/pages/templates/components/InputVariablesForm.tsx b/apps/web/src/pages/templates/components/InputVariablesForm.tsx index dff89da03d4..38a19e495a2 100644 --- a/apps/web/src/pages/templates/components/InputVariablesForm.tsx +++ b/apps/web/src/pages/templates/components/InputVariablesForm.tsx @@ -27,7 +27,7 @@ export const InputVariablesForm = ({ onChange }: { onChange?: (data: any) => voi } try { - const module = require('@novu/ee-billing-web'); + const module = require('@novu/ee-echo-web'); const InputVariablesComponent = module.InputVariablesForm; return ( @@ -40,7 +40,7 @@ export const InputVariablesForm = ({ onChange }: { onChange?: (data: any) => voi /> ); - } catch (e) {} - - return null; + } catch (e) { + throw e; + } }; diff --git a/apps/web/src/pages/templates/components/WorkflowSidebar.tsx b/apps/web/src/pages/templates/components/WorkflowSidebar.tsx index e803fd89f74..d32059667ef 100644 --- a/apps/web/src/pages/templates/components/WorkflowSidebar.tsx +++ b/apps/web/src/pages/templates/components/WorkflowSidebar.tsx @@ -2,16 +2,17 @@ import { ReactNode } from 'react'; import { Title } from '@mantine/core'; import { useNavigate } from 'react-router-dom'; import { colors, Sidebar } from '@novu/design-system'; +import { useMantineColorScheme } from '@mantine/core'; import { useBasePath } from '../hooks/useBasePath'; -import { useLocalThemePreference } from '@novu/shared-web'; export const WorkflowSidebar = ({ children, title }: { children: ReactNode; title: string }) => { const navigate = useNavigate(); const path = useBasePath(); - const { themeStatus } = useLocalThemePreference(); - const isDark = themeStatus === 'dark'; + const { colorScheme } = useMantineColorScheme(); + + const isDark = colorScheme === 'dark'; return ( { - return ; + return ; }} /> ); diff --git a/apps/web/src/pages/templates/hooks/usePreviewEmailTemplate.ts b/apps/web/src/pages/templates/hooks/usePreviewEmailTemplate.ts index 0965203d7e1..327a8bcb0e6 100644 --- a/apps/web/src/pages/templates/hooks/usePreviewEmailTemplate.ts +++ b/apps/web/src/pages/templates/hooks/usePreviewEmailTemplate.ts @@ -5,6 +5,7 @@ import { usePreviewEmail } from '../../../api/hooks'; import { IForm } from '../components/formTypes'; import { useStepFormCombinedErrors } from './useStepFormCombinedErrors'; import { useStepFormPath } from './useStepFormPath'; +import { parsePayload } from '../../../utils'; export const usePreviewEmailTemplate = ({ locale, payload }: { locale?: string; payload: string }) => { const { watch } = useFormContext(); @@ -39,7 +40,7 @@ export const usePreviewEmailTemplate = ({ locale, payload }: { locale?: string; contentType: contentType, content, layoutId: layoutId, - payload: JSON.parse(payloadArg), + payload: parsePayload(payloadArg), subject: subject ?? '', locale, }); diff --git a/apps/web/src/pages/templates/hooks/usePreviewInAppTemplate.ts b/apps/web/src/pages/templates/hooks/usePreviewInAppTemplate.ts index a8beaf4cbfd..e5fff8af8a4 100644 --- a/apps/web/src/pages/templates/hooks/usePreviewInAppTemplate.ts +++ b/apps/web/src/pages/templates/hooks/usePreviewInAppTemplate.ts @@ -6,6 +6,7 @@ import { useProcessVariables } from '../../../hooks'; import { IForm } from '../components/formTypes'; import { useStepFormCombinedErrors } from './useStepFormCombinedErrors'; import { useStepFormPath } from './useStepFormPath'; +import { parsePayload } from '../../../utils'; export type ParsedPreviewStateType = { ctaButtons: IMessageButton[]; @@ -42,7 +43,7 @@ export const usePreviewInAppTemplate = ({ locale }: { locale?: string }) => { getInAppPreview({ locale, content: templateContent as string, - payload: JSON.parse(payloadArg), + payload: parsePayload(payloadArg), cta: templateCta, }); }, diff --git a/apps/web/src/pages/templates/workflow/digest/TimedDigestWillBeSentHeader.tsx b/apps/web/src/pages/templates/workflow/digest/TimedDigestWillBeSentHeader.tsx index b8ff9380772..6b07ae932ea 100644 --- a/apps/web/src/pages/templates/workflow/digest/TimedDigestWillBeSentHeader.tsx +++ b/apps/web/src/pages/templates/workflow/digest/TimedDigestWillBeSentHeader.tsx @@ -5,7 +5,6 @@ import { DigestUnitEnum, MonthlyTypeEnum } from '@novu/shared'; import { colors } from '@novu/design-system'; import { pluralizeTime } from '../../../../utils'; -import { useStepFormPath } from '../../hooks/useStepFormPath'; const Highlight = ({ children, isHighlight }) => { const { colorScheme } = useMantineColorScheme(); @@ -61,13 +60,12 @@ const sortWeekdays = (weekdays: string[]): string[] => { return weekdays.sort((a, b) => WEEKDAYS_ORDER.indexOf(a) - WEEKDAYS_ORDER.indexOf(b)); }; -export const TimedDigestWillBeSentHeader = ({ isHighlight = true }: { isHighlight?: boolean }) => { +export const TimedDigestWillBeSentHeader = ({ path, isHighlight = true }: { path: string; isHighlight?: boolean }) => { const { watch } = useFormContext(); - const stepFormPath = useStepFormPath(); - const unit = watch(`${stepFormPath}.digestMetadata.timed.unit`); + const unit = watch(`${path}.digestMetadata.timed.unit`); if (unit == DigestUnitEnum.MINUTES) { - const amount = watch(`${stepFormPath}.digestMetadata.timed.minutes.amount`); + const amount = watch(`${path}.digestMetadata.timed.minutes.amount`); return ( <> @@ -77,7 +75,7 @@ export const TimedDigestWillBeSentHeader = ({ isHighlight = true }: { isHighligh } if (unit == DigestUnitEnum.HOURS) { - const amount = watch(`${stepFormPath}.digestMetadata.timed.hours.amount`); + const amount = watch(`${path}.digestMetadata.timed.hours.amount`); return ( <> @@ -87,8 +85,8 @@ export const TimedDigestWillBeSentHeader = ({ isHighlight = true }: { isHighligh } if (unit === DigestUnitEnum.DAYS) { - const amount = watch(`${stepFormPath}.digestMetadata.timed.days.amount`); - const atTime = watch(`${stepFormPath}.digestMetadata.timed.days.atTime`); + const amount = watch(`${path}.digestMetadata.timed.days.amount`); + const atTime = watch(`${path}.digestMetadata.timed.days.atTime`); if (amount !== '' && amount !== '1') { return ( @@ -118,9 +116,9 @@ export const TimedDigestWillBeSentHeader = ({ isHighlight = true }: { isHighligh } if (unit === DigestUnitEnum.WEEKS) { - const amount = watch(`${stepFormPath}.digestMetadata.timed.weeks.amount`); - const atTime = watch(`${stepFormPath}.digestMetadata.timed.weeks.atTime`); - const weekDays = watch(`${stepFormPath}.digestMetadata.timed.weeks.weekDays`) || []; + const amount = watch(`${path}.digestMetadata.timed.weeks.amount`); + const atTime = watch(`${path}.digestMetadata.timed.weeks.atTime`); + const weekDays = watch(`${path}.digestMetadata.timed.weeks.weekDays`) || []; const weekDaysString = weekDays?.length > 2 @@ -162,14 +160,14 @@ export const TimedDigestWillBeSentHeader = ({ isHighlight = true }: { isHighligh ); } - const amount = watch(`${stepFormPath}.digestMetadata.timed.months.amount`); - const monthlyType = watch(`${stepFormPath}.digestMetadata.timed.months.monthlyType`); - const atTime = watch(`${stepFormPath}.digestMetadata.timed.months.atTime`); - const monthDays = watch(`${stepFormPath}.digestMetadata.timed.months.monthDays`) || []; + const amount = watch(`${path}.digestMetadata.timed.months.amount`); + const monthlyType = watch(`${path}.digestMetadata.timed.months.monthlyType`); + const atTime = watch(`${path}.digestMetadata.timed.months.atTime`); + const monthDays = watch(`${path}.digestMetadata.timed.months.monthDays`) || []; if (monthlyType === MonthlyTypeEnum.ON) { - const ordinal = watch(`${stepFormPath}.digestMetadata.timed.months.ordinal`); - const ordinalValue = watch(`${stepFormPath}.digestMetadata.timed.months.ordinalValue`); + const ordinal = watch(`${path}.digestMetadata.timed.months.ordinal`); + const ordinalValue = watch(`${path}.digestMetadata.timed.months.ordinalValue`); if (!ordinal || !ordinalValue) { return null; diff --git a/apps/web/src/pages/templates/workflow/digest/WillBeSentHeader.tsx b/apps/web/src/pages/templates/workflow/digest/WillBeSentHeader.tsx index 78cc99cc17c..984c4a730a7 100644 --- a/apps/web/src/pages/templates/workflow/digest/WillBeSentHeader.tsx +++ b/apps/web/src/pages/templates/workflow/digest/WillBeSentHeader.tsx @@ -37,7 +37,7 @@ export const WillBeSentHeader = ({ path, isHighlight = true }: { path: string; i const type = watch(`${path}.digestMetadata.type`); if (type === DigestTypeEnum.TIMED) { - return ; + return ; } const unit = watch(`${path}.digestMetadata.regular.unit`); diff --git a/apps/web/src/utils/errors.ts b/apps/web/src/utils/errors.ts new file mode 100644 index 00000000000..214017bcded --- /dev/null +++ b/apps/web/src/utils/errors.ts @@ -0,0 +1,9 @@ +import type { IResponseError } from '@novu/shared'; + +export function parseServerErrorMessage(error: IResponseError | null): String { + if (!error) { + return ''; + } + + return Array.isArray(error?.message) ? error?.message[0] : error?.message; +} diff --git a/apps/web/src/utils/utils.ts b/apps/web/src/utils/utils.ts index 7416e14e6b5..66f66c7433d 100644 --- a/apps/web/src/utils/utils.ts +++ b/apps/web/src/utils/utils.ts @@ -18,3 +18,11 @@ export function formatNumber(num: number, digits: number) { return item ? (num / item.value).toFixed(digits).replace(rx, '$1') + item.symbol : '0'; } + +export function parsePayload(payload: string) { + try { + return JSON.parse(payload); + } catch (e) { + return {}; + } +} diff --git a/apps/web/tests/activity-graph.spec.ts b/apps/web/tests/activity-graph.spec.ts new file mode 100644 index 00000000000..f8b784ce627 --- /dev/null +++ b/apps/web/tests/activity-graph.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; +import { createNotifications } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); + await createNotifications({ + identifier: session.templates[0].triggers[0].identifier, + token: session.token, + count: 25, + organizationId: session.organization._id, + environmentId: session.environment._id, + }); +}); + +test('should be able to add a new channel', async ({ page }) => { + await page.goto('/activities'); + await expect(page).toHaveURL(/\/activities/); + + const addChannelButton = getByTestId(page, 'activity-stats-weekly-sent'); + await expect(addChannelButton).toContainText('25'); +}); diff --git a/apps/web/tests/digest-playground.spec.ts b/apps/web/tests/digest-playground.spec.ts new file mode 100644 index 00000000000..3dce3206873 --- /dev/null +++ b/apps/web/tests/digest-playground.spec.ts @@ -0,0 +1,185 @@ +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context, { noTemplates: true }); +}); + +test('should have a link to the docs', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + const learnMoreLink = page.locator('a[href^="https://docs.novu.co/workflows/digest"]'); + await expect(learnMoreLink).toHaveText('Learn more in docs'); +}); + +test('the set up digest workflow should redirect to template edit page', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + await expect(page).toHaveURL(/\/workflows\/edit/); +}); + +test('should show the digest workflow hints', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/workflows\/edit/); + + // check the digest hint + let digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up time interval'); + await expect(digestWorkflowTooltip).toContainText( + 'Specify for how long the digest should collect events before sending a digested event to the next step step in the workflow.' + ); + let primaryButton = page.getByRole('button', { name: 'Next' }); + await expect(primaryButton).toBeVisible(); + let skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).toBeVisible(); + let dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // check if has digest step + const digestNode = getByTestId(page, 'node-digestSelector'); + await expect(digestNode).toBeVisible(); + // check if digest step settings opened + let digestSettings = getByTestId(page, 'step-editor-sidebar'); + await expect(digestSettings).toBeVisible(); + await expect(digestSettings).toContainText('All events'); + + // click next on hint + await primaryButton.click(); + + // check the email hint + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up email content'); + await expect(digestWorkflowTooltip).toContainText( + 'Use custom HTML or our visual editor to define how the email will look like when sent to the subscriber.' + ); + primaryButton = page.getByRole('button', { name: 'Next' }); + await expect(primaryButton).toBeVisible(); + skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).toBeVisible(); + dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // check if email step settings opened + digestSettings = getByTestId(page, 'step-editor-sidebar'); + await expect(digestSettings).toBeVisible(); + await expect(digestSettings).toContainText('Email'); + + // click next on hint + await primaryButton.click(); + + // check the email hint + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Test your workflow'); + await expect(digestWorkflowTooltip).toContainText( + 'We will trigger the workflow multiple times to represent how it aggregates notifications.' + ); + primaryButton = page.getByRole('button', { name: 'Got it' }); + await expect(primaryButton).toBeVisible(); + skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await expect(skipTourButton).not.toBeVisible(); + dotsNavigation = getByTestId(page, 'digest-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); + + // the step settings should be hidden + const workflowSidebar = getByTestId(page, 'workflow-sidebar'); + await expect(workflowSidebar).toBeVisible(); + await expect(workflowSidebar).toContainText('Trigger'); + + // click got it should hide the hint + await primaryButton.click(); + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).not.toBeVisible(); +}); + +test('should hide the digest workflow hints when clicking on skip tour button', async ({ page }) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const setupDigestWorkflowButton = page.getByRole('button', { name: 'Set Up Digest Workflow' }); + await setupDigestWorkflowButton.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/workflows\/edit/); + + // check the digest hint + let digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).toContainText('Set-up time interval'); + const skipTourButton = page.getByRole('button', { name: 'Skip tour' }); + await skipTourButton.click(); + + digestWorkflowTooltip = getByTestId(page, 'digest-workflow-tooltip'); + await expect(digestWorkflowTooltip).not.toBeVisible(); +}); + +test('when clicking on the back button from the playground it should redirect to /get-started/preview', async ({ + page, +}) => { + await page.goto('/get-started'); + + const getStartedFooterLeftSide = getByTestId(page, 'get-started-footer-left-side'); + await getStartedFooterLeftSide.click(); + + // click try digest playground + const tryDigestPlaygroundBtn = getByTestId(page, 'try-digest-playground-btn'); + await tryDigestPlaygroundBtn.click(); + + await expect(page).toHaveURL(/\/digest-playground/); + await expect(page).toHaveTitle(/Digest Workflow Playground/); + + // click set up digest workflow + const goBack = page.getByRole('button', { name: 'Go Back' }); + await goBack.click(); + + // in the template workflow editor + await expect(page).toHaveURL(/\/get-started\/preview/); +}); diff --git a/apps/web/tests/integrations-list-modal.spec.ts b/apps/web/tests/integrations-list-modal.spec.ts new file mode 100644 index 00000000000..9db0e4e0d58 --- /dev/null +++ b/apps/web/tests/integrations-list-modal.spec.ts @@ -0,0 +1,831 @@ +import { + ChannelTypeEnum, + chatProviders, + EmailProviderIdEnum, + emailProviders, + InAppProviderIdEnum, + inAppProviders, + pushProviders, + SmsProviderIdEnum, + smsProviders, +} from '@novu/shared'; +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; +import { + checkTableLoading, + checkTableRow, + clickOnListRow, + interceptIntegrationsRequest, + navigateToGetStarted, +} from './utils.ts/integrations'; +import { deleteProvider } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should show the table loading skeleton and empty state', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: () => ({ data: [] }), + }); + + await navigateToGetStarted(page, 'channel-card-sms'); + + const providerSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(providerSidebar).toBeVisible(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableLoading(page); + await integrationsPromise; + + const noIntegrationsPlaceholder = getByTestId(page, 'no-integrations-placeholder'); + await expect(noIntegrationsPlaceholder).toBeVisible(); + await expect(noIntegrationsPlaceholder).toContainText('Choose a channel you want to start sending notifications'); + + const inAppCard = getByTestId(page, 'integration-channel-card-in_app'); + await expect(inAppCard).toBeEnabled(); + await expect(inAppCard).toContainText('In-App'); + const emailCard = getByTestId(page, 'integration-channel-card-email'); + await expect(emailCard).toBeEnabled(); + await expect(emailCard).toContainText('Email'); + const chatCard = getByTestId(page, 'integration-channel-card-chat'); + await expect(chatCard).toBeEnabled(); + await expect(chatCard).toContainText('Chat'); + const pushCard = getByTestId(page, 'integration-channel-card-push'); + await expect(pushCard).toBeEnabled(); + await expect(pushCard).toContainText('Push'); + const smsCard = getByTestId(page, 'integration-channel-card-sms'); + await expect(smsCard).toBeEnabled(); + await expect(smsCard).toContainText('SMS'); +}); + +test('should show the table loading skeleton and then table', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + }); + + await navigateToGetStarted(page, 'channel-card-sms'); + + const providerSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(providerSidebar).toBeVisible(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableLoading(page); + await integrationsPromise; + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await expect(addProvider).toContainText('Add a provider'); + + await checkTableRow(page, { + name: 'SendGrid', + provider: 'SendGrid', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Twilio', + provider: 'Twilio', + channel: 'SMS', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Slack', + provider: 'Slack', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Discord', + provider: 'Discord', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Firebase Cloud Messaging', + provider: 'Firebase Cloud Messaging', + channel: 'Push', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Novu In-App', + isFree: false, + provider: 'Novu In-App', + channel: 'In-App', + environment: 'Development', + status: 'Active', + }); +}); + +test('should show the select provider sidebar', async ({ page }) => { + await deleteProvider({ + providerId: InAppProviderIdEnum.Novu, + channel: ChannelTypeEnum.IN_APP, + environmentId: session.environment.id, + organizationId: session.organization.id, + }); + + await navigateToGetStarted(page); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + await expect(selectProviderSidebar).toContainText('Select a provider'); + await expect(selectProviderSidebar).toContainText('Select a provider to create instance for a channel'); + const search = selectProviderSidebar.locator('input[type="search"]'); + await expect(search).toHaveAttribute('placeholder', 'Search a provider...'); + const sidebarClose = getByTestId(selectProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const activeTab = channelTabs.locator('[data-active="true"]'); + await expect(activeTab).toContainText('Email'); + await expect(channelTabs).toContainText('In-App'); + await expect(channelTabs).toContainText('Email'); + await expect(channelTabs).toContainText('Chat'); + await expect(channelTabs).toContainText('Push'); + await expect(channelTabs).toContainText('SMS'); + + const inAppGroup = getByTestId(page, 'providers-group-in_app'); + await expect(inAppGroup).toContainText('In-App'); + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const chatGroup = getByTestId(page, 'providers-group-chat'); + await expect(chatGroup).toContainText('Chat'); + const pushGroup = getByTestId(page, 'providers-group-push'); + await expect(pushGroup).toContainText('Push'); + const smsGroup = getByTestId(page, 'providers-group-sms'); + await expect(smsGroup).toContainText('SMS'); + + const allProviders = inAppProviders.concat(emailProviders, chatProviders, pushProviders, smsProviders); + for (const provider of allProviders) { + if (provider.id === EmailProviderIdEnum.Novu || provider.id === SmsProviderIdEnum.Novu) { + continue; + } + + const providerInGroup = getByTestId(selectProviderSidebar, `provider-${provider.id}`); + await expect(providerInGroup).toContainText(provider.displayName); + } + + const cancel = getByTestId(selectProviderSidebar, 'select-provider-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow for searching', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('Mail'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeVisible(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + const mailgun = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailgun}`); + await expect(mailgun).toContainText('Mailgun'); + const mailerSend = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.MailerSend}`); + await expect(mailerSend).toContainText('MailerSend'); + const emailWebhook = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.EmailWebhook}`); + await expect(emailWebhook).toContainText('Email Webhook'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should show empty search results', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('safasdfasdfasdfasdfas'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeHidden(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const noSearchResults = getByTestId(page, 'select-provider-no-search-results-img'); + await expect(noSearchResults).toBeVisible(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow selecting a provider', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const selectedProviderImage = getByTestId( + selectProviderSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(selectProviderSidebar, 'selected-provider-name'); + await expect(selectedProviderName).toContainText('Mailjet'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeEnabled(); +}); + +test('should allow moving to create sidebar', async ({ page }) => { + await navigateToGetStarted(page); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const createProviderInstanceSidebar = getByTestId(page, 'create-provider-instance-sidebar'); + await expect(createProviderInstanceSidebar).toBeVisible(); + await expect(createProviderInstanceSidebar).toContainText( + 'Specify assignment preferences to automatically allocate the provider instance to the Email channel.' + ); + await expect(createProviderInstanceSidebar).toContainText('Environment'); + await expect(createProviderInstanceSidebar).toContainText('Provider instance executes only for'); + + const sidebarClose = getByTestId(createProviderInstanceSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const selectedProviderImage = getByTestId( + createProviderInstanceSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(createProviderInstanceSidebar, 'provider-instance-name'); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Mailjet'); + + const environmentRadios = createProviderInstanceSidebar.locator('[role="radiogroup"]'); + const selectedEnv = environmentRadios.locator('[data-checked="true"]'); + await expect(selectedEnv).toContainText('Development'); + await expect(environmentRadios).toContainText('Production'); + + const cancel = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + await expect(cancel).toBeEnabled(); + + const create = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); +}); + +test('should allow moving back from create provider sidebar to select provider sidebar', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const back = getByTestId(page, 'create-provider-instance-sidebar-back'); + await expect(back).toBeVisible(); + await back.click(); + + selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); +}); + +test('should create a new mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should update the mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText('Set up credentials to start sending notifications.'); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should update the mailjet integration from the list', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should allow to delete the mailjet integration', async ({ page }) => { + await navigateToGetStarted(page); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const menu = updateProviderSidebar.locator('[aria-haspopup="menu"]'); + await menu.click(); + const deleteButton = updateProviderSidebar.locator('button[data-menu-item="true"]', { hasText: 'Delete' }); + await deleteButton.click(); + + const deleteModal = getByTestId(page, 'delete-provider-instance-modal'); + await expect(deleteModal).toBeVisible(); + await expect(deleteModal).toContainText('Delete Mailjet Integration instance?'); + await expect(deleteModal).toContainText( + 'Deleting a provider instance will fail workflows relying on its configuration, leading to undelivered notifications.' + ); + + const cancel = deleteModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeEnabled(); + const deleteInstanceButton = deleteModal.getByRole('button', { name: 'Delete instance' }); + await expect(deleteInstanceButton).toBeEnabled(); + await deleteInstanceButton.click(); + + const integrationsTable = getByTestId(page, 'integration-name-cell', { hasText: 'Mailjet Integration' }); + await expect(integrationsTable).toBeHidden(); +}); + +test('should show the Novu in-app integration', async ({ page }) => { + await navigateToGetStarted(page); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText( + 'Select a framework to set up credentials to start sending notifications.' + ); + + const sidebarClose = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('In-App'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const linkToDocs = updateProviderSidebar.getByRole('link', { name: 'Explore set-up guide' }); + await expect(linkToDocs).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('true'); + + const isDarkThemeEnabled = await isDarkTheme(page); + const selectedProviderImage = getByTestId( + updateProviderSidebar, + `selected-provider-image-${InAppProviderIdEnum.Novu}` + ); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${InAppProviderIdEnum.Novu}.svg` + ); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu In-App'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/novu-in-app/); + + const hmacCheckbox = getByTestId(updateProviderSidebar, 'hmac'); + await expect(hmacCheckbox).not.toBeChecked(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('Integrate In-App using a framework below'); + await expect(novuInAppFrameworks).toContainText('React'); + await expect(novuInAppFrameworks).toContainText('Angular'); + await expect(novuInAppFrameworks).toContainText('Web Component'); + await expect(novuInAppFrameworks).toContainText('Headless'); + await expect(novuInAppFrameworks).toContainText('Vue'); + await expect(novuInAppFrameworks).toContainText('iFrame'); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu in-app integration - React guide', async ({ page }) => { + await navigateToGetStarted(page); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('React'); + + const reactGuide = novuInAppFrameworks.locator('div').filter({ hasText: 'React' }).nth(1); + await reactGuide.click(); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toContainText('React integration guide'); + + const sidebarBack = getByTestId(updateProviderSidebar, 'sidebar-back'); + await expect(sidebarBack).toBeVisible(); + const setupTimeline = getByTestId(updateProviderSidebar, 'setup-timeline'); + await expect(setupTimeline).toBeVisible(); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu Email integration sidebar', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, + }); + + await navigateToGetStarted(page); + await integrationsPromise; + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Novu Email.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${ + EmailProviderIdEnum.Novu + }.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu Email'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 300 emails per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('300 emails per month'); +}); + +test('should show the Novu SMS integration sidebar', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, + }); + + await navigateToGetStarted(page); + await integrationsPromise; + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Novu SMS.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${SmsProviderIdEnum.Novu}.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('SMS'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu SMS'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 20 messages per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('20 messages per month'); +}); diff --git a/apps/web/tests/integrations-list-page.spec.ts b/apps/web/tests/integrations-list-page.spec.ts new file mode 100644 index 00000000000..88c1056c81f --- /dev/null +++ b/apps/web/tests/integrations-list-page.spec.ts @@ -0,0 +1,1105 @@ +import { + ChannelTypeEnum, + chatProviders, + EmailProviderIdEnum, + emailProviders, + InAppProviderIdEnum, + inAppProviders, + pushProviders, + SmsProviderIdEnum, + smsProviders, +} from '@novu/shared'; +import { test, expect } from '@playwright/test'; + +import { getByTestId, initializeSession, isDarkTheme } from './utils.ts/browser'; +import { + checkTableLoading, + checkTableRow, + clickOnListRow, + interceptIntegrationsRequest, +} from './utils.ts/integrations'; +import { deleteProvider } from './utils.ts/plugins'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should show the table loading skeleton and empty state', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: () => ({ data: [] }), + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await checkTableLoading(page); + await integrationsPromise; + + const noIntegrationsPlaceholder = getByTestId(page, 'no-integrations-placeholder'); + await expect(noIntegrationsPlaceholder).toBeVisible(); + await expect(noIntegrationsPlaceholder).toContainText('Choose a channel you want to start sending notifications'); + + const inAppCard = getByTestId(page, 'integration-channel-card-in_app'); + await expect(inAppCard).toBeEnabled(); + await expect(inAppCard).toContainText('In-App'); + const emailCard = getByTestId(page, 'integration-channel-card-email'); + await expect(emailCard).toBeEnabled(); + await expect(emailCard).toContainText('Email'); + const chatCard = getByTestId(page, 'integration-channel-card-chat'); + await expect(chatCard).toBeEnabled(); + await expect(chatCard).toContainText('Chat'); + const pushCard = getByTestId(page, 'integration-channel-card-push'); + await expect(pushCard).toBeEnabled(); + await expect(pushCard).toContainText('Push'); + const smsCard = getByTestId(page, 'integration-channel-card-sms'); + await expect(smsCard).toBeEnabled(); + await expect(smsCard).toContainText('SMS'); +}); + +test('should show the table loading skeleton and then table', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await checkTableLoading(page); + await integrationsPromise; + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await expect(addProvider).toContainText('Add a provider'); + + await checkTableRow(page, { + name: 'SendGrid', + provider: 'SendGrid', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Twilio', + provider: 'Twilio', + channel: 'SMS', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Slack', + provider: 'Slack', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Discord', + provider: 'Discord', + channel: 'Chat', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Firebase Cloud Messaging', + provider: 'Firebase Cloud Messaging', + channel: 'Push', + environment: 'Development', + status: 'Active', + }); + + await checkTableRow(page, { + name: 'Novu In-App', + isFree: false, + provider: 'Novu In-App', + channel: 'In-App', + environment: 'Development', + status: 'Active', + }); +}); + +test('should show the select provider sidebar', async ({ page }) => { + await deleteProvider({ + providerId: InAppProviderIdEnum.Novu, + channel: ChannelTypeEnum.IN_APP, + environmentId: session.environment.id, + organizationId: session.organization.id, + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + await expect(selectProviderSidebar).toContainText('Select a provider'); + await expect(selectProviderSidebar).toContainText('Select a provider to create instance for a channel'); + const search = selectProviderSidebar.locator('input[type="search"]'); + await expect(search).toHaveAttribute('placeholder', 'Search a provider...'); + const sidebarClose = getByTestId(selectProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const activeTab = channelTabs.locator('[data-active="true"]'); + await expect(activeTab).toContainText('In-App'); + await expect(channelTabs).toContainText('In-App'); + await expect(channelTabs).toContainText('Email'); + await expect(channelTabs).toContainText('Chat'); + await expect(channelTabs).toContainText('Push'); + await expect(channelTabs).toContainText('SMS'); + + const inAppGroup = getByTestId(page, 'providers-group-in_app'); + await expect(inAppGroup).toContainText('In-App'); + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const chatGroup = getByTestId(page, 'providers-group-chat'); + await expect(chatGroup).toContainText('Chat'); + const pushGroup = getByTestId(page, 'providers-group-push'); + await expect(pushGroup).toContainText('Push'); + const smsGroup = getByTestId(page, 'providers-group-sms'); + await expect(smsGroup).toContainText('SMS'); + + const allProviders = inAppProviders.concat(emailProviders, chatProviders, pushProviders, smsProviders); + for (const provider of allProviders) { + if (provider.id === EmailProviderIdEnum.Novu || provider.id === SmsProviderIdEnum.Novu) { + continue; + } + + const providerInGroup = getByTestId(selectProviderSidebar, `provider-${provider.id}`); + await expect(providerInGroup).toContainText(provider.displayName); + } + + const cancel = getByTestId(selectProviderSidebar, 'select-provider-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow for searching', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('Mail'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeVisible(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const emailGroup = getByTestId(page, 'providers-group-email'); + await expect(emailGroup).toContainText('Email'); + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + const mailgun = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailgun}`); + await expect(mailgun).toContainText('Mailgun'); + const mailerSend = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.MailerSend}`); + await expect(mailerSend).toContainText('MailerSend'); + const emailWebhook = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.EmailWebhook}`); + await expect(emailWebhook).toContainText('Email Webhook'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should show empty search results', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const search = selectProviderSidebar.locator('input[type="search"]'); + await search.fill('safasdfasdfasdfasdfas'); + + const channelTabs = selectProviderSidebar.locator('[role="tablist"]'); + const inAppTab = channelTabs.locator('button', { hasText: 'In-App' }); + await expect(inAppTab).toBeHidden(); + const emailTab = channelTabs.locator('button', { hasText: 'Email' }); + await expect(emailTab).toBeHidden(); + const chatTab = channelTabs.locator('button', { hasText: 'Chat' }); + await expect(chatTab).toBeHidden(); + const pushTab = channelTabs.locator('button', { hasText: 'Push' }); + await expect(pushTab).toBeHidden(); + const smsTab = channelTabs.locator('button', { hasText: 'SMS' }); + await expect(smsTab).toBeHidden(); + + const noSearchResults = getByTestId(page, 'select-provider-no-search-results-img'); + await expect(noSearchResults).toBeVisible(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeDisabled(); +}); + +test('should allow selecting a provider', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const selectedProviderImage = getByTestId( + selectProviderSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(selectProviderSidebar, 'selected-provider-name'); + await expect(selectedProviderName).toContainText('Mailjet'); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await expect(next).toBeEnabled(); +}); + +test('should allow moving to create sidebar', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const createProviderInstanceSidebar = getByTestId(page, 'create-provider-instance-sidebar'); + await expect(createProviderInstanceSidebar).toBeVisible(); + await expect(createProviderInstanceSidebar).toContainText( + 'Specify assignment preferences to automatically allocate the provider instance to the Email channel.' + ); + await expect(createProviderInstanceSidebar).toContainText('Environment'); + await expect(createProviderInstanceSidebar).toContainText('Provider instance executes only for'); + + const sidebarClose = getByTestId(createProviderInstanceSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const selectedProviderImage = getByTestId( + createProviderInstanceSidebar, + `selected-provider-image-${EmailProviderIdEnum.Mailjet}` + ).first(); + const isDarkThemeEnabled = await isDarkTheme(page); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${EmailProviderIdEnum.Mailjet}.svg` + ); + + const selectedProviderName = getByTestId(createProviderInstanceSidebar, 'provider-instance-name'); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Mailjet'); + + const environmentRadios = createProviderInstanceSidebar.locator('[role="radiogroup"]'); + const selectedEnv = environmentRadios.locator('[data-checked="true"]'); + await expect(selectedEnv).toContainText('Development'); + await expect(environmentRadios).toContainText('Production'); + + const cancel = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-cancel'); + await expect(cancel).toContainText('Cancel'); + await expect(cancel).toBeEnabled(); + + const create = getByTestId(createProviderInstanceSidebar, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); +}); + +test('should allow moving back from create provider sidebar to select provider sidebar', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const back = getByTestId(page, 'create-provider-instance-sidebar-back'); + await expect(back).toBeVisible(); + await back.click(); + + selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); +}); + +test('should create a new mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should create a new mailjet integration with conditions', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + const providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const addConditionsButton = getByTestId(page, 'add-conditions-btn'); + await addConditionsButton.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for Mailjet Integration provider instance'); + + const addConditionButton = getByTestId(page, 'add-new-condition'); + await addConditionButton.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + await expect(addConditionsButton).toContainText('Edit conditions'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('1'); + await headerAddConditions.click(); + + const addNewCondition = getByTestId(page, 'add-new-condition'); + await addNewCondition.click(); + + const conditionsFormKey = getByTestId(page, 'conditions-form-key').last(); + await conditionsFormKey.fill('identifier'); + + const conditionsFormValue = getByTestId(page, 'conditions-form-value').last(); + await conditionsFormValue.fill('tenant456'); + + applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('2'); + + await expect(page).toHaveURL(/\/integrations\//); + + const close = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(close).toBeVisible(); + await close.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Disabled', + }); +}); + +test('should remove as primary when adding conditions', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`SendGrid.*Development`)); + + const headerAddConditions = getByTestId(page, 'header-add-conditions-btn'); + await headerAddConditions.click(); + + const removePrimaryFlagModal = getByTestId(page, 'remove-primary-flag-modal'); + await expect(removePrimaryFlagModal).toBeVisible(); + await expect(removePrimaryFlagModal).toContainText('Primary flag will be removed'); + await expect(removePrimaryFlagModal).toContainText( + 'Adding conditions to the primary provider instance removes its primary status when a user applies changes by' + ); + + const cancel = removePrimaryFlagModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeVisible(); + + const gotIt = removePrimaryFlagModal.getByRole('button', { name: 'Got it' }); + await expect(gotIt).toBeVisible(); + await gotIt.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for SendGrid provider instance'); + + const addConditionButton = getByTestId(page, 'add-new-condition'); + await addConditionButton.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + const providerName = page.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('SendGrid test'); + + const fromField = getByTestId(page, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(page, 'senderName'); + await senderName.fill('Novu'); + + const updateButton = getByTestId(page, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const makePrimaryButton = page.locator('button', { hasText: 'Make primary' }); + await expect(makePrimaryButton).toBeVisible(); + + const closeButton = page.locator('.mantine-Modal-close'); + await closeButton.click(); +}); + +test('should remove conditions when set to primary', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + await expect(page).toHaveURL(/\/integrations\/create/); + + const selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + await expect(page).toHaveURL(/\/integrations\/create\/email\/mailjet/); + + const providerName = page.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const addConditionsButton = getByTestId(page, 'add-conditions-btn'); + await addConditionsButton.click(); + + const conditionsTitle = getByTestId(page, 'conditions-form-title'); + await expect(conditionsTitle).toContainText('Conditions for Mailjet Integration provider instance'); + + const addNewCondition = getByTestId(page, 'add-new-condition'); + await addNewCondition.click(); + + const formOn = getByTestId(page, 'conditions-form-on'); + await expect(formOn).toHaveValue('Tenant'); + + const formKey = getByTestId(page, 'conditions-form-key'); + await formKey.fill('identifier'); + + const formOperator = getByTestId(page, 'conditions-form-operator'); + await expect(formOperator).toHaveValue('Equal'); + + const formValue = getByTestId(page, 'conditions-form-value'); + await formValue.fill('tenant123'); + + let applyButton = getByTestId(page, 'apply-conditions-btn'); + await applyButton.click(); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const headerAddConditions = getByTestId(updateProviderSidebar, 'header-add-conditions-btn'); + await expect(headerAddConditions).toContainText('1'); + + let makePrimaryButton = getByTestId(updateProviderSidebar, 'header-make-primary-btn'); + await makePrimaryButton.click(); + + const removeConditionsModal = getByTestId(page, 'remove-conditions-modal'); + await expect(removeConditionsModal).toBeVisible(); + await expect(removeConditionsModal).toContainText('Conditions will be removed'); + await expect(removeConditionsModal).toContainText('Marking this instance as primary will remove all conditions'); + const cancel = removeConditionsModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeVisible(); + const removeConditionsButton = removeConditionsModal.getByRole('button', { name: 'Remove conditions' }); + await expect(removeConditionsButton).toBeVisible(); + await removeConditionsButton.click(); + + makePrimaryButton = getByTestId(updateProviderSidebar, 'header-make-primary-btn'); + await expect(makePrimaryButton).toBeHidden(); + + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, new RegExp(`Mailjet Integration.*Development`)); +}); + +test('should update the mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText('Set up credentials to start sending notifications.'); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + const sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should update the mailjet integration from the list', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('false'); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await expect(providerName).toHaveValue('Mailjet Integration'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/mailjet/); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toBeDisabled(); + + providerName = updateProviderSidebar.getByPlaceholder('Enter instance name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration Updated'); + + await isActive.locator('~ label').click(); + + const apiKey = getByTestId(updateProviderSidebar, 'apiKey'); + await apiKey.fill('fake-api-key'); + + const secretKey = getByTestId(updateProviderSidebar, 'secretKey'); + await secretKey.fill('fake-secret-key'); + + const fromField = getByTestId(updateProviderSidebar, 'from'); + await fromField.fill('info@novu.co'); + + const senderName = getByTestId(updateProviderSidebar, 'senderName'); + await senderName.fill('Novu'); + + await expect(updateButton).toBeEnabled(); + await updateButton.click(); + + const modalClose = page.locator('.mantine-Modal-close'); + await modalClose.click(); + sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await checkTableRow(page, { + name: 'Mailjet Integration Updated', + provider: 'Mailjet', + channel: 'Email', + environment: 'Development', + status: 'Active', + }); +}); + +test('should allow to delete the mailjet integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + const addProvider = getByTestId(page, 'add-provider'); + await expect(addProvider).toBeEnabled(); + await addProvider.click(); + + let selectProviderSidebar = getByTestId(page, 'select-provider-sidebar'); + await expect(selectProviderSidebar).toBeVisible(); + + const mailjet = getByTestId(selectProviderSidebar, `provider-${EmailProviderIdEnum.Mailjet}`); + await expect(mailjet).toContainText('Mailjet'); + await mailjet.click(); + + const next = getByTestId(selectProviderSidebar, 'select-provider-sidebar-next'); + await expect(next).toContainText('Next'); + await next.click(); + + let providerName = getByTestId(page, 'provider-instance-name'); + await providerName.clear(); + await providerName.fill('Mailjet Integration'); + + const create = getByTestId(page, 'create-provider-instance-sidebar-create'); + await expect(create).toContainText('Create'); + await expect(create).toBeEnabled(); + await create.click(); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + let sidebarClose = getByTestId(page, 'sidebar-close'); + await sidebarClose.click(); + + await clickOnListRow(page, 'Mailjet Integration'); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const menu = updateProviderSidebar.locator('[aria-haspopup="menu"]'); + await menu.click(); + const deleteButton = updateProviderSidebar.locator('button[data-menu-item="true"]', { hasText: 'Delete' }); + await deleteButton.click(); + + const deleteModal = getByTestId(page, 'delete-provider-instance-modal'); + await expect(deleteModal).toBeVisible(); + await expect(deleteModal).toContainText('Delete Mailjet Integration instance?'); + await expect(deleteModal).toContainText( + 'Deleting a provider instance will fail workflows relying on its configuration, leading to undelivered notifications.' + ); + + const cancel = deleteModal.getByRole('button', { name: 'Cancel' }); + await expect(cancel).toBeEnabled(); + const deleteInstanceButton = deleteModal.getByRole('button', { name: 'Delete instance' }); + await expect(deleteInstanceButton).toBeEnabled(); + await deleteInstanceButton.click(); + + const integrationsTable = getByTestId(page, 'integration-name-cell', { hasText: 'Mailjet Integration' }); + await expect(integrationsTable).toBeHidden(); +}); + +test('should show the Novu in-app integration', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + const updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + await expect(updateProviderSidebar).toContainText( + 'Select a framework to set up credentials to start sending notifications.' + ); + + const sidebarClose = getByTestId(updateProviderSidebar, 'sidebar-close'); + await expect(sidebarClose).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('In-App'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const linkToDocs = updateProviderSidebar.getByRole('link', { name: 'Explore set-up guide' }); + await expect(linkToDocs).toBeVisible(); + + const isActive = getByTestId(updateProviderSidebar, 'is_active_id'); + await expect(isActive).toHaveValue('true'); + + const isDarkThemeEnabled = await isDarkTheme(page); + const selectedProviderImage = getByTestId( + updateProviderSidebar, + `selected-provider-image-${InAppProviderIdEnum.Novu}` + ); + await expect(selectedProviderImage).toHaveAttribute( + 'src', + `/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${InAppProviderIdEnum.Novu}.svg` + ); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu In-App'); + + const identifier = getByTestId(updateProviderSidebar, 'provider-instance-identifier'); + await expect(identifier).toHaveValue(/novu-in-app/); + + const hmacCheckbox = getByTestId(updateProviderSidebar, 'hmac'); + await expect(hmacCheckbox).not.toBeChecked(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('Integrate In-App using a framework below'); + await expect(novuInAppFrameworks).toContainText('React'); + await expect(novuInAppFrameworks).toContainText('Angular'); + await expect(novuInAppFrameworks).toContainText('Web Component'); + await expect(novuInAppFrameworks).toContainText('Headless'); + await expect(novuInAppFrameworks).toContainText('Vue'); + await expect(novuInAppFrameworks).toContainText('iFrame'); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu in-app integration - React guide', async ({ page }) => { + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + + await clickOnListRow(page, new RegExp(`Novu In-App.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toBeVisible(); + + const novuInAppFrameworks = getByTestId(updateProviderSidebar, 'novu-in-app-frameworks'); + await expect(novuInAppFrameworks).toContainText('React'); + + const reactGuide = novuInAppFrameworks.locator('div').filter({ hasText: 'React' }).nth(1); + await reactGuide.click(); + + updateProviderSidebar = getByTestId(page, 'update-provider-sidebar'); + await expect(updateProviderSidebar).toContainText('React integration guide'); + + const sidebarBack = getByTestId(updateProviderSidebar, 'sidebar-back'); + await expect(sidebarBack).toBeVisible(); + const setupTimeline = getByTestId(updateProviderSidebar, 'setup-timeline'); + await expect(setupTimeline).toBeVisible(); + + const updateButton = getByTestId(updateProviderSidebar, 'update-provider-sidebar-update'); + await expect(updateButton).toContainText('Update'); + await expect(updateButton).toBeDisabled(); +}); + +test('should show the Novu Email integration sidebar', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: EmailProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: EmailProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.EMAIL, + name: 'Novu Email', + identifier: EmailProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + await integrationsPromise; + + await clickOnListRow(page, new RegExp(`Novu Email.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${ + EmailProviderIdEnum.Novu + }.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('Email'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu Email'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 300 emails per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('300 emails per month'); +}); + +test('should show the Novu SMS integration sidebar', async ({ page }) => { + const integrationsPromise = interceptIntegrationsRequest({ + page, + modifyBody: (body) => { + const [firstIntegration] = body.data; + body.data = [ + { + _id: SmsProviderIdEnum.Novu, + _environmentId: firstIntegration._environmentId, + providerId: SmsProviderIdEnum.Novu, + active: true, + channel: ChannelTypeEnum.SMS, + name: 'Novu SMS', + identifier: SmsProviderIdEnum.Novu, + }, + ...body.data, + ]; + + return body; + }, + }); + + await page.goto('/integrations'); + await expect(page).toHaveURL(/\/integrations/); + await integrationsPromise; + + await clickOnListRow(page, new RegExp(`Novu SMS.*Development`)); + + let updateProviderSidebar = getByTestId(page, 'update-provider-sidebar-novu'); + await expect(updateProviderSidebar).toContainText('Test Provider'); + await expect(updateProviderSidebar).toBeVisible(); + + const isDarkThemeEnabled = await isDarkTheme(page); + const novuEmailLogo = updateProviderSidebar.locator( + `img[src="/static/images/providers/${isDarkThemeEnabled ? 'dark' : 'light'}/square/${SmsProviderIdEnum.Novu}.svg"]` + ); + await expect(novuEmailLogo).toBeVisible(); + + const integrationChannel = getByTestId(updateProviderSidebar, 'provider-instance-channel'); + await expect(integrationChannel).toContainText('SMS'); + + const integrationEnvironment = getByTestId(updateProviderSidebar, 'provider-instance-environment'); + await expect(integrationEnvironment).toContainText('Development'); + + const selectedProviderName = getByTestId(updateProviderSidebar, 'provider-instance-name').first(); + await expect(selectedProviderName).toBeVisible(); + await expect(selectedProviderName).toHaveValue('Novu SMS'); + + const providerLimits = getByTestId(updateProviderSidebar, 'novu-provider-limits'); + const providerLimitsText = await providerLimits.innerText(); + await expect(providerLimitsText).toEqual( + 'Novu provider allows sending max 20 messages per month,\nto send more messages, configure a different provider' + ); + + const limitbarLimit = getByTestId(updateProviderSidebar, 'limitbar-limit'); + const limitbarText = await limitbarLimit.innerText(); + await expect(limitbarText).toEqual('20 messages per month'); +}); diff --git a/apps/web/tests/main-functionality.spec.ts b/apps/web/tests/main-functionality.spec.ts new file mode 100644 index 00000000000..0ccbf787aa6 --- /dev/null +++ b/apps/web/tests/main-functionality.spec.ts @@ -0,0 +1,577 @@ +import { test, expect } from '@playwright/test'; +import os from 'node:os'; + +import { dragAndDrop, getByTestId, initializeSession } from './utils.ts/browser'; +import { + addAndEditChannel, + editChannel, + fillBasicNotificationDetails, + goBack, + updateWorkflowButtonClick, +} from './utils.ts/workflow-editor'; + +let session; + +const isMac = os.platform() === 'darwin'; +const modifier = isMac ? 'Meta' : 'Control'; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context); +}); + +test('should not reset data when switching channel types', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page); + await goBack(page); + await addAndEditChannel(page, 'inApp'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('{{firstName}} someone assigned you to {{taskName}}'); + + await goBack(page); + await addAndEditChannel(page, 'email'); + + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + const preheaderEl = getByTestId(page, 'emailPreheader'); + await preheaderEl.fill('this is email preheader'); + + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('This text is written from a test {{firstName}}'); + + await goBack(page); + + await editChannel(page, 'inApp'); + await goBack(page); + + await editChannel(page, 'email'); + await expect(getByTestId(page, 'emailSubject')).toHaveValue('this is email subject'); + await expect(getByTestId(page, 'emailPreheader')).toHaveValue('this is email preheader'); + await expect(getByTestId(page, 'editable-text-content')).toContainText('This text is written from a test'); +}); + +test('should update to empty data when switching from editor to customHtml', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page, { title: 'Test Notification' }); + await goBack(page); + await addAndEditChannel(page, 'email'); + + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('This text is written from a test {{firstName}}'); + + let subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + await updateWorkflowButtonClick(page); + + await page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .getByText(/Custom Code/) + .first() + .click(); + + subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.clear(); + await subjectEl.fill('new email subject'); + + await updateWorkflowButtonClick(page, { noWaitAfter: true }); + + const templatesLinkPage = getByTestId(page, 'side-nav-templates-link'); + await templatesLinkPage.click(); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Test Notification/).click(); + + await editChannel(page, 'email'); + + await expect( + page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .locator('[data-active="true"]') + .getByText(/Custom Code/) + .first() + ).toBeVisible(); +}); + +test('should save avatar enabled and content for in app', async ({ page }) => { + await page.goto('/workflows/create'); + + await fillBasicNotificationDetails(page, { title: 'Test save avatar' }); + await goBack(page); + await addAndEditChannel(page, 'inApp'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('new content for notification'); + + const enableAddAvatar = getByTestId(page, 'enable-add-avatar'); + await enableAddAvatar.click(); + const chooseAvatar = getByTestId(page, 'choose-avatar-btn'); + await chooseAvatar.click(); + const avatarIconInfo = getByTestId(page, 'avatar-icon-info'); + await avatarIconInfo.click(); + + await updateWorkflowButtonClick(page); + + await expect(getByTestId(page, 'enabled-avatar')).toBeChecked(); + await expect(getByTestId(page, 'avatar-icon-info')).toBeVisible(); +}); + +test('should edit in-app notification', async ({ page }) => { + const template = session.templates[0]; + await page.goto(`/workflows/edit/${template._id}`); + + const settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + let nameInput = getByTestId(page, 'name-input'); + await expect(nameInput.first()).toHaveValue(template.name); + + await goBack(page); + await editChannel(page, 'inApp'); + + let editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Test content for {{firstName}}'); + + await goBack(page); + + nameInput = getByTestId(page, 'name-input'); + await nameInput.clear(); + await nameInput.fill('This is the new notification title'); + + await editChannel(page, 'inApp'); + const feedButton = getByTestId(page, 'feed-button-1'); + await feedButton.click(); + + const monacoEditor = page.locator('.monaco-editor').nth(0); + await monacoEditor.click(); + await monacoEditor.press(`${modifier}+KeyX`); + await page.keyboard.type('new content for notification'); + + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await expect(await notificationsTemplate.getByText(/This is the new notification title/)).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + await expect(getByTestId(page, 'feed-button-0')).toBeVisible(); + await expect(getByTestId(page, 'feed-button-1-checked')).toBeVisible(); + const createFeedInput = getByTestId(page, 'create-feed-input'); + await createFeedInput.fill('test4'); + + const addFeedButton = getByTestId(page, 'add-feed-button'); + await addFeedButton.click(); + await expect(getByTestId(page, 'feed-button-2-checked')).toBeVisible(); +}); + +test('should unset feedId for in app step', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + let feedsCheckbox = getByTestId(page, 'use-feeds-checkbox'); + await expect(feedsCheckbox).toBeChecked(); + await feedsCheckbox.click(); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await expect(notificationsTemplate.getByText(template.name, { exact: false })).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'inApp'); + + feedsCheckbox = getByTestId(page, 'use-feeds-checkbox'); + await expect(feedsCheckbox).not.toBeChecked(); +}); + +test('should edit email notification', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + await editChannel(page, 'email'); + + const emailEditor = getByTestId(page, 'email-editor'); + const firstEditorRow = getByTestId(emailEditor, 'editor-row').first(); + await firstEditorRow.click(); + await firstEditorRow.press(`${modifier}+KeyA`); + await firstEditorRow.press(`${modifier}+KeyX`); + await page.keyboard.type('Hello world!'); +}); + +test('should update notification active status', async ({ page }) => { + const template = session.templates[0]; + + await page.goto(`/workflows/edit/${template._id}`); + + let settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + const toggleSwitch = getByTestId(page, 'active-toggle-switch'); + await expect(toggleSwitch).toBeVisible(); + await expect(page.getByText('Active')).toBeVisible(); + await toggleSwitch.locator('~ label').click({ force: true }); + await expect(page.getByText('Inactive')).toBeVisible(); + + await page.goto(`/workflows/edit/${template._id}`); + + settingsPage = getByTestId(page, 'settings-page'); + await settingsPage.click(); + + await expect(page.getByText('Inactive')).toBeVisible(); +}); + +test('should toggle active states of channels', async ({ page }) => { + await page.goto(`/workflows/create`); + + await fillBasicNotificationDetails(page, { title: 'Test toggle active states of channels' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + let stepActiveSwitch = getByTestId(page, 'step-active-switch'); + await expect(stepActiveSwitch).toHaveValue('on'); + + await stepActiveSwitch.locator('~ label').click({ force: true }); + await stepActiveSwitch.locator('~ label').click({ force: true }); + + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + stepActiveSwitch = getByTestId(page, 'step-active-switch'); + await expect(stepActiveSwitch).toHaveValue('on'); +}); + +test('should show trigger snippet block when editing', async ({ page }) => { + const template = session.templates[0]; + await page.goto(`/workflows/edit/${template._id}`); + + const getSnippetButton = getByTestId(page, 'get-snippet-btn'); + await getSnippetButton.click(); + + const triggerCodeSnippet = getByTestId(page, 'trigger-code-snippet'); + await expect(triggerCodeSnippet).toContainText('test-event'); +}); + +test('should show error on node if message field is missing', async ({ page }) => { + await page.goto(`/workflows/create`); + + await fillBasicNotificationDetails(page); + await goBack(page); + + await dragAndDrop(page, `dnd-emailSelector`, 'addNodeButton'); + let emailNode = getByTestId(page, 'node-emailSelector'); + let errorCircle = getByTestId(emailNode, 'error-circle'); + await expect(errorCircle).toBeVisible(); + + await editChannel(page, 'email'); + const emailSubject = getByTestId(page, 'emailSubject'); + await expect(emailSubject).toHaveClass(/mantine-TextInput-invalid/); + + await emailSubject.fill('this is email subject'); + await goBack(page); + emailNode = getByTestId(page, 'node-emailSelector'); + errorCircle = getByTestId(emailNode, 'error-circle'); + await expect(errorCircle).not.toBeVisible(); +}); + +test('should allow uploading a logo from email editor', async ({ page }) => { + await page.route('**/organizations', async (route) => { + const response = await page.request.fetch(route.request()); + const body = await response.json(); + if (body) { + delete body.data[0].branding.logo; + } + + await route.fulfill({ + response, + body, + }); + }); + + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test allow uploading a logo from email editor' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + const uploadImageButton = getByTestId(page, 'upload-image-button'); + await uploadImageButton.click(); + + const modalButton = page.getByRole('button', { name: 'Yes' }); + + await modalButton.click(); + await expect(page.url()).toContain('/brand'); +}); + +test('should show the brand logo on main page', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test allow uploading a logo from email editor' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + const brandLogo = getByTestId(page, 'brand-logo'); + await expect(brandLogo).toHaveAttribute('src', 'https://web.novu.co/static/images/logo-light.png'); +}); + +test('should support RTL text content', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test support RTL text content' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + let editableTextContent = getByTestId(page, 'editable-text-content'); + await editableTextContent.hover(); + await expect(editableTextContent).toHaveCSS('text-align', 'left'); + + const settingsRowButton = getByTestId(page, 'settings-row-btn'); + await settingsRowButton.click(); + + const alignRightButton = getByTestId(page, 'align-right-btn'); + await alignRightButton.click(); + editableTextContent = getByTestId(page, 'editable-text-content'); + await expect(editableTextContent).toHaveCSS('text-align', 'right'); +}); + +test('should create an SMS channel message', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Test SMS Notification Title' }); + await goBack(page); + + await addAndEditChannel(page, 'sms'); + + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('{{firstName}} someone assigned you to {{taskName}}'); + await goBack(page); + + const submitButton = getByTestId(page, 'notification-template-submit-btn'); + await submitButton.click(); + + const getSnippetButton = getByTestId(page, 'get-snippet-btn'); + await getSnippetButton.click(); + const workflowSidebar = getByTestId(page, 'workflow-sidebar'); + await expect(workflowSidebar).toBeVisible(); + const triggerCodeSnippet = getByTestId(workflowSidebar, 'trigger-code-snippet'); + await expect(triggerCodeSnippet).toContainText('test-sms-notification-title'); + await expect(triggerCodeSnippet).toContainText("import { Novu } from '@novu/node'"); + await expect(triggerCodeSnippet).toContainText('taskName'); + await expect(triggerCodeSnippet).toContainText('firstName'); +}); + +test('should save HTML template email', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Custom Code HTML Notification Title' }); + await goBack(page); + + await addAndEditChannel(page, 'email'); + + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + + await page + .locator('[data-test-id="editor-type-selector"] .mantine-Tabs-tabsList') + .getByText(/Custom Code/) + .first() + .click(); + + let editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Hello world code {{name}}
Test
'); + + await goBack(page); + + await editChannel(page, 'email'); + + editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await expect(editorParent).toContainText('Hello world code {{name}}
Test
'); +}); + +test('should redirect to the workflows page when switching environments', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'Environment Switching' }); + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/changes`); + const promoteChangesPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/v1\/changes\/.*\/apply/) && response.request().method() === 'POST'; + }); + const promoteButton = getByTestId(page, 'promote-btn').first(); + await promoteButton.click(); + await promoteChangesPromise; + + let environmentSwitchPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/auth\/environments\/.*\/switch/) && response.request().method() === 'POST'; + }); + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + await environmentSwitchPromise; + await expect(page).toHaveURL(/\/workflows/); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Environment Switching/).click(); + await expect(page).toHaveURL(/\/workflows\/edit/); + + environmentSwitchPromise = page.waitForResponse((response) => { + return !!response.url().match(/\/auth\/environments\/.*\/switch/) && response.request().method() === 'POST'; + }); + environmentSwitch = getByTestId(page, 'environment-switch'); + const developmentButton = environmentSwitch.getByText('Development'); + await developmentButton.click(); + await environmentSwitchPromise; + await expect(page).toHaveURL(/\/workflows/); +}); + +test('New workflow button should be disabled in the Production', async ({ page }) => { + await page.goto(`/workflows`); + + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + + const createWorkflowButton = getByTestId(page, 'create-workflow-btn'); + await expect(createWorkflowButton).toBeDisabled(); +}); + +test('Should not allow to go to New Template page in Production', async ({ page }) => { + await page.goto(`/workflows/create`); + + let environmentSwitch = getByTestId(page, 'environment-switch'); + const productionButton = environmentSwitch.getByText('Production'); + await productionButton.click(); + + await expect(page.url()).toContain('/workflows'); +}); + +test('should save Cta buttons state in inApp channel', async ({ page }) => { + await page.goto(`/workflows/create`); + await fillBasicNotificationDetails(page, { title: 'In App CTA Button' }); + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Text content'); + + const controlAdd = getByTestId(page, 'control-add').first(); + await controlAdd.click(); + + const clickArea = getByTestId(page, 'template-container-click-area').first(); + await clickArea.click(); + + await goBack(page); + + await updateWorkflowButtonClick(page); + + await page.goto(`/workflows`); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/In App CTA Button/).click(); + await expect(page.url()).toContain('/workflows/edit'); + + await editChannel(page, 'inApp'); + + const templateContainerInput = getByTestId(page, 'template-container').first().locator('input'); + await expect(templateContainerInput).toHaveCount(1); + + const removeButton = getByTestId(page, 'remove-button-icon'); + await removeButton.click(); + + await goBack(page); + + await editChannel(page, 'inApp'); + getByTestId(page, 'control-add').first(); +}); + +test('should load successfully the recently created notification template, when going back from editor -> templates list -> editor', async ({ + page, +}) => { + await page.goto(`/workflows`); + + const createWorkflowButton = getByTestId(page, 'create-workflow-btn'); + await createWorkflowButton.click(); + + const createBlankWorkflow = getByTestId(page, 'create-workflow-blank'); + await createBlankWorkflow.click(); + + await fillBasicNotificationDetails(page, { title: 'Test notification' }); + await goBack(page); + + await addAndEditChannel(page, 'inApp'); + const editorParent = page.locator('.monaco-editor textarea').locator('xpath=..'); + await editorParent.click(); + await editorParent.locator('textarea').fill('Test in-app'); + await goBack(page); + + await addAndEditChannel(page, 'email'); + const editableText = getByTestId(page, 'editable-text-content'); + await editableText.clear(); + await editableText.pressSequentially('Test email'); + const subjectEl = getByTestId(page, 'emailSubject'); + await subjectEl.fill('this is email subject'); + const emailPreheader = getByTestId(page, 'emailPreheader'); + await emailPreheader.fill('this is email preheader'); + await goBack(page); + + await updateWorkflowButtonClick(page); + + const workflowsLink = getByTestId(page, 'side-nav-templates-link'); + await workflowsLink.click(); + + const notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(/Test notification/).click(); + await expect(page.url()).toContain('/workflows/edit'); + + const inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + const emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); +}); + +test('should load successfully the same notification template, when going back from templates list -> editor -> templates list -> editor', async ({ + page, +}) => { + await page.goto(`/workflows`); + const template = session.templates[0]; + + let notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(template.name).click(); + await expect(page.url()).toContain('/workflows/edit'); + + let inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + let emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); + + const workflowsLink = getByTestId(page, 'side-nav-templates-link'); + await workflowsLink.click(); + + notificationsTemplate = getByTestId(page, 'notifications-template'); + await notificationsTemplate.getByText(template.name).click(); + await expect(page.url()).toContain('/workflows/edit'); + + inAppNode = getByTestId(page, 'node-inAppSelector'); + await expect(inAppNode).toBeVisible(); + emailNode = getByTestId(page, 'node-emailSelector'); + await expect(emailNode).toBeVisible(); +}); diff --git a/apps/web/tests/start-from-scratch-tour.spec.ts b/apps/web/tests/start-from-scratch-tour.spec.ts new file mode 100644 index 00000000000..9463676dadb --- /dev/null +++ b/apps/web/tests/start-from-scratch-tour.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from '@playwright/test'; +import os from 'node:os'; + +import { getByTestId, initializeSession } from './utils.ts/browser'; + +let session; + +test.beforeEach(async ({ context }) => { + session = await initializeSession(context, { showOnBoardingTour: true }); +}); + +test('should show the start from scratch intro step', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); +}); + +test('should hide the start from scratch intro step after clicking on watch later', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); + +test('should show the start from scratch tour hints', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + await scratchWorkflowTooltipPrimaryButton.click(); + + const scratchWorkflowTooltipTitle2 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle2).toHaveText('Click to edit workflow name'); + + const scratchWorkflowTooltipDescription2 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription2).toHaveText( + 'Specify a name for your workflow here or in the workflow settings.' + ); + + const scratchWorkflowTooltipPrimaryButton2 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton2).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton2.click(); + + const scratchWorkflowTooltipTitle3 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle3).toHaveText('Verify workflow settings'); + + const scratchWorkflowTooltipDescription3 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription3).toHaveText( + 'Manage name, identifier, group and description. Set up channels, active by default.' + ); + + const scratchWorkflowTooltipPrimaryButton3 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton3).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton3.click(); + + const scratchWorkflowTooltipTitle4 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle4).toHaveText('Build a notification workflow'); + + const scratchWorkflowTooltipDescription4 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription4).toHaveText( + 'Add channels you would like to send notifications to. The channels will be inserted to the trigger snippet.' + ); + + const scratchWorkflowTooltipPrimaryButton4 = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton4).toHaveText('Next'); + await scratchWorkflowTooltipPrimaryButton4.click(); + + const scratchWorkflowTooltipTitle5 = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle5).toHaveText('Run a test or Get Snippet'); + + const scratchWorkflowTooltipDescription5 = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription5).toHaveText( + 'Test a trigger as if it was sent from your API. Deploy it to your App by copy/paste trigger snippet.' + ); + + const gotItButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(gotItButton).toHaveText('Got it'); + await gotItButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); + +test('should show the dots navigation after the intro step', async ({ page }) => { + await page.goto('/workflows/create'); + + const scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + await scratchWorkflowTooltipPrimaryButton.click(); + + const dotsNavigation = await getByTestId(page, 'scratch-workflow-tooltip-dots-navigation'); + await expect(dotsNavigation).toBeVisible(); +}); + +test('should show not show the start from scratch tour hints after it is shown twice ', async ({ page }) => { + await page.goto('/workflows/create'); + + let scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + const scratchWorkflowTooltipTitle = await getByTestId(page, 'scratch-workflow-tooltip-title'); + await expect(scratchWorkflowTooltipTitle).toHaveText('Discover a quick guide'); + + const scratchWorkflowTooltipDescription = await getByTestId(page, 'scratch-workflow-tooltip-description'); + await expect(scratchWorkflowTooltipDescription).toHaveText('Four simple tips to become a workflow expert.'); + + const scratchWorkflowTooltipPrimaryButton = await getByTestId(page, 'scratch-workflow-tooltip-primary-button'); + await expect(scratchWorkflowTooltipPrimaryButton).toHaveText('Show me'); + + let scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + await expect(scratchWorkflowTooltip).not.toBeVisible(); + + await page.reload(); + + scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).toBeVisible(); + + scratchWorkflowTooltipSkipButton = await getByTestId(page, 'scratch-workflow-tooltip-skip-button'); + await expect(scratchWorkflowTooltipSkipButton).toHaveText('Watch later'); + await scratchWorkflowTooltipSkipButton.click(); + + const scratchWorkflowTooltip2 = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip2).not.toBeVisible(); + + await page.reload(); + + scratchWorkflowTooltip = await getByTestId(page, 'scratch-workflow-tooltip'); + await expect(scratchWorkflowTooltip).not.toBeVisible(); +}); diff --git a/apps/web/tests/utils.ts/browser.ts b/apps/web/tests/utils.ts/browser.ts new file mode 100644 index 00000000000..1985519aa13 --- /dev/null +++ b/apps/web/tests/utils.ts/browser.ts @@ -0,0 +1,31 @@ +import { BrowserContext, Locator, Page } from '@playwright/test'; + +import { getSession, ISessionOptions } from './plugins'; + +export async function initializeSession(context: BrowserContext, settings: ISessionOptions = {}) { + const session = await getSession(settings); + + await context.addInitScript((session) => { + (window as any).isPlaywright = true; + localStorage.setItem('auth_token', session.token); + }, session); + + return session; +} + +export function getByTestId(page: Page | Locator, selector: string, options?: Parameters[1]) { + return page.locator(`[data-test-id="${selector}"]`, options); +} + +export async function dragAndDrop(page: Page, dragSelector: string, dropSelector: string) { + const dndEl = await getByTestId(page, dragSelector); + await dndEl.dragTo(await getByTestId(page, dropSelector), { force: true }); +} + +export async function isDarkTheme(page: Page) { + const backgroundColor = await page.evaluate(() => { + const body = document.body; + return window.getComputedStyle(body).backgroundColor; + }); + return backgroundColor.toLowerCase() !== '#EDF0F2' && backgroundColor.toLowerCase() !== 'rgb(237, 240, 242)'; +} diff --git a/apps/web/tests/utils.ts/integrations.ts b/apps/web/tests/utils.ts/integrations.ts new file mode 100644 index 00000000000..f16ed59cfb5 --- /dev/null +++ b/apps/web/tests/utils.ts/integrations.ts @@ -0,0 +1,107 @@ +import { expect, Locator, Page } from '@playwright/test'; + +import { getByTestId } from './browser'; + +export const navigateToGetStarted = async (page: Page, card = 'channel-card-email') => { + await page.goto('/get-started'); + await expect(page).toHaveURL(/\/get-started/); + + const cardComponent = getByTestId(page, card); + const button = cardComponent.locator('button'); + await expect(button).toContainText('Change Provider'); + await button.click(); + + const integrationsModal = getByTestId(page, 'integrations-list-modal'); + await expect(integrationsModal).toBeVisible(); + await expect(integrationsModal).toContainText('Integrations Store'); +}; + +export const checkTableLoading = async (page: Page | Locator) => { + const nameCellLoadingElements = getByTestId(page, 'integration-name-cell-loading'); + await expect(nameCellLoadingElements).toHaveCount(10); + await expect(nameCellLoadingElements.first()).toBeVisible(); + + const providerCellLoadingElements = getByTestId(page, 'integration-provider-cell-loading'); + await expect(providerCellLoadingElements).toHaveCount(10); + await expect(providerCellLoadingElements.first()).toBeVisible(); + + const channelCellLoadingElements = getByTestId(page, 'integration-channel-cell-loading'); + await expect(channelCellLoadingElements).toHaveCount(10); + await expect(channelCellLoadingElements.first()).toBeVisible(); + + const envCellLoadingElements = getByTestId(page, 'integration-environment-cell-loading'); + await expect(envCellLoadingElements).toHaveCount(10); + await expect(envCellLoadingElements.first()).toBeVisible(); + + const statusCellLoadingElements = getByTestId(page, 'integration-status-cell-loading'); + await expect(statusCellLoadingElements).toHaveCount(10); + await expect(statusCellLoadingElements.first()).toBeVisible(); +}; + +export const checkTableRow = async ( + page: Page | Locator, + { + name, + isFree, + provider, + channel, + environment, + status, + }: { + name: string; + isFree?: boolean; + provider: string; + channel: string; + environment?: string; + status: string; + } +) => { + const integrationsTable = getByTestId(page, 'integrations-list-table'); + const nthRow = integrationsTable.locator('tbody tr', { hasText: new RegExp(`${name}.*${environment ?? ''}`) }); + const nameCell = getByTestId(nthRow, 'integration-name-cell', { hasText: name }); + await expect(nameCell).toBeVisible(); + + if (isFree) { + await expect(nameCell).toContainText('Test Provider'); + } + + const providerCell = getByTestId(nthRow, 'integration-provider-cell', { hasText: provider }); + await expect(providerCell).toBeVisible(); + + const channelCell = getByTestId(nthRow, 'integration-channel-cell', { hasText: channel }); + await expect(channelCell).toBeVisible(); + + if (environment) { + const environmentCell = getByTestId(nthRow, 'integration-environment-cell', { hasText: environment }); + await expect(environmentCell).toBeVisible(); + } + + const statusCell = getByTestId(nthRow, 'integration-status-cell', { hasText: status }); + await expect(statusCell).toBeVisible(); +}; + +export const clickOnListRow = async (page: Page | Locator, name: string | RegExp) => { + const integrationsTable = getByTestId(page, 'integrations-list-table'); + const row = integrationsTable.locator('tr', { hasText: name }).first(); + await expect(row).toBeVisible(); + await row.click(); +}; + +export async function interceptIntegrationsRequest({ + page, + modifyBody, +}: { + page: Page; + modifyBody?: (body: any) => any; +}) { + return page.route('**/v1/integrations', async (route) => { + const response = await page.request.fetch(route.request()); + await new Promise((resolve) => setTimeout(resolve, 3000)); + const body = await response.json(); + + await route.fulfill({ + response, + body: JSON.stringify(modifyBody ? modifyBody(body) : body), + }); + }); +} diff --git a/apps/web/tests/utils.ts/plugins.ts b/apps/web/tests/utils.ts/plugins.ts new file mode 100644 index 00000000000..013a6dfaa42 --- /dev/null +++ b/apps/web/tests/utils.ts/plugins.ts @@ -0,0 +1,122 @@ +import { DalService, IntegrationRepository, NotificationTemplateEntity } from '@novu/dal'; +import { ChannelTypeEnum, ProvidersIdEnum } from '@novu/shared'; +import { + UserSession, + NotificationTemplateService, + SubscribersService, + NotificationsService, + JobsService, +} from '@novu/testing'; + +export interface ISessionOptions { + noEnvironment?: boolean; + partialTemplate?: Partial; + noTemplates?: boolean; + showOnBoardingTour?: boolean; +} + +export async function getSession(settings: ISessionOptions = {}) { + const dal = new DalService(); + await dal.connect(process.env.MONGODB_URL ?? ''); + + const session = new UserSession('http://127.0.0.1:1336'); + await session.initialize({ + noEnvironment: settings?.noEnvironment, + showOnBoardingTour: settings?.showOnBoardingTour, + }); + + const notificationTemplateService = new NotificationTemplateService( + session.user._id, + session.organization._id, + session.environment._id as string + ); + + let templates; + if (!settings?.noTemplates) { + const templatePartial = settings?.partialTemplate || {}; + + templates = await Promise.all([ + notificationTemplateService.createTemplate({ ...(templatePartial as any) }), + notificationTemplateService.createTemplate({ + active: false, + draft: true, + }), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + notificationTemplateService.createTemplate(), + ]); + } + + return { + token: session.token.split(' ')[1], + user: session.user, + organization: session.organization, + environment: session.environment, + templates, + }; +} + +export async function deleteProvider(query: { + providerId: ProvidersIdEnum; + channel: ChannelTypeEnum; + environmentId: string; + organizationId: string; +}) { + const dal = new DalService(); + await dal.connect(process.env.MONGODB_URL ?? ''); + + const repository = new IntegrationRepository(); + + return await repository.deleteMany({ + channel: query.channel, + providerId: query.providerId, + _environmentId: query.environmentId, + _organizationId: query.organizationId, + }); +} + +export async function createNotifications({ + identifier, + token, + count = 1, + subscriberId, + environmentId, + organizationId, + templateId, +}: { + identifier: string; + token: string; + count?: number; + subscriberId?: string; + environmentId: string; + organizationId: string; + templateId?: string; +}) { + const jobsService = new JobsService(); + let subId = subscriberId; + if (!subId) { + const subscribersService = new SubscribersService(organizationId, environmentId); + const subscriber = await subscribersService.createSubscriber(); + subId = subscriber.subscriberId; + } + + const triggerIdentifier = identifier; + const service = new NotificationsService(token); + const session = new UserSession(process.env.API_URL); + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < count; i++) { + await service.triggerEvent(triggerIdentifier, subId, {}); + } + + if (organizationId) { + await session.awaitRunningJobs(templateId, undefined, 0, organizationId); + } + + while ((await jobsService.standardQueue.getWaitingCount()) || (await jobsService.standardQueue.getActiveCount())) { + await new Promise((resolve) => setTimeout(resolve, 50)); + } + + return 'ok'; +} diff --git a/apps/web/tests/utils.ts/workflow-editor.ts b/apps/web/tests/utils.ts/workflow-editor.ts new file mode 100644 index 00000000000..90ab651843e --- /dev/null +++ b/apps/web/tests/utils.ts/workflow-editor.ts @@ -0,0 +1,61 @@ +import { Page } from '@playwright/test'; + +import { dragAndDrop, getByTestId } from './browser'; + +export type Channel = 'inApp' | 'email' | 'sms' | 'chat' | 'push' | 'digest' | 'delay'; + +export async function fillBasicNotificationDetails( + page: Page, + { title, description }: { title?: string; description?: string } = {} +) { + const settings = await getByTestId(page, 'settings-page'); + await settings.click(); + + const titleEl = await getByTestId(page, 'title'); + await titleEl.first().clear(); + await titleEl.first().fill(title ?? 'Test Notification Title'); + + const descriptionEl = await getByTestId(page, 'description'); + await descriptionEl.fill(description ?? 'This is a test description for a test title'); +} + +export async function goBack(page: Page) { + const closeButton = await getByTestId(page, 'sidebar-close'); + await closeButton.click(); +} + +export async function editChannel(page: Page, channel: Channel) { + const stepNode = await getByTestId(page, `node-${channel}Selector`); + await stepNode.last().click(); + + if (['inApp', 'email', 'sms', 'chat', 'push'].includes(channel)) { + const sidebarComponent = await getByTestId(page, 'step-editor-sidebar'); + const editButton = await getByTestId(sidebarComponent, 'edit-action'); + await editButton.click(); + } +} + +export async function addAndEditChannel(page: Page, channel: Channel) { + await dragAndDrop(page, `dnd-${channel}Selector`, 'addNodeButton'); + await editChannel(page, channel); +} + +export async function updateWorkflowButtonClick(page: Page, { noWaitAfter = false }: { noWaitAfter?: boolean } = {}) { + if (noWaitAfter) { + const updateWorkflowButton = getByTestId(page, 'notification-template-submit-btn'); + await updateWorkflowButton.click(); + + return; + } + + const updateTemplateRequest = page.waitForResponse((response) => { + return response.url().match(/\/v1\/notification-templates\/.*/) && response.request().method() === 'PUT'; + }); + const getTemplateRequest = page.waitForResponse((response) => { + return response.url().match(/\/v1\/notification-templates\/.*/) && response.request().method() === 'GET'; + }); + let updateWorkflowButton = getByTestId(page, 'notification-template-submit-btn'); + await updateWorkflowButton.click(); + await updateTemplateRequest; + await getTemplateRequest; +} diff --git a/apps/webhook/package.json b/apps/webhook/package.json index 10fff80369a..00845628c8c 100644 --- a/apps/webhook/package.json +++ b/apps/webhook/package.json @@ -1,6 +1,6 @@ { "name": "@novu/webhook", - "version": "0.24.0", + "version": "0.24.1", "description": "", "author": "", "private": true, @@ -26,11 +26,11 @@ "@nestjs/core": "^10.2.2", "@nestjs/platform-express": "^10.2.2", "@nestjs/terminus": "^10.0.1", - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/testing": "^0.24.1", "@sentry/node": "^7.66.0", "axios": "^1.6.2", "class-transformer": "^0.5.1", diff --git a/apps/webhook/src/app.module.ts b/apps/webhook/src/app.module.ts index 079c6b0b52b..7c6fab5b756 100644 --- a/apps/webhook/src/app.module.ts +++ b/apps/webhook/src/app.module.ts @@ -19,7 +19,7 @@ const modules = [ SharedModule, HealthModule, WebhooksModule, - TracingModule.register(packageJson.name), + TracingModule.register(packageJson.name, packageJson.version), ProfilingModule.register(packageJson.name), LoggerModule.forRoot( createNestLoggingModuleOptions({ diff --git a/apps/webhook/src/health/health.controller.ts b/apps/webhook/src/health/health.controller.ts index cf7335561f3..93310ed892f 100644 --- a/apps/webhook/src/health/health.controller.ts +++ b/apps/webhook/src/health/health.controller.ts @@ -12,14 +12,12 @@ export class HealthController { @HealthCheck() async healthCheck(): Promise { const result = await this.healthCheckService.check([ - async () => { - return { - apiVersion: { - version, - status: 'up', - }, - }; - }, + async () => ({ + apiVersion: { + version, + status: 'up', + }, + }), () => this.dalHealthIndicator.isHealthy(), ]); diff --git a/apps/widget/cypress/e2e/notifications-list.spec.ts b/apps/widget/cypress/e2e/notifications-list.spec.ts index e7a24666b19..5aadb6ef2f6 100644 --- a/apps/widget/cypress/e2e/notifications-list.spec.ts +++ b/apps/widget/cypress/e2e/notifications-list.spec.ts @@ -191,18 +191,9 @@ describe('Notifications List', function () { scrollToBottom(); cy.getByTestId('notification-list-item').should('have.length', 26); - cy.getByTestId('notification-list-item') - .should('exist') - .each(($item, $index) => { - const notificationContentNumber = 20 - $index > 0 ? ' ' + (20 - $index).toString() : ''; - cy.wrap($item) - .invoke('text') - .then((text) => { - if ($index !== 0) { - expect(text).to.contains(`John${notificationContentNumber}`); - } - }); - }); + for (let i = 0; i < 21; i++) { + cy.getByTestId('notification-list-item').contains(`John ${i}`).should('exist'); + } }); }); diff --git a/apps/widget/package.json b/apps/widget/package.json index e59d929fa03..0e5575e2207 100644 --- a/apps/widget/package.json +++ b/apps/widget/package.json @@ -1,6 +1,6 @@ { "name": "@novu/widget", - "version": "0.24.0", + "version": "0.24.1", "private": true, "scripts": { "start": "react-app-rewired start", @@ -29,8 +29,8 @@ "@emotion/styled": "^11.6.0", "@mantine/core": "4.2.12", "@mantine/hooks": "4.2.12", - "@novu/notification-center": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/notification-center": "^0.24.1", + "@novu/shared": "^0.24.1", "antd": "^4.10.0", "babel-plugin-import": "^1.13.3", "chroma-js": "^2.4.2", @@ -63,8 +63,8 @@ "@emotion/react": "^11.7.1", "@emotion/styled": "^11.6.0", "@faker-js/faker": "^6.0.0", - "@novu/dal": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/dal": "^0.24.1", + "@novu/testing": "^0.24.1", "@types/jest": "^29.5.0", "@types/node": "^12.0.0", "@types/react": "^17.0.0", diff --git a/apps/worker/package.json b/apps/worker/package.json index 0514b00c93e..63dcae3c0fb 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,6 +1,6 @@ { "name": "@novu/worker", - "version": "0.24.0", + "version": "0.24.1", "description": "description", "author": "", "private": "true", @@ -30,11 +30,11 @@ "@nestjs/platform-express": "^10.2.2", "@nestjs/swagger": "^7.1.9", "@nestjs/terminus": "^10.0.1", - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/testing": "^0.24.1", "@sentry/node": "^7.40.0", "@sentry/tracing": "^7.40.0", "@types/newrelic": "^9.13.0", @@ -84,10 +84,10 @@ "typescript": "4.9.5" }, "optionalDependencies": { - "@novu/ee-chimera-connect": "^0.24.0", - "@novu/ee-auth": "^0.24.0", - "@novu/ee-billing": "^0.24.0", - "@novu/ee-translation": "^0.24.0" + "@novu/ee-chimera-connect": "^0.24.1", + "@novu/ee-auth": "^0.24.1", + "@novu/ee-billing": "^0.24.1", + "@novu/ee-translation": "^0.24.1" }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ diff --git a/apps/worker/src/.env.test b/apps/worker/src/.env.test index 75042df4b1b..f7eea5a5d35 100644 --- a/apps/worker/src/.env.test +++ b/apps/worker/src/.env.test @@ -79,6 +79,7 @@ LAUNCH_DARKLY_SDK_KEY= AUTO_CREATE_INDEXES=true IS_USE_MERGED_DIGEST_ID_ENABLED=true +IS_TOPIC_NOTIFICATION_ENABLED=true BROADCAST_QUEUE_CHUNK_SIZE=100 MULTICAST_QUEUE_CHUNK_SIZE=100 diff --git a/apps/worker/src/app/health/health.controller.ts b/apps/worker/src/app/health/health.controller.ts index 1a71031996d..0363132e47e 100644 --- a/apps/worker/src/app/health/health.controller.ts +++ b/apps/worker/src/app/health/health.controller.ts @@ -21,14 +21,12 @@ export class HealthController { return this.healthCheckService.check([ async () => this.dalHealthIndicator.isHealthy(), ...this.healthIndicators.map((indicator) => async () => indicator.isHealthy()), - async () => { - return { - apiVersion: { - version, - status: 'up', - }, - }; - }, + async () => ({ + apiVersion: { + version, + status: 'up', + }, + }), ]); } } diff --git a/apps/worker/src/app/workflow/services/active-jobs-metric.service.ts b/apps/worker/src/app/workflow/services/active-jobs-metric.service.ts index ab27231edc2..96f73db8f0e 100644 --- a/apps/worker/src/app/workflow/services/active-jobs-metric.service.ts +++ b/apps/worker/src/app/workflow/services/active-jobs-metric.service.ts @@ -1,3 +1,5 @@ +const nr = require('newrelic'); + import { ActiveJobsMetricQueueService, ActiveJobsMetricWorkerService, @@ -75,7 +77,11 @@ export class ActiveJobsMetricService { return undefined; }) - .catch((error) => Logger.error('Metric Job Exists function errored', LOG_CONTEXT, error)); + .catch((error) => { + nr.noticeError(error); + + Logger.error('Metric Job Exists function errored', LOG_CONTEXT, error); + }); } private getWorkerOptions(): WorkerOptions { diff --git a/apps/worker/src/app/workflow/services/execution-log.worker.ts b/apps/worker/src/app/workflow/services/execution-log.worker.ts index bf5ab242f08..cef1b40e3f0 100644 --- a/apps/worker/src/app/workflow/services/execution-log.worker.ts +++ b/apps/worker/src/app/workflow/services/execution-log.worker.ts @@ -48,7 +48,9 @@ export class ExecutionLogWorker extends ExecutionLogWorkerService { _this.createExecutionDetails .execute(data) .then(resolve) - .catch(reject) + .catch((e) => { + reject(e); + }) .finally(() => { transaction.end(); }); diff --git a/apps/worker/src/app/workflow/services/standard.worker.ts b/apps/worker/src/app/workflow/services/standard.worker.ts index e619f08584a..a797a0c44c5 100644 --- a/apps/worker/src/app/workflow/services/standard.worker.ts +++ b/apps/worker/src/app/workflow/services/standard.worker.ts @@ -153,6 +153,8 @@ export class StandardWorker extends StandardWorkerService { private async jobHasFailed(job: Job, error: Error): Promise { let jobId; + nr.noticeError(error); + try { const minimalData = this.extractMinimalJobData(job.data); jobId = minimalData.jobId; diff --git a/apps/worker/src/app/workflow/services/subscriber-process.worker.ts b/apps/worker/src/app/workflow/services/subscriber-process.worker.ts index 70621545d0c..96f39c6b60b 100644 --- a/apps/worker/src/app/workflow/services/subscriber-process.worker.ts +++ b/apps/worker/src/app/workflow/services/subscriber-process.worker.ts @@ -46,6 +46,7 @@ export class SubscriberProcessWorker extends SubscriberProcessWorkerService { .then(resolve) .catch((e) => { Logger.error(e, 'unexpected error', 'SubscriberProcessWorkerService - getWorkerProcessor'); + nr.noticeError(e); reject(e); }) diff --git a/apps/worker/src/app/workflow/services/workflow.worker.ts b/apps/worker/src/app/workflow/services/workflow.worker.ts index d52eeec1a3a..f13cb3b5e22 100644 --- a/apps/worker/src/app/workflow/services/workflow.worker.ts +++ b/apps/worker/src/app/workflow/services/workflow.worker.ts @@ -51,7 +51,10 @@ export class WorkflowWorker extends WorkflowWorkerService { _this.triggerEventUsecase .execute(data) .then(resolve) - .catch(reject) + .catch((e) => { + nr.noticeError(e); + reject(e); + }) .finally(() => { transaction.end(); }); diff --git a/apps/worker/src/app/workflow/usecases/send-message/digest/digest.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/digest/digest.usecase.ts index 342a055c0b3..28e1827ca9d 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/digest/digest.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/digest/digest.usecase.ts @@ -71,11 +71,13 @@ export class Digest extends SendMessageType { }) ); + const jobsToUpdate = [...nextJobs.map((job) => job._id), command.job._id]; + await this.jobRepository.update( { _environmentId: command.environmentId, _id: { - $in: nextJobs.map((job) => job._id), + $in: jobsToUpdate, }, }, { diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts index be6e0b75302..b251200d156 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message-email.usecase.ts @@ -453,7 +453,7 @@ export class SendMessageEmail extends SendMessageBase { message, 'error', 'mail_unexpected_error', - 'Error while sending email with provider', + error.message || error.name || 'Error while sending email with provider', command, LogCodeEnum.MAIL_PROVIDER_DELIVERY_ERROR, error @@ -468,7 +468,7 @@ export class SendMessageEmail extends SendMessageBase { status: ExecutionDetailsStatusEnum.FAILED, isTest: false, isRetry: false, - raw: JSON.stringify(error), + raw: JSON.stringify(error) === '{}' ? JSON.stringify({ message: error.message }) : JSON.stringify(error), }) ); diff --git a/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts b/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts index 5cfe288974a..febe515fcca 100644 --- a/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts +++ b/apps/worker/src/app/workflow/usecases/send-message/send-message.usecase.ts @@ -88,13 +88,16 @@ export class SendMessage { const stepType = command.step?.template?.type; - const chimeraResponse = await this.chimeraConnector.execute< - SendMessageCommand & { variables: IFilterVariables }, - ExecuteOutput | null - >({ - ...command, - variables: shouldRun.variables, - }); + let chimeraResponse: ExecuteOutput | null = null; + if (!['digest', 'delay'].includes(stepType as any)) { + chimeraResponse = await this.chimeraConnector.execute< + SendMessageCommand & { variables: IFilterVariables }, + ExecuteOutput | null + >({ + ...command, + variables: shouldRun.variables, + }); + } if (!command.payload?.$on_boarding_trigger) { const usedFilters = shouldRun?.conditions.reduce(ConditionsFilter.sumFilters, { diff --git a/apps/ws/package.json b/apps/ws/package.json index ae5ee302efe..b07212bc1a0 100644 --- a/apps/ws/package.json +++ b/apps/ws/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ws", - "version": "0.24.0", + "version": "0.24.1", "description": "", "author": "", "private": true, @@ -28,10 +28,10 @@ "@nestjs/swagger": "^7.1.9", "@nestjs/terminus": "^10.0.1", "@nestjs/websockets": "^10.2.2", - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/testing": "^0.24.1", "@sentry/node": "^7.30.0", "@socket.io/redis-adapter": "^7.2.0", "class-transformer": "^0.5.1", diff --git a/apps/ws/src/app.module.ts b/apps/ws/src/app.module.ts index 1762244d937..1cc7423e305 100644 --- a/apps/ws/src/app.module.ts +++ b/apps/ws/src/app.module.ts @@ -19,7 +19,7 @@ import * as packageJson from '../package.json'; const modules = [ SharedModule, HealthModule, - TracingModule.register(packageJson.name), + TracingModule.register(packageJson.name, packageJson.version), ProfilingModule.register(packageJson.name), SocketModule, LoggerModule.forRoot( diff --git a/apps/ws/src/health/health.controller.ts b/apps/ws/src/health/health.controller.ts index 26ac141ed20..92eb672d6b2 100644 --- a/apps/ws/src/health/health.controller.ts +++ b/apps/ws/src/health/health.controller.ts @@ -23,14 +23,12 @@ export class HealthController { ...indicatorHealthCheckFunctions, async () => this.dalHealthIndicator.isHealthy(), async () => this.wsServerHealthIndicator.isHealthy(), - async () => { - return { - apiVersion: { - version, - status: 'up', - }, - }; - }, + async () => ({ + apiVersion: { + version, + status: 'up', + }, + }), ]); return result; diff --git a/apps/ws/src/shared/subscriber-online/subscriber-online.service.ts b/apps/ws/src/shared/subscriber-online/subscriber-online.service.ts index 513e6578767..8a68cd02ab5 100644 --- a/apps/ws/src/shared/subscriber-online/subscriber-online.service.ts +++ b/apps/ws/src/shared/subscriber-online/subscriber-online.service.ts @@ -30,7 +30,7 @@ export class SubscriberOnlineService { private async updateOnlineStatus(subscriber: ISubscriberJwt, updatePayload: IUpdateSubscriberPayload) { await this.subscriberRepository.update( - { _id: subscriber._id, _organizationId: subscriber.organizationId }, + { _id: subscriber._id, _environmentId: subscriber.environmentId }, { $set: updatePayload, } diff --git a/apps/ws/src/socket/services/ws-server-health-indicator.service.ts b/apps/ws/src/socket/services/ws-server-health-indicator.service.ts index 46d29d955ae..892e7dc07ae 100644 --- a/apps/ws/src/socket/services/ws-server-health-indicator.service.ts +++ b/apps/ws/src/socket/services/ws-server-health-indicator.service.ts @@ -1,4 +1,4 @@ -import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'; +import { HealthCheckError, HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'; import { Injectable } from '@nestjs/common'; import { IHealthIndicator } from '@novu/application-generic'; @@ -7,16 +7,21 @@ import { WSGateway } from '../ws.gateway'; @Injectable() export class WSServerHealthIndicator extends HealthIndicator implements IHealthIndicator { - private INDICATOR_KEY = 'ws-server'; + private static KEY = 'ws-server'; constructor(private wsGateway: WSGateway) { super(); } async isHealthy(): Promise { - const status = !!this.wsGateway.server; + const isHealthy = !!this.wsGateway.server; + const result = this.getStatus(WSServerHealthIndicator.KEY, isHealthy); - return this.getStatus(this.INDICATOR_KEY, status); + if (isHealthy) { + return result; + } + + throw new HealthCheckError('WS server health check failed', result); } isActive(): Promise { diff --git a/enterprise/packages/auth/package.json b/enterprise/packages/auth/package.json index 1f00e70b814..92368af56a5 100644 --- a/enterprise/packages/auth/package.json +++ b/enterprise/packages/auth/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-auth", - "version": "0.24.0", + "version": "0.24.1", "private": true, "main": "dist/index.js", "scripts": { @@ -12,9 +12,9 @@ "test-ee": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --timeout 10000 --require ts-node/register --exit --file tests/setup.ts src/**/**/*.spec.ts" }, "dependencies": { - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", "passport-google-oauth": "^2.0.0" }, "devDependencies": { diff --git a/enterprise/packages/billing-web/package.json b/enterprise/packages/billing-web/package.json index 07b2f12454d..6500bf3ab2e 100644 --- a/enterprise/packages/billing-web/package.json +++ b/enterprise/packages/billing-web/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-billing-web", - "version": "0.24.0", + "version": "0.24.1", "description": "", "repository": "https://github.com/novuhq/novu", "license": "ISC", @@ -30,10 +30,10 @@ "@emotion/styled": "^11.6.0", "@mantine/core": "^5.7.1", "@mantine/hooks": "^5.7.1", - "@novu/client": "^0.24.0", - "@novu/design-system": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/shared-web": "^0.24.0", + "@novu/client": "^0.24.1", + "@novu/design-system": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/shared-web": "^0.24.1", "@rjsf/chakra-ui": "^5.17.1", "@rjsf/core": "^5.17.1", "@rjsf/utils": "^5.17.1", diff --git a/enterprise/packages/billing/package.json b/enterprise/packages/billing/package.json index d7019d977b1..f3a7367443d 100644 --- a/enterprise/packages/billing/package.json +++ b/enterprise/packages/billing/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-billing", - "version": "0.24.0", + "version": "0.24.1", "private": true, "main": "dist/index.js", "scripts": { @@ -15,9 +15,9 @@ "dependencies": { "@nestjs/swagger": "^7.1.8", "@nestjs/throttler": "^5.0.1", - "@novu/application-generic": "^0.24.0", - "@novu/ee-dal": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/ee-dal": "^0.24.1", + "@novu/shared": "^0.24.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "date-fns": "^2.29.2", @@ -43,6 +43,6 @@ "@nestjs/common": "10.2.2", "@nestjs/jwt": "10.2.0", "@nestjs/platform-express": "^10.2.2", - "@novu/dal": "^0.24.0" + "@novu/dal": "^0.24.1" } } diff --git a/enterprise/packages/chimera-connect/package.json b/enterprise/packages/chimera-connect/package.json index 69c1afe9aa3..55d2ae757b9 100644 --- a/enterprise/packages/chimera-connect/package.json +++ b/enterprise/packages/chimera-connect/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-chimera-connect", - "version": "0.24.0", + "version": "0.24.1", "private": true, "main": "dist/index.js", "scripts": { @@ -12,11 +12,11 @@ "test-ee": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --timeout 10000 --require ts-node/register --exit --file tests/setup.ts src/**/**/*.spec.ts" }, "dependencies": { - "@novu/ee-dal": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/ee-dal": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/testing": "^0.24.1", "axios": "^1.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", diff --git a/enterprise/packages/chimera/package.json b/enterprise/packages/chimera/package.json index f2889d6aa00..860d8137523 100644 --- a/enterprise/packages/chimera/package.json +++ b/enterprise/packages/chimera/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-chimera", - "version": "0.24.0", + "version": "0.24.1", "private": true, "main": "dist/index.js", "scripts": { @@ -12,12 +12,12 @@ "test-ee": "cross-env TS_NODE_COMPILER_OPTIONS='{\"strictNullChecks\": false}' TZ=UTC NODE_ENV=test E2E_RUNNER=true mocha --timeout 10000 --require ts-node/register --exit --file tests/setup.ts src/**/**/*.spec.ts" }, "dependencies": { - "@novu/application-generic": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/ee-dal": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/testing": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/ee-dal": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/testing": "^0.24.1", "axios": "^1.6.2", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", diff --git a/enterprise/packages/libs/dal/package.json b/enterprise/packages/libs/dal/package.json index 18bef58e056..33a01e3629e 100644 --- a/enterprise/packages/libs/dal/package.json +++ b/enterprise/packages/libs/dal/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-dal", - "version": "0.24.0", + "version": "0.24.1", "description": "", "private": true, "scripts": { @@ -18,8 +18,8 @@ "license": "ISC", "main": "dist/index.js", "dependencies": { - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", "mongoose": "^7.5.0", "mongoose-delete": "^1.0.1", "rimraf": "^3.0.2" diff --git a/enterprise/packages/translation-web/package.json b/enterprise/packages/translation-web/package.json index 7149da3fe92..d58fecae031 100644 --- a/enterprise/packages/translation-web/package.json +++ b/enterprise/packages/translation-web/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-translation-web", - "version": "0.24.0", + "version": "0.24.1", "description": "", "repository": "https://github.com/novuhq/novu", "license": "ISC", @@ -33,10 +33,10 @@ "@mantine/hooks": "^5.7.1", "@mantine/prism": "^5.7.1", "@monaco-editor/react": "^4.6.0", - "@novu/client": "^0.24.0", - "@novu/design-system": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/shared-web": "^0.24.0", + "@novu/client": "^0.24.1", + "@novu/design-system": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/shared-web": "^0.24.1", "@tanstack/react-query": "^4.20.4", "axios": "^1.4.0", "date-fns": "^2.29.2", diff --git a/enterprise/packages/translation/package.json b/enterprise/packages/translation/package.json index b73c052af72..9b737ff7c69 100644 --- a/enterprise/packages/translation/package.json +++ b/enterprise/packages/translation/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ee-translation", - "version": "0.24.0", + "version": "0.24.1", "private": true, "main": "dist/index.js", "scripts": { @@ -14,9 +14,9 @@ "dependencies": { "@handlebars/parser": "^2.1.0", "@nestjs/swagger": "^7.1.8", - "@novu/application-generic": "^0.24.0", - "@novu/ee-dal": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/application-generic": "^0.24.1", + "@novu/ee-dal": "^0.24.1", + "@novu/shared": "^0.24.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "multer": "^1.4.5-lts.1", @@ -41,6 +41,6 @@ "@nestjs/common": "10.2.2", "@nestjs/jwt": "10.2.0", "@nestjs/platform-express": "^10.2.2", - "@novu/dal": "^0.24.0" + "@novu/dal": "^0.24.1" } } diff --git a/enterprise/packages/web/echo/.eslintrc.js b/enterprise/packages/web/echo/.eslintrc.js new file mode 100644 index 00000000000..2f449acbcd9 --- /dev/null +++ b/enterprise/packages/web/echo/.eslintrc.js @@ -0,0 +1,43 @@ +module.exports = { + rules: { + 'func-names': 'off', + 'react/jsx-props-no-spreading': 'off', + 'react/no-array-index-key': 'off', + 'no-empty-pattern': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'react/no-unescaped-entities': 'off', + 'react/jsx-closing-bracket-location': 'off', + '@typescript-eslint/ban-types': 'off', + 'react/jsx-wrap-multilines': 'off', + 'jsx-a11y/anchor-is-valid': 'off', + 'promise/catch-or-return': 'off', + 'react/jsx-one-expression-per-line': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'jsx-a11y/aria-role': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + 'react/require-default-props': 'off', + 'react/no-danger': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + '@typescript-eslint/naming-convention': [ + 'error', + { + filter: '_', + selector: 'variableLike', + leadingUnderscore: 'allow', + format: ['PascalCase', 'camelCase', 'UPPER_CASE'], + }, + ], + '@typescript-eslint/no-empty-function': 'off', + }, + env: { + 'cypress/globals': true, + }, + ignorePatterns: ['craco.config.js', 'cypress/*'], + extends: ['plugin:cypress/recommended', '../../../.eslintrc.js'], + plugins: ['cypress'], + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2020, + sourceType: 'module', + }, +}; diff --git a/enterprise/packages/web/echo/.gitignore b/enterprise/packages/web/echo/.gitignore new file mode 100644 index 00000000000..013add19d46 --- /dev/null +++ b/enterprise/packages/web/echo/.gitignore @@ -0,0 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + + +# production +build +dist + +.npmrc +.idea/* +.nyc_output + +test + +src/**.js +coverage +*.log +package-lock.json + +storybook-static + diff --git a/enterprise/packages/web/echo/check-ee.mjs b/enterprise/packages/web/echo/check-ee.mjs new file mode 100644 index 00000000000..0db73052ff4 --- /dev/null +++ b/enterprise/packages/web/echo/check-ee.mjs @@ -0,0 +1,65 @@ +import spawn from 'cross-spawn'; +import { fileURLToPath } from 'url'; +import path from 'path'; +import * as fs from 'fs'; +const dirname = path.dirname(fileURLToPath(import.meta.url)); + +const ROOT_PATH = path.resolve(dirname); +const ENCODING_TYPE = 'utf8'; +const NEW_LINE_CHAR = '\n'; + +class CliLogs { + constructor() { + this._logs = []; + this.log = this.log.bind(this); + } + + log(log) { + const cleanLog = log.trim(); + if (cleanLog.length) { + this._logs.push(cleanLog); + } + } + + get logs() { + return this._logs; + } + + get joinedLogs() { + return this.logs.join(NEW_LINE_CHAR); + } +} + +function pnpmRun(...args) { + const logData = new CliLogs(); + let pnpmProcess; + + return new Promise((resolve, reject) => { + const processOptions = { + cwd: ROOT_PATH, + env: process.env, + }; + + pnpmProcess = spawn('pnpm', args, processOptions); + + pnpmProcess.stdin.setEncoding(ENCODING_TYPE); + pnpmProcess.stdout.setEncoding(ENCODING_TYPE); + pnpmProcess.stderr.setEncoding(ENCODING_TYPE); + pnpmProcess.stdout.on('data', logData.log); + pnpmProcess.stderr.on('data', logData.log); + + pnpmProcess.on('close', (code) => { + if (code !== 0) { + reject(logData.joinedLogs); + } else { + resolve(logData.joinedLogs); + } + }); + }); +} + +const hasSrcFolder = fs.existsSync(path.resolve(ROOT_PATH, 'src')); +if (hasSrcFolder) { + await pnpmRun('build:esm'); + await pnpmRun('build:types'); +} diff --git a/enterprise/packages/web/echo/package.json b/enterprise/packages/web/echo/package.json new file mode 100644 index 00000000000..4262b7a49de --- /dev/null +++ b/enterprise/packages/web/echo/package.json @@ -0,0 +1,54 @@ +{ + "name": "@novu/ee-echo-web", + "version": "0.24.1", + "description": "", + "repository": "https://github.com/novuhq/novu", + "license": "ISC", + "author": "", + "private": true, + "sideEffects": false, + "module": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "files": [ + "dist/esm" + ], + "scripts": { + "prebuild": "rimraf dist", + "build": "node ./check-ee.mjs", + "build:esm": "cross-env node_modules/.bin/tsc -p tsconfig.esm.json", + "build:esm:watch": "cross-env node_modules/.bin/tsc -p tsconfig.esm.json -w --preserveWatchOutput", + "build:types": "tsc --declaration --emitDeclarationOnly --declarationMap --declarationDir dist/types -p tsconfig.json", + "build:watch": "npm run build:esm:watch", + "lint": "eslint --ext .ts,.tsx src --no-error-on-unmatched-pattern", + "test": "echo 'skip test in the ci'", + "start": "npm run build:watch" + }, + "dependencies": { + "@mantine/core": "^5.7.1", + "@mantine/hooks": "^5.7.1", + "@novu/design-system": "^0.24.1", + "@novu/shared-web": "^0.24.1", + "@rjsf/core": "^5.17.1", + "@rjsf/validator-ajv8": "^5.17.1", + "@tanstack/react-query": "^4.20.4", + "react-router-dom": "6.2.2", + "tslib": "^2.3.1" + }, + "devDependencies": { + "eslint": "^8.33.0", + "eslint-plugin-react-hooks": "^4.4.0", + "@types/node": "^18.11.12", + "@types/react": "^17.0.0", + "@types/react-dom": "^17.0.0", + "react": "^17.0.1", + "react-dom": "^17.0.1", + "rimraf": "^3.0.2", + "ts-loader": "~9.4.0", + "tslib": "^2.3.1", + "typescript": "4.9.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } +} diff --git a/enterprise/packages/web/echo/src b/enterprise/packages/web/echo/src new file mode 120000 index 00000000000..7edcb0e30e4 --- /dev/null +++ b/enterprise/packages/web/echo/src @@ -0,0 +1 @@ +../../../../.source/web/echo/src \ No newline at end of file diff --git a/enterprise/packages/web/echo/tsconfig.esm.json b/enterprise/packages/web/echo/tsconfig.esm.json new file mode 100644 index 00000000000..48e68b0620b --- /dev/null +++ b/enterprise/packages/web/echo/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "outDir": "./dist/esm" + } +} diff --git a/enterprise/packages/web/echo/tsconfig.json b/enterprise/packages/web/echo/tsconfig.json new file mode 100644 index 00000000000..59907bb89b1 --- /dev/null +++ b/enterprise/packages/web/echo/tsconfig.json @@ -0,0 +1,34 @@ +{ + "include": [ + "src" + ], + "extends": "../../../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist/cjs", + "forceConsistentCasingInFileNames": true, + "target": "es6", + "strict": true, + "typeRoots": [ + "./node_modules/@types" + ], + "jsx": "react", + "module": "commonjs", + "lib": [ + "ESNext", + "dom" + ], + "skipLibCheck": true, + "declaration": false, + "declarationMap": false, + "sourceMap": true, + "removeComments": false, + "allowSyntheticDefaultImports": true, + "baseUrl": "." + }, + "exclude": [ + "src/**/*.test.*", + "src/*.test.*", + "node_modules", + "**/node_modules/*" + ] +} diff --git a/lerna.json b/lerna.json index 7fe0be67c6b..ea0c8406a56 100644 --- a/lerna.json +++ b/lerna.json @@ -8,5 +8,5 @@ "message": "chore(release): publish - ci skip" } }, - "version": "0.24.0" + "version": "0.24.1" } diff --git a/libs/dal/package.json b/libs/dal/package.json index 1f2ed28a6fd..b5491c777ca 100644 --- a/libs/dal/package.json +++ b/libs/dal/package.json @@ -1,6 +1,6 @@ { "name": "@novu/dal", - "version": "0.24.0", + "version": "0.24.1", "description": "", "private": true, "scripts": { @@ -24,7 +24,7 @@ "@aws-sdk/client-s3": "^3.382.0", "@aws-sdk/s3-request-presigner": "^3.382.0", "@faker-js/faker": "^6.0.0", - "@novu/shared": "^0.24.0", + "@novu/shared": "^0.24.1", "JSONStream": "^1.3.5", "archiver": "^5.0.0", "async": "^3.2.0", diff --git a/libs/dal/src/repositories/change/change.schema.ts b/libs/dal/src/repositories/change/change.schema.ts index a3bc9f9ae6f..ad4e64605f6 100644 --- a/libs/dal/src/repositories/change/change.schema.ts +++ b/libs/dal/src/repositories/change/change.schema.ts @@ -23,7 +23,7 @@ const changeSchema = new Schema( type: Schema.Types.ObjectId, ref: 'Organization', }, - _entityId: Schema.Types.ObjectId, + _entityId: { type: Schema.Types.ObjectId, index: true }, _creatorId: { type: Schema.Types.ObjectId, ref: 'User', diff --git a/libs/dal/src/repositories/environment/environment.schema.ts b/libs/dal/src/repositories/environment/environment.schema.ts index 9e8dd637713..f973cc63f28 100644 --- a/libs/dal/src/repositories/environment/environment.schema.ts +++ b/libs/dal/src/repositories/environment/environment.schema.ts @@ -15,7 +15,6 @@ const environmentSchema = new Schema( _organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', - index: true, }, apiKeys: [ { @@ -60,6 +59,22 @@ const environmentSchema = new Schema( schemaOptions ); +/* + * Path: ./get-platform-notification-usage.usecase.ts + * Context: execute() + * Query: organizationRepository.aggregate( + * $lookup: + * { + * from: 'environments', + * localField: '_id', + * foreignField: '_organizationId', + * as: 'environments', + * } + */ +environmentSchema.index({ + _organizationId: 1, +}); + // eslint-disable-next-line @typescript-eslint/naming-convention export const Environment = (mongoose.models.Environment as mongoose.Model) || diff --git a/libs/dal/src/repositories/integration/integration.schema.ts b/libs/dal/src/repositories/integration/integration.schema.ts index fe689fea143..bec9530f52a 100644 --- a/libs/dal/src/repositories/integration/integration.schema.ts +++ b/libs/dal/src/repositories/integration/integration.schema.ts @@ -15,7 +15,6 @@ const integrationSchema = new Schema( _organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', - index: true, }, providerId: Schema.Types.String, channel: Schema.Types.String, @@ -95,6 +94,11 @@ const integrationSchema = new Schema( schemaOptions ); +integrationSchema.index({ + _organizationId: 1, + active: 1, +}); + integrationSchema.plugin(mongooseDelete, { deletedAt: true, deletedBy: true, overrideMethods: 'all' }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/libs/dal/src/repositories/layout/layout.schema.ts b/libs/dal/src/repositories/layout/layout.schema.ts index 6a936ffb97b..4713a4b60b5 100644 --- a/libs/dal/src/repositories/layout/layout.schema.ts +++ b/libs/dal/src/repositories/layout/layout.schema.ts @@ -10,6 +10,7 @@ const layoutSchema = new Schema( _environmentId: { type: Schema.Types.ObjectId, ref: 'Environment', + index: true, }, _organizationId: { type: Schema.Types.ObjectId, diff --git a/libs/dal/src/repositories/message-template/message-template.schema.ts b/libs/dal/src/repositories/message-template/message-template.schema.ts index 514566289bb..04ec1cbc60c 100644 --- a/libs/dal/src/repositories/message-template/message-template.schema.ts +++ b/libs/dal/src/repositories/message-template/message-template.schema.ts @@ -89,6 +89,10 @@ messageTemplateSchema.index({ 'triggers.identifier': 1, }); +messageTemplateSchema.index({ + _parentId: 1, +}); + messageTemplateSchema.plugin(mongooseDelete, { deletedAt: true, deletedBy: true, overrideMethods: 'all' }); // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/libs/dal/src/repositories/notification-template/notification-template.repository.ts b/libs/dal/src/repositories/notification-template/notification-template.repository.ts index 9bc69f81f34..7f1c84e8832 100644 --- a/libs/dal/src/repositories/notification-template/notification-template.repository.ts +++ b/libs/dal/src/repositories/notification-template/notification-template.repository.ts @@ -56,7 +56,10 @@ export class NotificationTemplateRepository extends BaseRepository< _id: id, }; - const item = await this.MongooseModel.findOne(requestQuery).populate('steps.template').lean(); + const item = await this.MongooseModel.findOne(requestQuery) + .populate('steps.template') + .populate('notificationGroup') + .lean(); return this.mapEntity(item); } @@ -70,7 +73,10 @@ export class NotificationTemplateRepository extends BaseRepository< triggers: { $elemMatch: { identifier: identifier } }, }; - const item = await this.MongooseModel.findOne(requestQuery).populate('steps.template').lean(); + const item = await this.MongooseModel.findOne(requestQuery) + .populate('steps.template') + .populate('notificationGroup') + .lean(); return this.mapEntity(item); } diff --git a/libs/dal/src/repositories/notification/notification.schema.ts b/libs/dal/src/repositories/notification/notification.schema.ts index a9c50e2c4d9..61f560233a7 100644 --- a/libs/dal/src/repositories/notification/notification.schema.ts +++ b/libs/dal/src/repositories/notification/notification.schema.ts @@ -10,26 +10,21 @@ const notificationSchema = new Schema( _templateId: { type: Schema.Types.ObjectId, ref: 'NotificationTemplate', - index: true, }, _environmentId: { type: Schema.Types.ObjectId, ref: 'Environment', - index: true, }, _organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', - index: true, }, _subscriberId: { type: Schema.Types.ObjectId, ref: 'Subscriber', - index: true, }, transactionId: { type: Schema.Types.String, - index: true, }, channels: [ { @@ -140,6 +135,18 @@ notificationSchema.index({ * _environmentId: this.convertStringToObjectId(environmentId), * createdAt: {$gte: monthBefore} * weekly: { $sum: { $cond: [{ $gte: ['$createdAt', weekBefore] }, 1, 0] } }, + * + * + * Path: ./get-platform-notification-usage.usecase.ts + * Context: execute() + * Query: organizationRepository.aggregate( + * $lookup: + * { + * from: 'notifications', + * localField: 'environments._id', + * foreignField: '_environmentId', + * as: 'notifications', + * } */ notificationSchema.index({ _environmentId: 1, diff --git a/libs/dal/src/repositories/organization/organization.entity.ts b/libs/dal/src/repositories/organization/organization.entity.ts index 2d5790fcec4..23669ad663d 100644 --- a/libs/dal/src/repositories/organization/organization.entity.ts +++ b/libs/dal/src/repositories/organization/organization.entity.ts @@ -30,6 +30,8 @@ export class OrganizationEntity implements IOrganizationEntity { createdAt: string; updatedAt: string; + + externalId?: string; } export type OrganizationDBModel = OrganizationEntity; diff --git a/libs/dal/src/repositories/organization/organization.repository.ts b/libs/dal/src/repositories/organization/organization.repository.ts index 3d4cf08280b..1c5392590f1 100644 --- a/libs/dal/src/repositories/organization/organization.repository.ts +++ b/libs/dal/src/repositories/organization/organization.repository.ts @@ -1,4 +1,4 @@ -import { IPartnerConfiguration, OrganizationEntity, OrganizationDBModel } from './organization.entity'; +import { IPartnerConfiguration, OrganizationDBModel, OrganizationEntity } from './organization.entity'; import { BaseRepository } from '../base-repository'; import { Organization } from './organization.schema'; import { MemberRepository } from '../member'; @@ -12,25 +12,24 @@ export class OrganizationRepository extends BaseRepository { - const data = await this.MongooseModel.findById(id, select); + const data = await this.MongooseModel.findById(id, select).read('secondaryPreferred'); if (!data) return null; return this.mapEntity(data.toObject()); } - async findOrganizationById(organizationId: string): Promise { - const data = await this.MongooseModel.findById(organizationId).read('secondaryPreferred'); - if (!data) return null; + async findUserActiveOrganizations(userId: string): Promise { + const organizationIds = await this.getUsersMembersOrganizationIds(userId); - return this.mapEntity(data.toObject()); + return await this.find({ + _id: { $in: organizationIds }, + }); } - async findUserActiveOrganizations(userId: string): Promise { + private async getUsersMembersOrganizationIds(userId: string): Promise { const members = await this.memberRepository.findUserActiveMembers(userId); - return await this.find({ - _id: members.map((member) => member._organizationId), - }); + return members.map((member) => member._organizationId); } async updateBrandingDetails(organizationId: string, branding: { color: string; logo: string }) { @@ -73,11 +72,11 @@ export class OrganizationRepository extends BaseRepository member._organizationId), + _id: { $in: organizationIds }, 'partnerConfigurations.configurationId': configurationId, }, { 'partnerConfigurations.$': 1 } @@ -85,11 +84,11 @@ export class OrganizationRepository extends BaseRepository member._organizationId), + _id: { $in: organizationIds }, }, { $push: { @@ -100,11 +99,10 @@ export class OrganizationRepository extends BaseRepository, configurationId: string) { - const members = await this.memberRepository.findUserActiveMembers(userId); - const allOrgs = members.map((member) => member._organizationId); + const organizationIds = await this.getUsersMembersOrganizationIds(userId); const usedOrgIds = Object.keys(data); - const unusedOrgIds = allOrgs.filter((org) => !usedOrgIds.includes(org)); - const bulkWriteOps = allOrgs.map((orgId) => { + const unusedOrgIds = organizationIds.filter((org) => !usedOrgIds.includes(org)); + const bulkWriteOps = organizationIds.map((orgId) => { return { updateOne: { filter: { _id: orgId, 'partnerConfigurations.configurationId': configurationId }, diff --git a/libs/dal/src/repositories/organization/organization.schema.ts b/libs/dal/src/repositories/organization/organization.schema.ts index 86113eb7936..90dbdac2ad3 100644 --- a/libs/dal/src/repositories/organization/organization.schema.ts +++ b/libs/dal/src/repositories/organization/organization.schema.ts @@ -60,6 +60,7 @@ const organizationSchema = new Schema( default: false, }, }, + externalId: Schema.Types.String, }, schemaOptions ); diff --git a/libs/dal/src/repositories/subscriber/subscriber.repository.ts b/libs/dal/src/repositories/subscriber/subscriber.repository.ts index 3c2fd22aa85..5fc6defe3eb 100644 --- a/libs/dal/src/repositories/subscriber/subscriber.repository.ts +++ b/libs/dal/src/repositories/subscriber/subscriber.repository.ts @@ -10,6 +10,8 @@ import type { EnforceEnvOrOrgIds } from '../../types'; import { EnvironmentId, ISubscribersDefine, OrganizationId } from '@novu/shared'; type SubscriberQuery = FilterQuery & EnforceEnvOrOrgIds; +type SubscriberDeleteQuery = Pick & EnforceEnvOrOrgIds; +type SubscriberDeleteManyQuery = Pick & EnforceEnvOrOrgIds; export class SubscriberRepository extends BaseRepository { private subscriber: SoftDeleteModel; @@ -151,21 +153,31 @@ export class SubscriberRepository extends BaseRepository( { _organizationId: { type: Schema.Types.ObjectId, ref: 'Organization', - index: true, }, _environmentId: { type: Schema.Types.ObjectId, ref: 'Environment', - index: true, }, firstName: Schema.Types.String, lastName: Schema.Types.String, @@ -165,11 +164,24 @@ subscriberSchema.index({ * subscriberId: /on-boarding-subscriber/i, * }); */ -subscriberSchema.index({ - subscriberId: 1, - _environmentId: 1, - _id: 1, -}); + +/* + * This index needs to be unique and exclude "_id" to prevent duplicate subscribers during concurrent creation attempts. + * This situation could occur if two attempts are made to create a subscriber with the same subscriberId (e.g., 2022) simultaneously. + * We want to ensure that the _id field is not included in the index to avoid scenarios where MongoDB's unique validation fails to prevent duplicates, such as: + * subscriberId_2022:environmentId_123:_id_123 + * subscriberId_2022:environmentId_123:_id_1234 + * We expect an exception to be thrown when attempting to create two subscribers with the same subscriberId (e.g., 2022) within the same environment. + * + * We can not add `deleted` field to the index the client wont be able to delete twice subscriber with the same subscriberId. + */ +index( + { + subscriberId: 1, + _environmentId: 1, + }, + { unique: true } +); subscriberSchema.plugin(mongooseDelete, { deletedAt: true, deletedBy: true, overrideMethods: 'all' }); @@ -177,3 +189,7 @@ subscriberSchema.plugin(mongooseDelete, { deletedAt: true, deletedBy: true, over export const Subscriber = (mongoose.models.Subscriber as mongoose.Model) || mongoose.model('Subscriber', subscriberSchema); + +function index(fields: IndexDefinition, options?: IndexOptions) { + subscriberSchema.index(fields, options); +} diff --git a/libs/dal/src/repositories/user/user.entity.ts b/libs/dal/src/repositories/user/user.entity.ts index 3cf131ac94c..d1dce167702 100644 --- a/libs/dal/src/repositories/user/user.entity.ts +++ b/libs/dal/src/repositories/user/user.entity.ts @@ -53,6 +53,8 @@ export class UserEntity implements IUserEntity { servicesHashes?: { intercom?: string }; jobTitle?: JobTitleEnum; + + externalId?: string; } export type UserDBModel = UserEntity; diff --git a/libs/dal/src/repositories/user/user.schema.ts b/libs/dal/src/repositories/user/user.schema.ts index 26541b276c1..8d55b3909cc 100644 --- a/libs/dal/src/repositories/user/user.schema.ts +++ b/libs/dal/src/repositories/user/user.schema.ts @@ -41,6 +41,7 @@ const userSchema = new Schema( intercom: Schema.Types.String, }, jobTitle: Schema.Types.String, + externalId: Schema.Types.String, }, schemaOptions ); diff --git a/libs/dal/src/shared/types/index.ts b/libs/dal/src/shared/types/index.ts new file mode 100644 index 00000000000..4686f8ab840 --- /dev/null +++ b/libs/dal/src/shared/types/index.ts @@ -0,0 +1 @@ +export * from './index.type'; diff --git a/libs/dal/src/shared/types/index.type.ts b/libs/dal/src/shared/types/index.type.ts new file mode 100644 index 00000000000..9a14db2cab2 --- /dev/null +++ b/libs/dal/src/shared/types/index.type.ts @@ -0,0 +1,3 @@ +import { IndexDirection } from 'mongoose'; + +export type IndexDefinition = Partial>; diff --git a/libs/dal/src/types/error.enum.ts b/libs/dal/src/types/error.enum.ts new file mode 100644 index 00000000000..aa09fcf046f --- /dev/null +++ b/libs/dal/src/types/error.enum.ts @@ -0,0 +1,3 @@ +export enum ErrorCodesEnum { + DUPLICATE_KEY = '11000', +} diff --git a/libs/dal/src/types/index.ts b/libs/dal/src/types/index.ts index 75cb418f741..e05844d67f1 100644 --- a/libs/dal/src/types/index.ts +++ b/libs/dal/src/types/index.ts @@ -1,3 +1,4 @@ export * from './enforce'; export * from './helpers'; export * from './results'; +export * from './error.enum'; diff --git a/libs/design-system/.gitignore b/libs/design-system/.gitignore index 1b5e637ef2d..0a8b383a027 100644 --- a/libs/design-system/.gitignore +++ b/libs/design-system/.gitignore @@ -27,4 +27,7 @@ storybook-static ## Panda styled-system -styled-system-studio \ No newline at end of file +styled-system-studio + +# react-scanner +src/component-audit/component-scans diff --git a/libs/design-system/.storybook/NovuTheme.tsx b/libs/design-system/.storybook/NovuTheme.tsx new file mode 100644 index 00000000000..d8f509b6114 --- /dev/null +++ b/libs/design-system/.storybook/NovuTheme.tsx @@ -0,0 +1,23 @@ +import { ThemeVarsPartial } from '@storybook/theming'; +import { create } from '@storybook/theming/create'; + +const themeBase: ThemeVarsPartial = { + base: 'light', + brandTitle: 'Novu Design System', + brandTarget: '_self', +} +/** + * Novu Design System theme for Storybook + * + * @see https://storybook.js.org/docs/configure/theming + */ +export const lightTheme = create({ + ...themeBase, + brandImage: './novu-logo-light.svg', +}); + +export const darkTheme = create({ + ...themeBase, + base: 'dark', + brandImage: './novu-logo-dark.svg', +}); diff --git a/libs/design-system/.storybook/main.js b/libs/design-system/.storybook/main.ts similarity index 75% rename from libs/design-system/.storybook/main.js rename to libs/design-system/.storybook/main.ts index a1e2ad206ab..d28b5b1dcfe 100644 --- a/libs/design-system/.storybook/main.js +++ b/libs/design-system/.storybook/main.ts @@ -1,5 +1,7 @@ import { dirname, join } from 'path'; -module.exports = { +import { StorybookConfig } from '@storybook/react-webpack5'; + +export default { stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ @@ -10,7 +12,7 @@ module.exports = { ], framework: { - name: getAbsolutePath('@storybook/react-webpack5'), + name: '@storybook/react-webpack5', options: {}, }, @@ -21,7 +23,9 @@ module.exports = { docs: { autodocs: true, }, -}; + + staticDirs: ['./public'], +} satisfies StorybookConfig; function getAbsolutePath(value) { return dirname(require.resolve(join(value, 'package.json'))); diff --git a/libs/design-system/.storybook/manager-head.html b/libs/design-system/.storybook/manager-head.html new file mode 100644 index 00000000000..62499dd0581 --- /dev/null +++ b/libs/design-system/.storybook/manager-head.html @@ -0,0 +1 @@ + diff --git a/libs/design-system/.storybook/preview.jsx b/libs/design-system/.storybook/preview.tsx similarity index 69% rename from libs/design-system/.storybook/preview.jsx rename to libs/design-system/.storybook/preview.tsx index 75a12da9c8b..f51174ac66d 100644 --- a/libs/design-system/.storybook/preview.jsx +++ b/libs/design-system/.storybook/preview.tsx @@ -4,15 +4,18 @@ import { DARK_MODE_EVENT_NAME } from 'storybook-dark-mode'; import { ThemeProvider } from '../src/ThemeProvider'; import { DocsContainer } from './Doc.container'; import { useLocalThemePreference } from '@novu/shared-web'; +import { lightTheme, darkTheme } from './NovuTheme'; +import { Parameters, Decorator } from '@storybook/react' // Bring in the Panda-generated stylesheets import '../styled-system/styles.css'; -export const parameters = { +export const parameters: Parameters = { layout: 'fullscreen', viewMode: 'docs', docs: { - container: DocsContainer, + // @TODO: fix the container context + // container: DocsContainer, }, actions: { argTypesRegex: '^on[A-Z].*' }, controls: { @@ -21,6 +24,12 @@ export const parameters = { date: /Date$/, }, }, + darkMode: { + // Override the default dark theme + dark: darkTheme, + // Override the default light theme + light: lightTheme + } }; const channel = addons.getChannel(); @@ -43,4 +52,4 @@ function ColorSchemeThemeWrapper({ children }) { ); } -export const decorators = [(renderStory) => {renderStory()}]; +export const decorators: Decorator[] = [(renderStory) => {renderStory()}]; diff --git a/libs/design-system/.storybook/public/favicon.svg b/libs/design-system/.storybook/public/favicon.svg new file mode 100644 index 00000000000..b7046b6bb99 --- /dev/null +++ b/libs/design-system/.storybook/public/favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/libs/design-system/.storybook/public/novu-logo-dark.svg b/libs/design-system/.storybook/public/novu-logo-dark.svg new file mode 100644 index 00000000000..cbe6662e368 --- /dev/null +++ b/libs/design-system/.storybook/public/novu-logo-dark.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/libs/design-system/.storybook/public/novu-logo-light.svg b/libs/design-system/.storybook/public/novu-logo-light.svg new file mode 100644 index 00000000000..2c80902b29c --- /dev/null +++ b/libs/design-system/.storybook/public/novu-logo-light.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/libs/design-system/package.json b/libs/design-system/package.json index fdda4438a7e..290591cb20d 100644 --- a/libs/design-system/package.json +++ b/libs/design-system/package.json @@ -1,6 +1,6 @@ { "name": "@novu/design-system", - "version": "0.24.0", + "version": "0.24.1", "repository": "https://github.com/novuhq/novu", "description": "", "private": true, @@ -16,7 +16,9 @@ "dist/types" ], "scripts": { - "prepare": "panda codegen --clean", + "prepare": "pnpm prepare:panda && pnpm prepare:audit", + "prepare:panda": "panda codegen --clean", + "prepare:audit": "pnpm audit-components", "start": "npm run build:watch", "prebuild": "rimraf dist", "lint": "eslint --ext .ts,.tsx src", @@ -28,11 +30,12 @@ "build:esm:watch": "cross-env node_modules/.bin/tsc -p tsconfig.esm.json -w --preserveWatchOutput", "build:types": "tsc --declaration --emitDeclarationOnly --declarationMap --declarationDir dist/types -p tsconfig.json", "storybook": "pnpm panda --watch & storybook dev -p 6006", - "build-storybook": "storybook build", + "build-storybook": "pnpm panda && storybook build", "cypress:install": "cypress install", "cypress:open": "cross-env NODE_ENV=test cypress open", "cypress:run": "cross-env NODE_OPTIONS=--max_old_space_size=4096 NODE_ENV=test cypress run --component", - "test": "vitest" + "test": "vitest", + "audit-components": "pnpm react-scanner -c './react-scanner.config.js'" }, "dependencies": { "@cypress/react": "^7.0.3", @@ -42,9 +45,9 @@ "@mantine/core": "^5.7.1", "@mantine/hooks": "^5.7.1", "@mantine/notifications": "^5.7.1", - "@novu/client": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/shared-web": "^0.24.0", + "@novu/client": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/shared-web": "^0.24.1", "@segment/analytics-next": "1.59.0", "@sentry/react": "^7.40.0", "@tanstack/react-query": "^4.20.4", @@ -80,6 +83,7 @@ "react": "^17.0.1", "react-dom": "^17.0.1", "react-router-dom": "6.2.2", + "react-scanner": "^1.1.0", "rimraf": "^3.0.2", "storybook": "^7.4.2", "ts-loader": "~9.4.0", @@ -93,5 +97,14 @@ "peerDependencies": { "react": "^16.8.0 || ^17.0.0", "react-dom": "^16.8.0 || ^17.0.0" + }, + "nx": { + "targets": { + "build-storybook": { + "dependsOn": ["^build"], + "outputs": ["{projectRoot}/storybook-static"], + "inputs": ["{projectRoot}/.storybook", "{projectRoot}/src"] + } + } } } diff --git a/libs/design-system/react-scanner.config.js b/libs/design-system/react-scanner.config.js new file mode 100644 index 00000000000..16615ba23c9 --- /dev/null +++ b/libs/design-system/react-scanner.config.js @@ -0,0 +1,153 @@ +/** + * Configuration file for react-scanner: https://github.com/moroshko/react-scanner + * + * Used to assess usage of Mantine and Design System components in web. + * + * To use: + * 1. Run `pnpm audit-components` + * 2. Check `OUTPUT_PATH` for your scan results! + */ + +/** the path of the scan output */ +const OUTPUT_PATH = './src/component-audit/component-scans'; +const OUTPUT_FILE_NAME = 'scan'; +const OUTPUT_FILE_EXTENSION = 'json'; + +/** + * @param {string} suffix Optional filename suffix + * @returns file path for the output file + */ +const getOutputFilePath = (suffix) => { + return `${OUTPUT_PATH}/${OUTPUT_FILE_NAME}${suffix ?? ''}.${OUTPUT_FILE_EXTENSION}`; +}; + +const NOVU_ICON_REGEX = /^Icon(?!Button)[A-Z0-9]{1}[a-zA-Z0-9]+$/; +const RELATIVE_PATH_REGEX = /^(\.(\.){0,}\/)/; +const ANTD_ICON_MODULE_NAME = '@ant-design/icons'; + +const COMPONENT_NAME_EXCLUSION_REGEX = /^web\/.*(Page|Container|Provider|Sidebar|Modal)$/; + +module.exports = { + /** directory to scan */ + crawlFrom: '../../apps/web/src/', + includeSubComponents: true, + /** + * Regex for determining which imports to include. + * Currently includes: novu, antd, mantine, and local imports + * + * To see only local imports, replace with: /(\.(\.){0,}\/.*)/gim + */ + importedFrom: + /(@novu\/(design-system|shared-web|notification-center)|@mantine\/core|@ant-design)(\/[a-z0-9\-)]+){0,}|(\.(\.){0,}\/.*)/gim, + exclude: ['/src/api', '/src/styled-system'], + processors: [countComponentsAndPropsProcessor({ minNumInstances: 1 }), groupByNamespaceProcessor], + /** file patterns to scan */ + globs: ['**/!(*.test|*.spec|*.stories).@(js|ts)x'], + /** function for naming components -- we use the returned name as the "group by" key. */ + getComponentName, +}; + +function getComponentName({ imported, local, moduleName, importType }) { + const importedName = imported ?? local; + + // any relative imports should return early and be scoped to "web" + if (RELATIVE_PATH_REGEX.test(moduleName)) { + return `web/${importedName}`; + } + + // get the module namespace / org (AKA novu or mantine), but remove @ + const moduleOrg = moduleName.split('/').join('_').replace('@', ''); + + // group Icons if from Novu Design System or AntD + const name = + (moduleName === '@novu/design-system' && NOVU_ICON_REGEX.test(importedName)) || moduleName === ANTD_ICON_MODULE_NAME + ? 'Icon' + : importedName; + + return `${moduleOrg}/${name}`; +} + +/** + * @param {object} _ with the following properties: + * @param {number} minNumInstances Minimum instance count (inclusive) of a component to include it in the output + * @default 1 + * + * Extension of a built-in processor from react-scanner to make it easier to customize. + * https://github.com/moroshko/react-scanner/blob/master/src/processors/count-components-and-props.js + */ +function countComponentsAndPropsProcessor({ minNumInstances = 1 } = {}) { + return function ({ forEachComponent, sortObjectKeysByValue, output }) { + let result = {}; + + forEachComponent(({ componentName, component }) => { + const { instances } = component; + + if (!instances || instances.length < minNumInstances) { + return; + } + + if (COMPONENT_NAME_EXCLUSION_REGEX.test(componentName)) { + console.log('Excluding component ' + componentName); + + return; + } + + // include the package source as a prop + const [srcPkg] = componentName.split('/'); + + result[componentName] = { + instances: instances.length, + props: {}, + srcPkg, + }; + + instances.forEach((instance) => { + for (const prop in instance.props) { + if (result[componentName].props[prop] === undefined) { + result[componentName].props[prop] = 0; + } + + result[componentName].props[prop] += 1; + } + + // aggregate icon names and output as a prop to stay consistent across all output components. + if (componentName.includes('/Icon')) { + const iconName = instance.importInfo.imported; + const existingIconNames = result[componentName].props.iconNames; + + result[componentName].props.iconNames = existingIconNames ? existingIconNames.concat(iconName) : [iconName]; + } + }); + + result[componentName].props = sortObjectKeysByValue(result[componentName].props); + }); + + result = sortObjectKeysByValue(result, (component) => component.instances); + + output(result, getOutputFilePath()); + + return result; + }; +} + +/** + * @precondition Must be called after `countComponentsAndPropsProcessor` in the processors array. + * Processor for grouping by namespace (i.e. Novu, Mantine, etc) + */ +function groupByNamespaceProcessor({ prevResult, output }) { + const result = Object.entries(prevResult).reduce((groupedResult, [compKey, compVal]) => { + const [namespace, compName] = compKey.split('/'); + + return { + ...groupedResult, + [namespace]: { + ...groupedResult[namespace], + [compName]: compVal, + }, + }; + }, {}); + + output(result, getOutputFilePath('.grouped')); + + return result; +} diff --git a/libs/design-system/src/component-audit/ComponentAuditTable.stories.tsx b/libs/design-system/src/component-audit/ComponentAuditTable.stories.tsx new file mode 100644 index 00000000000..5b171fcd987 --- /dev/null +++ b/libs/design-system/src/component-audit/ComponentAuditTable.stories.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { StoryFn, Meta } from '@storybook/react'; +import { ComponentAuditTable } from './ComponentAuditTable'; +import { css } from '../../styled-system/css'; + +import scanJson from './component-scans/scan.json'; + +export default { + title: 'ComponentAudit', + component: ComponentAuditTable, + argTypes: {}, +} as Meta; + +const TableWrapper = ({ children }: { children: React.ReactNode }) => { + return ( +
+ {children} +
+ ); +}; + +const Template: StoryFn = ({ ...args }) => ( + <> +

+ If no data is appearing below, please run `pnpm audit-components` in your terminal in the `design-system` + directory +

+
+ + + + +); + +export const ComponentAudit = Template.bind({}); +ComponentAudit.args = {}; diff --git a/libs/design-system/src/component-audit/ComponentAuditTable.tsx b/libs/design-system/src/component-audit/ComponentAuditTable.tsx new file mode 100644 index 00000000000..5a3227f97de --- /dev/null +++ b/libs/design-system/src/component-audit/ComponentAuditTable.tsx @@ -0,0 +1,159 @@ +import React, { useMemo, useState } from 'react'; +import { css } from '../../styled-system/css'; + +interface JsonData { + [key: string]: { + instances: number; + props: { + [key: string]: number | string[]; + }; + srcPkg: string; + }; +} + +interface ComponentAuditTableProps { + data: JsonData; + className?: string; +} + +type SortableKey = keyof Omit | 'name'; + +export const ComponentAuditTable: React.FC = ({ data: jsonData, className }) => { + const [expandedRows, setExpandedRows] = useState([]); + const [sortColumn, setSortColumn] = useState(null); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc' | null>(null); + + const data = useMemo(() => { + return Object.entries(jsonData).map(([key, value]) => ({ ...value, name: key.split('/')[1] })); + }, [jsonData]); + + const toggleRow = (name: string) => { + setExpandedRows((prevState) => + prevState.includes(name) ? prevState.filter((row) => row !== name) : [...prevState, name] + ); + }; + + const sortData = (key: SortableKey) => { + if (sortColumn === key) { + setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc'); + } else { + setSortColumn(key); + setSortOrder('asc'); + } + }; + + const getSortIcon = (key: SortableKey) => { + if (sortColumn !== key) return null; + if (sortOrder === 'asc') return '⬆️'; + if (sortOrder === 'desc') return '⬇️'; + + return null; + }; + + const sortedData = sortColumn + ? data.sort((a, b) => { + const aValue = a[sortColumn]; + const bValue = b[sortColumn]; + + if (typeof aValue === 'string' && typeof bValue === 'string') { + return sortOrder === 'asc' ? aValue.localeCompare(bValue) : bValue.localeCompare(aValue); + } + + if (typeof aValue === 'number' && typeof bValue === 'number') { + return sortOrder === 'asc' ? aValue - bValue : bValue - aValue; + } + + return 0; + }) + : data; + + return ( + + + + + + + + + + + {sortedData.map(({ name, instances, props, srcPkg }) => ( + + + + + + + + {expandedRows.includes(name) && ( + + + + )} + + ))} + +
sortData('name')} + > + Name {getSortIcon('name')} + sortData('instances')} + > + Instance Count {getSortIcon('instances')} + sortData('srcPkg')} + > + Source {getSortIcon('srcPkg')} + Props
{name}{instances}{srcPkg} + toggleRow(name)} + > + {Object.entries(props) + .slice(0, 3) + .map(([prop, value]) => `${prop}: ${typeof value === 'number' ? value : value.join(', ')}`) + .join(', ')} + {Object.keys(props).length > 3 && '...'} + +
+
+ {Object.entries(props).map(([prop, value]) => ( +
+ {prop}: + {typeof value === 'number' ? value : value.join(', ')} +
+ ))} +
+
+ ); +}; diff --git a/libs/design-system/src/panda/colors.tokens.ts b/libs/design-system/src/panda/colors.tokens.ts index 22a6b42e607..c2c0f80aa2e 100644 --- a/libs/design-system/src/panda/colors.tokens.ts +++ b/libs/design-system/src/panda/colors.tokens.ts @@ -1,4 +1,4 @@ -import { defineTokens, defineSemanticTokens } from '@pandacss/dev'; +import { defineTokens } from '@pandacss/dev'; /** * @deprecated @@ -280,115 +280,3 @@ export const COLOR_PALETTE_TOKENS = defineTokens.colors({ }, }, }); - -export const COLOR_SEMANTIC_TOKENS = defineSemanticTokens.colors({ - typography: { - text: { - feedback: { - alert: { - value: { base: '{colors.red.20.light}', _dark: '{colors.red.20.dark}' }, - type: 'color', - }, - warning: { - value: { base: '{colors.amber.30.light}', _dark: '{colors.amber.30.dark}' }, - type: 'color', - }, - info: { - value: { base: '{colors.blue.20.light}', _dark: '{colors.blue.20.dark}' }, - type: 'color', - }, - success: { - value: { base: '{colors.green.20.light}', _dark: '{colors.green.20.dark}' }, - type: 'color', - }, - }, - main: { - value: { base: '{colors.mauve.10.light}', _dark: '{colors.mauve.10.dark}' }, - type: 'color', - }, - secondary: { - value: { base: '{colors.mauve.20.light}', _dark: '{colors.mauve.20.dark}' }, - type: 'color', - }, - disabled: { - value: { base: '{colors.mauve.30.light}', _dark: '{colors.mauve.30.dark}' }, - type: 'color', - }, - accent: { - value: { base: '{colors.blue.20.light}', _dark: '{colors.blue.20.dark}' }, - type: 'color', - }, - }, - }, - button: { - hovered: { - background: { - value: { base: '{colors.blue.30.light}', _dark: '{colors.blue.30.dark}' }, - type: 'color', - }, - border: { - value: { base: '{colors.blue.30.light}', _dark: '{colors.blue.30.dark}' }, - type: 'color', - }, - text: { - value: { base: '{colors.blue.120.light}', _dark: '{colors.blue.120.dark}' }, - type: 'color', - }, - }, - pressed: { - background: { - value: { base: '{colors.blue.20.light}', _dark: '{colors.blue.20.dark}' }, - type: 'color', - }, - border: { - value: { base: '{colors.blue.20.light}', _dark: '{colors.blue.20.dark}' }, - type: 'color', - }, - text: { - value: { base: '{colors.blue.120.light}', _dark: '{colors.blue.120.dark}' }, - type: 'color', - }, - }, - disabled: { - background: { - value: { base: '{colors.mauve.10.light}', _dark: '{colors.mauve.10.dark}' }, - type: 'color', - }, - border: { - value: { base: '{colors.mauve.50.light}', _dark: '{colors.mauve.50.dark}' }, - type: 'color', - }, - text: { - // this was a self-referential token (referring to semantic.colors.typography.text.disabled) - value: { base: '{colors.mauve.30.light}', _dark: '{colors.mauve.30.dark}' }, - type: 'color', - }, - }, - background: { - value: { base: '{colors.blue.40.light}', _dark: '{colors.blue.40.dark}' }, - type: 'color', - }, - border: { - value: { base: '{colors.blue.40.light}', _dark: '{colors.blue.40.dark}' }, - type: 'color', - }, - text: { - value: { base: '{colors.blue.120.light}', _dark: '{colors.blue.120.dark}' }, - type: 'color', - }, - }, - surface: { - page: { - value: { base: '{colors.mauve.100.light}', _dark: '{colors.mauve.100.dark}' }, - type: 'color', - }, - panel: { - value: { base: '{colors.mauve.110.light}', _dark: '{colors.mauve.110.dark}' }, - type: 'color', - }, - popover: { - value: { base: '{colors.mauve.120.light}', _dark: '{colors.mauve.120.dark}' }, - type: 'color', - }, - }, -}); diff --git a/libs/design-system/src/panda/semanticColors.tokens.ts b/libs/design-system/src/panda/semanticColors.tokens.ts index 391eebc4dfb..7cec016e1c1 100644 --- a/libs/design-system/src/panda/semanticColors.tokens.ts +++ b/libs/design-system/src/panda/semanticColors.tokens.ts @@ -8,11 +8,11 @@ export const LEGACY_COLOR_SEMANTIC_TOKENS = defineSemanticTokens.colors({ type: 'color', }, panel: { - value: { base: '{colors.legacy.BGDark}', _dark: '{colors.legacy.BGLight}' }, + value: { base: '{colors.legacy.BGLight}', _dark: '{colors.legacy.BGDark}' }, type: 'color', }, popover: { - value: { base: '{colors.mauve.120.light}', _dark: '{colors.mauve.120.dark}' }, + value: { base: '{colors.legacy.white}', _dark: '{colors.legacy.B20}' }, type: 'color', }, }, @@ -30,16 +30,25 @@ export const LEGACY_COLOR_SEMANTIC_TOKENS = defineSemanticTokens.colors({ value: { base: '{colors.legacy.B70}', _dark: '{colors.legacy.B40}' }, type: 'color', }, - /* - * disabled: { - * value: { base: '{colors.legacy.mauve.30.light}', _dark: '{colors.legacy.mauve.30.dark}' }, - * type: 'color', - * }, - * accent: { - * value: { base: '{colors.legacy.blue.20.light}', _dark: '{colors.legacy.blue.20.dark}' }, - * type: 'color', - * }, - */ + // not actually legacy, but makes the merging of the two easier for now. + feedback: { + alert: { + value: { base: '{colors.red.20.light}', _dark: '{colors.red.20.dark}' }, + type: 'color', + }, + warning: { + value: { base: '{colors.amber.30.light}', _dark: '{colors.amber.30.dark}' }, + type: 'color', + }, + info: { + value: { base: '{colors.blue.20.light}', _dark: '{colors.blue.20.dark}' }, + type: 'color', + }, + success: { + value: { base: '{colors.green.20.light}', _dark: '{colors.green.20.dark}' }, + type: 'color', + }, + }, }, }, }); diff --git a/libs/embed/package.json b/libs/embed/package.json index 5b7e53851df..aa90432b5f9 100644 --- a/libs/embed/package.json +++ b/libs/embed/package.json @@ -1,6 +1,6 @@ { "name": "@novu/embed", - "version": "0.24.0", + "version": "0.24.1", "private": true, "description": "", "keywords": [], @@ -117,7 +117,7 @@ "typescript": "4.9.5" }, "dependencies": { - "@novu/notification-center": "^0.24.0", + "@novu/notification-center": "^0.24.1", "@types/iframe-resizer": "^3.5.8", "iframe-resizer": "^4.3.1" } diff --git a/libs/shared-web/package.json b/libs/shared-web/package.json index 96e63f89c38..ae76df0419d 100644 --- a/libs/shared-web/package.json +++ b/libs/shared-web/package.json @@ -1,6 +1,6 @@ { "name": "@novu/shared-web", - "version": "0.24.0", + "version": "0.24.1", "repository": "https://github.com/novuhq/novu", "description": "", "private": true, @@ -29,7 +29,7 @@ }, "dependencies": { "@mantine/hooks": "^5.7.1", - "@novu/shared": "^0.24.0", + "@novu/shared": "^0.24.1", "@segment/analytics-next": "1.59.0", "@emotion/styled": "^11.6.0", "@sentry/react": "^7.40.0", diff --git a/libs/shared-web/src/config.ts b/libs/shared-web/src/config.ts index a8b77f3875d..b2d6e9aba9a 100644 --- a/libs/shared-web/src/config.ts +++ b/libs/shared-web/src/config.ts @@ -12,15 +12,17 @@ declare global { } const isCypress = (isBrowser() && (window as any).Cypress) || (isBrowser() && (window as any).parent.Cypress); +const isPlaywright = isBrowser() && (window as any).isPlaywright; export const API_ROOT = - window._env_.REACT_APP_API_URL || isCypress + window._env_.REACT_APP_API_URL || isCypress || isPlaywright ? window._env_.REACT_APP_API_URL || process.env.REACT_APP_API_URL || 'http://localhost:1336' : window._env_.REACT_APP_API_URL || process.env.REACT_APP_API_URL || 'http://localhost:3000'; -export const WS_URL = isCypress - ? window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:1340' - : window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:3002'; +export const WS_URL = + isCypress || isPlaywright + ? window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:1340' + : window._env_.REACT_APP_WS_URL || process.env.REACT_APP_WS_URL || 'http://localhost:3002'; export const SENTRY_DSN = window._env_.REACT_APP_SENTRY_DSN || process.env.REACT_APP_SENTRY_DSN; @@ -29,7 +31,7 @@ export const ENV = window._env_.REACT_APP_ENVIRONMENT || process.env.REACT_APP_E const blueprintApiUrlByEnv = ENV === 'production' || ENV === 'prod' ? 'https://api.novu.co' : 'https://dev.api.novu.co'; export const BLUEPRINTS_API_URL = - window._env_.REACT_APP_BLUEPRINTS_API_URL || isCypress + window._env_.REACT_APP_BLUEPRINTS_API_URL || isCypress || isPlaywright ? window._env_.REACT_APP_BLUEPRINTS_API_URL || process.env.REACT_APP_BLUEPRINTS_API_URL || 'http://localhost:1336' : blueprintApiUrlByEnv; @@ -49,9 +51,10 @@ export const INTERCOM_APP_ID = window._env_.REACT_APP_INTERCOM_APP_ID || process export const CONTEXT_PATH = getContextPath(NovuComponentEnum.WEB); -export const WEBHOOK_URL = isCypress - ? window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:1341' - : window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:3003'; +export const WEBHOOK_URL = + isCypress || isPlaywright + ? window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:1341' + : window._env_.REACT_APP_WEBHOOK_URL || process.env.REACT_APP_WEBHOOK_URL || 'http://localhost:3003'; export const MAIL_SERVER_DOMAIN = window._env_.REACT_APP_MAIL_SERVER_DOMAIN || process.env.REACT_APP_MAIL_SERVER_DOMAIN || 'dev.inbound-mail.novu.co'; @@ -60,7 +63,7 @@ export const LAUNCH_DARKLY_CLIENT_SIDE_ID = window._env_.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID || process.env.REACT_APP_LAUNCH_DARKLY_CLIENT_SIDE_ID; export const FEATURE_FLAGS = Object.values(FeatureFlagsKeysEnum).reduce((acc, key) => { - const defaultValue = isCypress ? true : false; + const defaultValue = isCypress || isPlaywright ? 'true' : 'false'; acc[key] = window._env_[key] || process.env[key] || defaultValue; return acc; diff --git a/libs/shared-web/src/hooks/index.ts b/libs/shared-web/src/hooks/index.ts index 9242ef71545..74bf13dd77c 100644 --- a/libs/shared-web/src/hooks/index.ts +++ b/libs/shared-web/src/hooks/index.ts @@ -4,3 +4,4 @@ export * from './useDataRef'; export * from './useKeyDown'; export * from './useEnvController'; export * from './useFeatureFlags'; +export * from './useProductFeature'; diff --git a/libs/shared-web/src/hooks/useProductFeature.ts b/libs/shared-web/src/hooks/useProductFeature.ts new file mode 100644 index 00000000000..ca6d736e36d --- /dev/null +++ b/libs/shared-web/src/hooks/useProductFeature.ts @@ -0,0 +1,16 @@ +import { ApiServiceLevelEnum, productFeatureEnabledForServiceLevel, ProductFeatureKeyEnum } from '@novu/shared'; +import { useEffect, useState } from 'react'; +import { useAuthController } from './useAuthController'; + +export const useProductFeature = (feature: ProductFeatureKeyEnum) => { + const { organization } = useAuthController(); + const [enabled, setEnabled] = useState(false); + + useEffect(() => { + setEnabled( + productFeatureEnabledForServiceLevel[feature].includes(organization?.apiServiceLevel as ApiServiceLevelEnum) + ); + }, [feature, organization?.apiServiceLevel]); + + return enabled; +}; diff --git a/libs/shared/package.json b/libs/shared/package.json index 061dfe27291..0a03d13914c 100644 --- a/libs/shared/package.json +++ b/libs/shared/package.json @@ -1,6 +1,6 @@ { "name": "@novu/shared", - "version": "0.24.0", + "version": "0.24.1", "description": "", "scripts": { "start": "npm run start:dev", @@ -25,6 +25,18 @@ "files": [ "dist/" ], + "exports": { + ".": { + "require": "./dist/cjs/index.js", + "import": "./dist/esm/index.js", + "types": "./dist/esm/index.d.js" + }, + "./utils": { + "require": "./dist/cjs/utils/index.js", + "import": "./dist/esm/utils/index.js", + "types": "./dist/esm/utils/index.d.js" + } + }, "dependencies": { "axios": "^1.6.2", "class-transformer": "0.5.1", diff --git a/libs/shared/src/consts/handlebar-helpers/getTemplateVariables.ts b/libs/shared/src/consts/handlebar-helpers/getTemplateVariables.ts index 388f8800751..6551adc9f78 100644 --- a/libs/shared/src/consts/handlebar-helpers/getTemplateVariables.ts +++ b/libs/shared/src/consts/handlebar-helpers/getTemplateVariables.ts @@ -10,19 +10,40 @@ export interface IMustacheVariable { } export function getTemplateVariables(bod: any[]): IMustacheVariable[] { + const pairVariables = bod + .filter((body) => body.type === 'HashPair') + .flatMap((body) => { + const varName = body.value?.original as string; + + if (!shouldAddVariable(varName)) { + return []; + } + + return { + type: TemplateVariableTypeEnum.STRING, + name: body.value?.original as string, + defaultValue: '', + required: false, + }; + }); + const stringVariables: IMustacheVariable[] = bod .filter((body) => body.type === 'MustacheStatement') .flatMap((body) => { const varName = body.params[0]?.original || (body.path.original as string); - if (body.path.original === HandlebarHelpersEnum.I18N) { + if (body.path?.original === HandlebarHelpersEnum.I18N) { + if (body.hash?.pairs) { + return getTemplateVariables(body.hash.pairs); + } + return []; } if (!shouldAddVariable(varName)) { return []; } - if (body.params[0]?.original) { + if (body.params?.[0]?.original) { if (!(Object.values(HandlebarHelpersEnum) as string[]).includes(body.path.original)) { return []; } @@ -30,7 +51,7 @@ export function getTemplateVariables(bod: any[]): IMustacheVariable[] { return { type: TemplateVariableTypeEnum.STRING, - name: body.params[0]?.original || (body.path.original as string), + name: body.params?.[0]?.original || (body.path?.original as string), defaultValue: '', required: false, }; @@ -89,7 +110,7 @@ export function getTemplateVariables(bod: any[]): IMustacheVariable[] { ]; }); - return stringVariables.concat(arrayVariables).concat(boolVariables); + return stringVariables.concat(arrayVariables).concat(boolVariables).concat(pairVariables); } const shouldAddVariable = (variableName): boolean => { diff --git a/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts b/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts index 5595ffb44bf..99e855a48df 100644 --- a/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts +++ b/libs/shared/src/consts/handlebar-helpers/handlebarHelpers.ts @@ -10,6 +10,12 @@ export enum HandlebarHelpersEnum { SORT_BY = 'sortBy', NUMBERFORMAT = 'numberFormat', I18N = 'i18n', + GT = 'gt', + GTE = 'gte', + LT = 'lt', + LTE = 'lte', + EQ = 'eq', + NE = 'ne', } // eslint-disable-next-line @typescript-eslint/naming-convention @@ -25,4 +31,10 @@ export const HandlebarHelpers = { [HandlebarHelpersEnum.SORT_BY]: { description: 'sort an array of objects by a property' }, [HandlebarHelpersEnum.NUMBERFORMAT]: { description: 'format number' }, [HandlebarHelpersEnum.I18N]: { description: 'translate' }, + [HandlebarHelpersEnum.GT]: { description: 'greater than' }, + [HandlebarHelpersEnum.GTE]: { description: 'greater than or equal to' }, + [HandlebarHelpersEnum.LT]: { description: 'lesser than' }, + [HandlebarHelpersEnum.LTE]: { description: 'lesser than or equal to' }, + [HandlebarHelpersEnum.EQ]: { description: 'strict equal' }, + [HandlebarHelpersEnum.NE]: { description: 'strict not equal to' }, }; diff --git a/libs/shared/src/consts/index.ts b/libs/shared/src/consts/index.ts index d1430d47ca1..fffcaeea5e0 100644 --- a/libs/shared/src/consts/index.ts +++ b/libs/shared/src/consts/index.ts @@ -6,3 +6,4 @@ export * from './password-helper'; export * from './filters'; export * from './template-store'; export * from './rate-limiting'; +export * from './productFeatureEnabledForServiceLevel'; diff --git a/libs/shared/src/consts/productFeatureEnabledForServiceLevel.ts b/libs/shared/src/consts/productFeatureEnabledForServiceLevel.ts new file mode 100644 index 00000000000..b4671200c0d --- /dev/null +++ b/libs/shared/src/consts/productFeatureEnabledForServiceLevel.ts @@ -0,0 +1,7 @@ +import { ApiServiceLevelEnum, ProductFeatureKeyEnum } from '../types'; + +export const productFeatureEnabledForServiceLevel: Record = Object.freeze( + { + [ProductFeatureKeyEnum.TRANSLATIONS]: [ApiServiceLevelEnum.BUSINESS, ApiServiceLevelEnum.ENTERPRISE], + } +); diff --git a/libs/shared/src/dto/notification-templates/create-template.dto.ts b/libs/shared/src/dto/notification-templates/create-template.dto.ts index 5e1852d1725..7b0068e432c 100644 --- a/libs/shared/src/dto/notification-templates/create-template.dto.ts +++ b/libs/shared/src/dto/notification-templates/create-template.dto.ts @@ -1,6 +1,7 @@ import { NotificationStepDto } from '../workflows'; import { IPreferenceChannels } from '../../entities/subscriber-preference'; import { NotificationTemplateCustomData } from '../../types'; +import { INotificationGroup } from '../../entities/notification-group'; export interface ICreateNotificationTemplateDto { name: string; @@ -11,7 +12,9 @@ export interface ICreateNotificationTemplateDto { steps: NotificationStepDto[]; - notificationGroupId: string; + notificationGroupId?: string; + + notificationGroup?: INotificationGroup; active?: boolean; diff --git a/libs/shared/src/entities/notification-template/notification-template.interface.ts b/libs/shared/src/entities/notification-template/notification-template.interface.ts index fd9355ccefd..835c7537a5a 100644 --- a/libs/shared/src/entities/notification-template/notification-template.interface.ts +++ b/libs/shared/src/entities/notification-template/notification-template.interface.ts @@ -4,6 +4,7 @@ import type { BuilderFieldType, BuilderGroupValues, TemplateVariableTypeEnum, Fi import { IMessageTemplate } from '../message-template'; import { IPreferenceChannels } from '../subscriber-preference'; import { IWorkflowStepMetadata } from '../step'; +import { INotificationGroup } from '../notification-group'; export enum NotificationTemplateTypeEnum { REGULAR = 'REGULAR', @@ -33,7 +34,11 @@ export interface INotificationTemplate { export class IGroupedBlueprint { name: string; - blueprints: INotificationTemplate[]; + blueprints: IBlueprint[]; +} + +export interface IBlueprint extends INotificationTemplate { + notificationGroup: INotificationGroup; } export enum TriggerTypeEnum { diff --git a/libs/shared/src/entities/organization/organization.interface.ts b/libs/shared/src/entities/organization/organization.interface.ts index d25d89baf19..f46173c6444 100644 --- a/libs/shared/src/entities/organization/organization.interface.ts +++ b/libs/shared/src/entities/organization/organization.interface.ts @@ -18,4 +18,5 @@ export interface IOrganizationEntity { productUseCases?: ProductUseCases; createdAt: string; updatedAt: string; + externalId?: string; } diff --git a/libs/shared/src/entities/user/user.interface.ts b/libs/shared/src/entities/user/user.interface.ts index 204f7c111ca..e102d331dc6 100644 --- a/libs/shared/src/entities/user/user.interface.ts +++ b/libs/shared/src/entities/user/user.interface.ts @@ -14,4 +14,5 @@ export interface IUserEntity { showOnBoardingTour?: number; servicesHashes?: IServicesHashes; jobTitle?: JobTitleEnum; + externalId?: string; } diff --git a/libs/shared/src/index.ts b/libs/shared/src/index.ts index 531b483b360..7d096c11120 100644 --- a/libs/shared/src/index.ts +++ b/libs/shared/src/index.ts @@ -1,26 +1,29 @@ -export * from './entities/user'; -export * from './entities/organization'; -export * from './entities/notification-template'; +export * from './config'; +export * from './consts'; +export * from './dto'; +export * from './entities/change'; export * from './entities/environment'; export * from './entities/execution-details'; -export * from './entities/messages'; export * from './entities/feed/feed.interface'; -export * from './entities/notification'; -export * from './entities/message-template'; +export * from './entities/integration'; +export * from './entities/job'; +export * from './entities/layout'; export * from './entities/log'; -export * from './entities/change'; +export * from './entities/message-template'; +export * from './entities/messages'; +export * from './entities/notification-group'; +export * from './entities/notification-template'; +export * from './entities/notification'; +export * from './entities/organization'; export * from './entities/step'; -export * from './entities/job'; export * from './entities/subscriber-preference'; export * from './entities/subscriber'; -export * from './entities/layout'; -export * from './entities/notification-group'; -export * from './entities/integration'; export * from './entities/tenant'; +export * from './entities/user'; export * from './entities/workflow-override'; +export * from './services'; export * from './types'; -export * from './dto'; -export * from './consts'; export * from './ui'; +export * from './utils'; export * from './services'; export * from './config'; diff --git a/libs/shared/src/types/index.ts b/libs/shared/src/types/index.ts index 6d15e971f99..a918a6933ff 100644 --- a/libs/shared/src/types/index.ts +++ b/libs/shared/src/types/index.ts @@ -22,3 +22,4 @@ export * from './rate-limiting'; export * from './auth'; export * from './timezones'; export * from './cron'; +export * from './product-features'; diff --git a/libs/shared/src/types/product-features/index.ts b/libs/shared/src/types/product-features/index.ts new file mode 100644 index 00000000000..8e2fa630c11 --- /dev/null +++ b/libs/shared/src/types/product-features/index.ts @@ -0,0 +1,3 @@ +export enum ProductFeatureKeyEnum { + TRANSLATIONS, +} diff --git a/libs/shared/src/utils/checkIsResponseError.spec.ts b/libs/shared/src/utils/checkIsResponseError.spec.ts new file mode 100644 index 00000000000..bb5121b452d --- /dev/null +++ b/libs/shared/src/utils/checkIsResponseError.spec.ts @@ -0,0 +1,60 @@ +import { checkIsResponseError } from './checkIsResponseError'; +import { IResponseError } from '../types'; + +describe('checkIsResponseError', () => { + it('should return true for a valid IResponseError object', () => { + const error: IResponseError = { + error: 'Something went wrong', + message: 'An error occurred', + statusCode: 500, + }; + + const result = checkIsResponseError(error); + expect(result).toBe(true); + }); + + it('should return false for null', () => { + const result = checkIsResponseError(null); + expect(result).toBe(false); + }); + + it('should return false for undefined', () => { + const result = checkIsResponseError(undefined); + expect(result).toBe(false); + }); + + it('should return false for a non-object value', () => { + const result = checkIsResponseError('This is a string'); + expect(result).toBe(false); + }); + + it('should return false if the object is missing the "error" property', () => { + const error = { + message: 'An error occurred', + statusCode: 500, + }; + + const result = checkIsResponseError(error); + expect(result).toBe(false); + }); + + it('should return false if the object is missing the "message" property', () => { + const error = { + error: 'Something went wrong', + statusCode: 500, + }; + + const result = checkIsResponseError(error); + expect(result).toBe(false); + }); + + it('should return false if the object is missing the "statusCode" property', () => { + const error = { + error: 'Something went wrong', + message: 'An error occurred', + }; + + const result = checkIsResponseError(error); + expect(result).toBe(false); + }); +}); diff --git a/libs/shared/src/utils/checkIsResponseError.ts b/libs/shared/src/utils/checkIsResponseError.ts new file mode 100644 index 00000000000..5c78aae4cf1 --- /dev/null +++ b/libs/shared/src/utils/checkIsResponseError.ts @@ -0,0 +1,8 @@ +import { IResponseError } from '../types'; + +/** + * Validate (type-guard) that an error response matches our IResponseError interface. + */ +export const checkIsResponseError = (err: unknown): err is IResponseError => { + return !!err && typeof err === 'object' && 'error' in err && 'message' in err && 'statusCode' in err; +}; diff --git a/libs/shared/src/utils/env.ts b/libs/shared/src/utils/env.ts new file mode 100644 index 00000000000..e2698699fce --- /dev/null +++ b/libs/shared/src/utils/env.ts @@ -0,0 +1,51 @@ +type CloudflareEnv = { env: Record }; + +// https://remix.run/blog/remix-vite-stable#cloudflare-pages-support +const hasCloudflareProxyContext = (context: any): context is { cloudflare: CloudflareEnv } => { + return !!context?.cloudflare?.env; +}; + +const hasCloudflareContext = (context: any): context is CloudflareEnv => { + return !!context?.env; +}; + +/** + * + * Utility function to get env variables across Node and Edge runtimes. + * + * @param name Pass the name of the environment variable. The param is case-sensitive. + * @returns string Returns the value of the environment variable if exists. + */ +export const getEnvVariable = (name: string, context?: any): string => { + // Node envs + if (typeof process !== 'undefined' && process.env && typeof process.env[name] === 'string') { + return process.env[name] as string; + } + + /* + * Remix + Cloudflare pages + * if (typeof (context?.cloudflare as CloudflareEnv)?.env !== 'undefined') { + */ + if (hasCloudflareProxyContext(context)) { + return context.cloudflare.env[name] || ''; + } + + // Cloudflare + if (hasCloudflareContext(context)) { + return context.env[name] || ''; + } + + // Check whether the value exists in the context object directly + if (context && typeof context[name] === 'string') { + return context[name] as string; + } + + // Cloudflare workers + try { + return globalThis[name as keyof typeof globalThis]; + } catch (_) { + // This will raise an error in Cloudflare Pages + } + + return ''; +}; diff --git a/libs/shared/src/utils/index.ts b/libs/shared/src/utils/index.ts new file mode 100644 index 00000000000..53bf04c6082 --- /dev/null +++ b/libs/shared/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './checkIsResponseError'; +export * from './env'; diff --git a/libs/testing/package.json b/libs/testing/package.json index 083eee4f47e..1ec82462f4c 100644 --- a/libs/testing/package.json +++ b/libs/testing/package.json @@ -1,6 +1,6 @@ { "name": "@novu/testing", - "version": "0.24.0", + "version": "0.24.1", "description": "", "private": true, "scripts": { @@ -22,8 +22,8 @@ "types": "dist/index.d.ts", "dependencies": { "@faker-js/faker": "^6.0.0", - "@novu/dal": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/dal": "^0.24.1", + "@novu/shared": "^0.24.1", "JSONStream": "^1.3.5", "async": "^3.2.0", "axios": "^1.6.2", diff --git a/package.json b/package.json index 2add5564652..4044088f66e 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "commit": "cz", "nx": "nx", "lint-staged": "lint-staged", - "generate:provider": "npx hygen provider new --version 0.24.0", - "lint": "nx run-many --target=lint --all", + "generate:provider": "npx hygen provider new --version 0.24.1", + "lint": "nx run-many --target=lint --all", "test": "cross-env CI=true lerna run test:watch --parallel", - "start:dev": "cross-env TZ=UTC lerna run start:dev --parallel --concurrency=20 --scope=@novu/{api,worker,web,widget,ws,notification-center}", + "start:dev": "cross-env TZ=UTC lerna run start:dev --stream --parallel --concurrency=20 --scope=@novu/{api,worker,web,widget,ws,notification-center}", "start:web": "cross-env nx run @novu/web:start", "start:ws": "cross-env nx run @novu/ws:start", "start:webhook": "cross-env nx run @novu/webhook:start", @@ -51,7 +51,7 @@ "build:web": "nx build @novu/web", "build:widget": "nx build @novu/widget", "build:embed": "nx build @novu/embed", - "build:storybook": "nx run @novu/web:build-storybook", + "build:storybook": "nx run @novu/design-system:build-storybook", "test:providers": "cross-env pnpm --filter './providers/**' test", "release:patch": "lerna version patch --no-push", "release:minor": "lerna version minor --no-push", @@ -88,7 +88,7 @@ "@octokit/core": "^4.0.0", "@pnpm/filter-workspace-packages": "^7.0.6", "@pnpm/logger": "^5.0.0", - "@types/inquirer": "8.2.9", + "@types/inquirer": "8.2.10", "@types/jest": "29.5.2", "@types/node": "16.11.7", "@typescript-eslint/eslint-plugin": "^5.50.0", diff --git a/packages/application-generic/package.json b/packages/application-generic/package.json index 60554d957da..830b1d4954e 100644 --- a/packages/application-generic/package.json +++ b/packages/application-generic/package.json @@ -1,6 +1,6 @@ { "name": "@novu/application-generic", - "version": "0.24.0", + "version": "0.24.1", "description": "Generic backend code used inside of Novu's different services", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -52,72 +52,72 @@ "@google-cloud/storage": "^6.2.3", "@hokify/agenda": "^6.3.0", "@nestjs/passport": "^10.0.1", - "@novu/africas-talking": "^0.24.0", - "@novu/apns": "^0.24.0", - "@novu/azure-sms": "^0.24.0", - "@novu/bandwidth": "^0.24.0", - "@novu/braze": "^0.24.0", - "@novu/brevo-sms": "^0.24.0", - "@novu/bulk-sms": "^0.24.0", - "@novu/burst-sms": "^0.24.0", - "@novu/clickatell": "^0.24.0", - "@novu/clicksend": "^0.24.0", - "@novu/dal": "^0.24.0", - "@novu/discord": "^0.24.0", - "@novu/email-webhook": "^0.24.0", - "@novu/emailjs": "^0.24.0", - "@novu/expo": "^0.24.0", - "@novu/fcm": "^0.24.0", - "@novu/firetext": "^0.24.0", - "@novu/forty-six-elks": "^0.24.0", - "@novu/generic-sms": "^0.24.0", - "@novu/getstream": "^0.24.0", - "@novu/grafana-on-call": "^0.24.0", - "@novu/gupshup": "^0.24.0", - "@novu/infobip": "^0.24.0", - "@novu/isend-sms": "^0.24.0", - "@novu/kannel": "^0.24.0", - "@novu/mailersend": "^0.24.0", - "@novu/mailgun": "^0.24.0", - "@novu/mailjet": "^0.24.0", - "@novu/mailtrap": "^0.24.0", - "@novu/mandrill": "^0.24.0", - "@novu/maqsam": "^0.24.0", - "@novu/mattermost": "^0.24.0", - "@novu/messagebird": "^0.24.0", - "@novu/ms-teams": "^0.24.0", - "@novu/netcore": "^0.24.0", - "@novu/nexmo": "^0.24.0", - "@novu/nodemailer": "^0.24.0", - "@novu/one-signal": "^0.24.0", - "@novu/outlook365": "^0.24.0", - "@novu/plivo": "^0.24.0", - "@novu/plunk": "^0.24.0", - "@novu/postmark": "^0.24.0", - "@novu/push-webhook": "^0.24.0", - "@novu/pusher-beams": "^0.24.0", - "@novu/pushpad": "^0.24.0", - "@novu/resend": "^0.24.0", - "@novu/ring-central": "^0.24.0", - "@novu/rocket-chat": "^0.24.0", - "@novu/ryver": "^0.24.0", - "@novu/sendchamp": "^0.24.0", - "@novu/sendgrid": "^0.24.0", - "@novu/sendinblue": "^0.24.0", - "@novu/ses": "^0.24.0", - "@novu/shared": "^0.24.0", - "@novu/simpletexting": "^0.24.0", - "@novu/slack": "^0.24.0", - "@novu/sms-central": "^0.24.0", - "@novu/sms77": "^0.24.0", - "@novu/sns": "^0.24.0", - "@novu/sparkpost": "^0.24.0", - "@novu/stateless": "^0.24.0", - "@novu/telnyx": "^0.24.0", - "@novu/termii": "^0.24.0", - "@novu/testing": "^0.24.0", - "@novu/twilio": "^0.24.0", - "@novu/zulip": "^0.24.0", + "@novu/africas-talking": "^0.24.1", + "@novu/apns": "^0.24.1", + "@novu/azure-sms": "^0.24.1", + "@novu/bandwidth": "^0.24.1", + "@novu/braze": "^0.24.1", + "@novu/brevo-sms": "^0.24.1", + "@novu/bulk-sms": "^0.24.1", + "@novu/burst-sms": "^0.24.1", + "@novu/clickatell": "^0.24.1", + "@novu/clicksend": "^0.24.1", + "@novu/dal": "^0.24.1", + "@novu/discord": "^0.24.1", + "@novu/email-webhook": "^0.24.1", + "@novu/emailjs": "^0.24.1", + "@novu/expo": "^0.24.1", + "@novu/fcm": "^0.24.1", + "@novu/firetext": "^0.24.1", + "@novu/forty-six-elks": "^0.24.1", + "@novu/generic-sms": "^0.24.1", + "@novu/getstream": "^0.24.1", + "@novu/grafana-on-call": "^0.24.1", + "@novu/gupshup": "^0.24.1", + "@novu/infobip": "^0.24.1", + "@novu/isend-sms": "^0.24.1", + "@novu/kannel": "^0.24.1", + "@novu/mailersend": "^0.24.1", + "@novu/mailgun": "^0.24.1", + "@novu/mailjet": "^0.24.1", + "@novu/mailtrap": "^0.24.1", + "@novu/mandrill": "^0.24.1", + "@novu/maqsam": "^0.24.1", + "@novu/mattermost": "^0.24.1", + "@novu/messagebird": "^0.24.1", + "@novu/ms-teams": "^0.24.1", + "@novu/netcore": "^0.24.1", + "@novu/nexmo": "^0.24.1", + "@novu/nodemailer": "^0.24.1", + "@novu/one-signal": "^0.24.1", + "@novu/outlook365": "^0.24.1", + "@novu/plivo": "^0.24.1", + "@novu/plunk": "^0.24.1", + "@novu/postmark": "^0.24.1", + "@novu/push-webhook": "^0.24.1", + "@novu/pusher-beams": "^0.24.1", + "@novu/pushpad": "^0.24.1", + "@novu/resend": "^0.24.1", + "@novu/ring-central": "^0.24.1", + "@novu/rocket-chat": "^0.24.1", + "@novu/ryver": "^0.24.1", + "@novu/sendchamp": "^0.24.1", + "@novu/sendgrid": "^0.24.1", + "@novu/sendinblue": "^0.24.1", + "@novu/ses": "^0.24.1", + "@novu/shared": "^0.24.1", + "@novu/simpletexting": "^0.24.1", + "@novu/slack": "^0.24.1", + "@novu/sms-central": "^0.24.1", + "@novu/sms77": "^0.24.1", + "@novu/sns": "^0.24.1", + "@novu/sparkpost": "^0.24.1", + "@novu/stateless": "^0.24.1", + "@novu/telnyx": "^0.24.1", + "@novu/termii": "^0.24.1", + "@novu/testing": "^0.24.1", + "@novu/twilio": "^0.24.1", + "@novu/zulip": "^0.24.1", "@opentelemetry/api": "^1.7.0", "@opentelemetry/auto-instrumentations-node": "^0.40.2", "@opentelemetry/context-async-hooks": "^1.19.0", @@ -125,6 +125,7 @@ "@opentelemetry/exporter-collector": "^0.25.0", "@opentelemetry/exporter-jaeger": "^1.19.0", "@opentelemetry/exporter-prometheus": "^0.46.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.49.1", "@opentelemetry/instrumentation": "^0.46.0", "@opentelemetry/propagator-b3": "^1.19.0", "@opentelemetry/propagator-jaeger": "^1.19.0", @@ -160,8 +161,8 @@ "rxjs": "7.8.1" }, "optionalDependencies": { - "@taskforcesh/bullmq-pro": "5.1.14", - "@novu/ee-chimera-connect": "^0.24.0" + "@novu/ee-chimera-connect": "^0.24.1", + "@taskforcesh/bullmq-pro": "5.1.14" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/packages/application-generic/src/health/cache.health-indicator.ts b/packages/application-generic/src/health/cache.health-indicator.ts index 1e000b83083..d6d882563ba 100644 --- a/packages/application-generic/src/health/cache.health-indicator.ts +++ b/packages/application-generic/src/health/cache.health-indicator.ts @@ -3,34 +3,25 @@ import { HealthIndicator, HealthIndicatorResult, } from '@nestjs/terminus'; -import { Injectable, Logger } from '@nestjs/common'; - +import { Injectable } from '@nestjs/common'; import { CacheService } from '../services/cache'; -const LOG_CONTEXT = 'CacheServiceHealthIndicator'; - @Injectable() export class CacheServiceHealthIndicator extends HealthIndicator { - private INDICATOR_KEY = 'cacheService'; + private static KEY = 'cacheService'; constructor(private cacheService: CacheService) { super(); } async isHealthy(): Promise { - const isReady = this.cacheService.cacheEnabled(); + const isHealthy = this.cacheService.cacheEnabled(); + const result = this.getStatus(CacheServiceHealthIndicator.KEY, isHealthy); - if (isReady) { - Logger.verbose('CacheService is ready', LOG_CONTEXT); - - return this.getStatus(this.INDICATOR_KEY, true); + if (isHealthy) { + return result; } - Logger.verbose('CacheServiceHealthIndicator is not ready', LOG_CONTEXT); - - throw new HealthCheckError( - 'Cache Health', - this.getStatus(this.INDICATOR_KEY, false) - ); + throw new HealthCheckError('Cache health check failed', result); } } diff --git a/packages/application-generic/src/health/dal.health-indicator.ts b/packages/application-generic/src/health/dal.health-indicator.ts index d4e4dab7794..8540a63c282 100644 --- a/packages/application-generic/src/health/dal.health-indicator.ts +++ b/packages/application-generic/src/health/dal.health-indicator.ts @@ -1,4 +1,8 @@ -import { HealthIndicator, HealthIndicatorResult } from '@nestjs/terminus'; +import { + HealthCheckError, + HealthIndicator, + HealthIndicatorResult, +} from '@nestjs/terminus'; import { Injectable } from '@nestjs/common'; import { DalService } from '@novu/dal'; import { IHealthIndicator } from './health-indicator.interface'; @@ -8,16 +12,21 @@ export class DalServiceHealthIndicator extends HealthIndicator implements IHealthIndicator { - private INDICATOR_KEY = 'db'; + private static KEY = 'db'; constructor(private dalService: DalService) { super(); } async isHealthy(): Promise { - const status = this.dalService.connection.readyState === 1; + const isHealthy = this.dalService.connection.readyState === 1; + const result = this.getStatus(DalServiceHealthIndicator.KEY, isHealthy); - return this.getStatus(this.INDICATOR_KEY, status); + if (isHealthy) { + return result; + } + + throw new HealthCheckError('DAL health check failed', result); } isActive(): Promise { diff --git a/packages/application-generic/src/tracing/tracing.module.ts b/packages/application-generic/src/tracing/tracing.module.ts index 0938b5efe85..46ce828ce14 100644 --- a/packages/application-generic/src/tracing/tracing.module.ts +++ b/packages/application-generic/src/tracing/tracing.module.ts @@ -17,13 +17,14 @@ const OtelModule = OpenTelemetryModule.forRoot({ @Module({}) export class TracingModule { - static register(serviceName: string): DynamicModule { + static register(serviceName: string, version: string): DynamicModule { return { module: TracingModule, imports: [OtelModule], providers: [ TracingService, { provide: 'TRACING_SERVICE_NAME', useValue: serviceName }, + { provide: 'TRACING_SERVICE_VERSION', useValue: version }, { provide: 'TRACING_ENABLE_OTEL', useValue: process.env.ENABLE_OTEL === 'true', diff --git a/packages/application-generic/src/tracing/tracing.service.ts b/packages/application-generic/src/tracing/tracing.service.ts index 97f64d9df9b..be06dd84be9 100644 --- a/packages/application-generic/src/tracing/tracing.service.ts +++ b/packages/application-generic/src/tracing/tracing.service.ts @@ -13,6 +13,7 @@ export class TracingService implements OnModuleInit, OnModuleDestroy { constructor( @Inject('TRACING_SERVICE_NAME') private readonly serviceName: string, + @Inject('TRACING_SERVICE_VERSION') private readonly version: string, @Inject('TRACING_ENABLE_OTEL') private readonly otelEnabled: boolean ) {} @@ -24,7 +25,7 @@ export class TracingService implements OnModuleInit, OnModuleDestroy { onModuleInit() { if (!this.hasValidParameters()) return; - this.otelSDKInstance = initializeOtelSdk(this.serviceName); + this.otelSDKInstance = initializeOtelSdk(this.serviceName, this.version); this.otelSDKInstance.start(); } diff --git a/packages/application-generic/src/tracing/tracing.ts b/packages/application-generic/src/tracing/tracing.ts index 0cd9cf7852c..0521f4fb6ab 100644 --- a/packages/application-generic/src/tracing/tracing.ts +++ b/packages/application-generic/src/tracing/tracing.ts @@ -4,21 +4,35 @@ import { W3CTraceContextPropagator, } from '@opentelemetry/core'; import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; import { JaegerPropagator } from '@opentelemetry/propagator-jaeger'; import { B3InjectEncoding, B3Propagator } from '@opentelemetry/propagator-b3'; import { PrometheusExporter } from '@opentelemetry/exporter-prometheus'; import { NodeSDK } from '@opentelemetry/sdk-node'; import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { Resource } from '@opentelemetry/resources'; +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; // eslint-disable-next-line @typescript-eslint/naming-convention -export function initializeOtelSdk(serviceName: string) { +export function initializeOtelSdk(serviceName: string, version: string) { return new NodeSDK({ + resource: new Resource({ + 'service.version': version, + 'service.group': 'instrumentation-group', + [SemanticResourceAttributes.SERVICE_NAME]: serviceName, + }), metricReader: new PrometheusExporter({ port: 9464, + preventServerStart: false, + appendTimestamp: true, + }), + spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter(), { + // The maximum queue size. After the size is reached spans are dropped. + maxQueueSize: 1000, + // The interval between two consecutive exports + scheduledDelayMillis: 30000, }), - spanProcessor: new BatchSpanProcessor(new JaegerExporter()), contextManager: new AsyncLocalStorageContextManager(), serviceName: serviceName, textMapPropagator: new CompositePropagator({ diff --git a/packages/application-generic/src/usecases/add-job/add-delay-job.usecase.ts b/packages/application-generic/src/usecases/add-job/add-delay-job.usecase.ts index 8462de9586c..b6efe9d3e31 100644 --- a/packages/application-generic/src/usecases/add-job/add-delay-job.usecase.ts +++ b/packages/application-generic/src/usecases/add-job/add-delay-job.usecase.ts @@ -2,6 +2,7 @@ import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { JobRepository, JobStatusEnum } from '@novu/dal'; import { + DelayTypeEnum, ExecutionDetailsSourceEnum, ExecutionDetailsStatusEnum, StepTypeEnum, @@ -46,7 +47,13 @@ export class AddDelayJob { stepMetadata: data.step.metadata, payload: data.payload, overrides: data.overrides, - chimeraResponse: command.chimeraResponse?.outputs, + // TODO: Remove fallback after other delay types are implemented. + chimeraResponse: command.chimeraResponse?.outputs + ? { + type: DelayTypeEnum.REGULAR, + ...command.chimeraResponse?.outputs, + } + : undefined, }); await this.jobRepository.updateStatus( diff --git a/packages/application-generic/src/usecases/add-job/add-job.usecase.ts b/packages/application-generic/src/usecases/add-job/add-job.usecase.ts index 482bf453622..a433019f9dc 100644 --- a/packages/application-generic/src/usecases/add-job/add-job.usecase.ts +++ b/packages/application-generic/src/usecases/add-job/add-job.usecase.ts @@ -5,6 +5,7 @@ import { ExecutionDetailsStatusEnum, StepTypeEnum, DigestCreationResultEnum, + DigestTypeEnum, } from '@novu/shared'; import { AddDelayJob } from './add-delay-job.usecase'; @@ -35,6 +36,7 @@ import { IUseCaseInterfaceInline, requireInject, } from '../../utils/require-inject'; +import { IFilterVariables } from '../../utils/filter-processing-details'; export enum BackoffStrategiesEnum { WEBHOOK_FILTER_BACKOFF = 'webhookFilterBackoff', @@ -85,7 +87,7 @@ export class AddJob { ); let filtered = false; - + let filterVariables: IFilterVariables | undefined; if ( [StepTypeEnum.DELAY, StepTypeEnum.DIGEST].includes( job.type as StepTypeEnum @@ -102,6 +104,7 @@ export class AddJob { }) ); + filterVariables = shouldRun.variables; filtered = !shouldRun.passed; } @@ -109,9 +112,12 @@ export class AddJob { let digestCreationResult: DigestCreationResultEnum | undefined; if (job.type === StepTypeEnum.DIGEST) { const chimeraResponse = await this.chimeraConnector.execute< - AddJobCommand, + AddJobCommand & { variables: IFilterVariables }, ExecuteOutput - >(command); + >({ + ...command, + variables: filterVariables, + }); validateDigest(job); @@ -119,7 +125,10 @@ export class AddJob { stepMetadata: job.digest, payload: job.payload, overrides: job.overrides, - chimeraResponse: chimeraResponse?.outputs, + // TODO: Remove fallback after other digest types are implemented. + chimeraResponse: chimeraResponse + ? { type: DigestTypeEnum.REGULAR, ...chimeraResponse.outputs } + : undefined, }); Logger.debug(`Digest step amount is: ${digestAmount}`, LOG_CONTEXT); @@ -164,9 +173,12 @@ export class AddJob { if (job.type === StepTypeEnum.DELAY) { const chimeraResponse = await this.chimeraConnector.execute< - AddJobCommand, + AddJobCommand & { variables: IFilterVariables }, ExecuteOutput - >(command); + >({ + ...command, + variables: filterVariables, + }); command.chimeraResponse = chimeraResponse; delayAmount = await this.addDelayJob.execute(command); diff --git a/packages/application-generic/src/usecases/add-job/merge-or-create-digest.usecase.ts b/packages/application-generic/src/usecases/add-job/merge-or-create-digest.usecase.ts index a0fabba8b0c..9a8600c4775 100644 --- a/packages/application-generic/src/usecases/add-job/merge-or-create-digest.usecase.ts +++ b/packages/application-generic/src/usecases/add-job/merge-or-create-digest.usecase.ts @@ -50,7 +50,8 @@ export class MergeOrCreateDigest { ): Promise { const { job } = command; - const digestMeta = job.digest as IDigestBaseMetadata | undefined; + const digestMeta = + command.chimeraData ?? (job.digest as IDigestBaseMetadata | undefined); const digestKey = command.chimeraData?.digestKey ?? digestMeta?.digestKey; const digestValue = getNestedValue(job.payload, digestKey); @@ -73,7 +74,10 @@ export class MergeOrCreateDigest { case DigestCreationResultEnum.SKIPPED: return await this.processSkippedDigest(job, command.filtered); case DigestCreationResultEnum.CREATED: - return await this.processCreatedDigest(digestMeta, job); + return await this.processCreatedDigest( + digestMeta as IDigestBaseMetadata, + job + ); default: throw new ApiException('Something went wrong with digest creation'); } diff --git a/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts b/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts index 4003ac48f29..9b2393168b2 100644 --- a/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts +++ b/packages/application-generic/src/usecases/compile-template/compile-template.spec.ts @@ -209,4 +209,148 @@ describe('Compile Template', function () { expect(result).toEqual('
Not a number
'); }); }); + + describe('gt helper', () => { + const template = `{{#gt steps 5 }}gt block{{else}}else block{{/gt}}`; + it('shoud render gt block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('gt block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('gte helper', () => { + const template = `{{#gte steps 5 }}gte block{{else}}else block{{/gte}}`; + it('shoud render gte block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('gte block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 4 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('lt helper', () => { + const template = `{{#lt steps 5 }}lt block{{else}}else block{{/lt}}`; + it('shoud render lt block', async () => { + const result = await useCase.execute({ + data: { steps: 4 }, + template, + }); + + expect(result).toEqual('lt block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('lte helper', () => { + const template = `{{#lte steps 5 }}lte block{{else}}else block{{/lte}}`; + it('shoud render lte block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('lte block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('eq helper', () => { + const template = `{{#eq steps 5 }}eq block{{else}}else block{{/eq}}`; + it('shoud render eq block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('eq block'); + }); + + it('shoud use strict check and render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: '5' }, + template, + }); + + expect(result).toEqual('else block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); + + describe('ne helper', () => { + const template = `{{#ne steps 5 }}ne block{{else}}else block{{/ne}}`; + it('shoud render ne block', async () => { + const result = await useCase.execute({ + data: { steps: 6 }, + template, + }); + + expect(result).toEqual('ne block'); + }); + + it('shoud use strict check and render ne block', async () => { + const result = await useCase.execute({ + data: { steps: '5' }, + template, + }); + + expect(result).toEqual('ne block'); + }); + + it('shoud render alternative block', async () => { + const result = await useCase.execute({ + data: { steps: 5 }, + template, + }); + + expect(result).toEqual('else block'); + }); + }); }); diff --git a/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts b/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts index 1669e6c0d13..bd78462aa2c 100644 --- a/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts +++ b/packages/application-generic/src/usecases/compile-template/compile-template.usecase.ts @@ -158,6 +158,60 @@ Handlebars.registerHelper( } ); +Handlebars.registerHelper( + HandlebarHelpersEnum.GT, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 > arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.GTE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 >= arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.LT, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 < arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.LTE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 <= arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.EQ, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 === arg2 ? options.fn(this) : options.inverse(this); + } +); + +Handlebars.registerHelper( + HandlebarHelpersEnum.NE, + function (arg1, arg2, options) { + // eslint-disable-next-line + // @ts-expect-error + return arg1 !== arg2 ? options.fn(this) : options.inverse(this); + } +); + @Injectable() export class CompileTemplate { async execute(command: CompileTemplateCommand): Promise { diff --git a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts index 7ce65018ee5..a49078fd0ac 100644 --- a/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts +++ b/packages/application-generic/src/usecases/create-subscriber/create-subscriber.usecase.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { SubscriberRepository } from '@novu/dal'; -import { SubscriberEntity } from '@novu/dal'; +import { SubscriberEntity, ErrorCodesEnum } from '@novu/dal'; import { CachedEntity, @@ -30,6 +30,30 @@ export class CreateSubscriber { })); if (!subscriber) { + subscriber = await this.createSubscriber(command); + } else { + subscriber = await this.updateSubscriber.execute( + UpdateSubscriberCommand.create({ + environmentId: command.environmentId, + organizationId: command.organizationId, + firstName: command.firstName, + lastName: command.lastName, + subscriberId: command.subscriberId, + email: command.email, + phone: command.phone, + avatar: command.avatar, + locale: command.locale, + data: command.data, + subscriber, + }) + ); + } + + return subscriber; + } + + private async createSubscriber(command: CreateSubscriberCommand) { + try { await this.invalidateCache.invalidateByKey({ key: buildSubscriberKey({ subscriberId: command.subscriberId, @@ -50,26 +74,20 @@ export class CreateSubscriber { data: command.data, }; - subscriber = await this.subscriberRepository.create(subscriberPayload); - } else { - subscriber = await this.updateSubscriber.execute( - UpdateSubscriberCommand.create({ - environmentId: command.environmentId, - organizationId: command.organizationId, - firstName: command.firstName, - lastName: command.lastName, + return await this.subscriberRepository.create(subscriberPayload); + } catch (err: any) { + /* + * Possible race condition on subscriber creation, try fetch newly created the subscriber + */ + if (err.code === ErrorCodesEnum.DUPLICATE_KEY) { + return await this.fetchSubscriber({ + _environmentId: command.environmentId, subscriberId: command.subscriberId, - email: command.email, - phone: command.phone, - avatar: command.avatar, - locale: command.locale, - data: command.data, - subscriber, - }) - ); + }); + } else { + throw err; + } } - - return subscriber; } @CachedEntity({ diff --git a/packages/application-generic/src/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts b/packages/application-generic/src/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts index 377ae664eab..181cfc5f77c 100644 --- a/packages/application-generic/src/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts +++ b/packages/application-generic/src/usecases/subscriber-job-bound/subscriber-job-bound.usecase.ts @@ -85,10 +85,20 @@ export class SubscriberJobBound { await this.validateSubscriberIdProperty(subscriber); + /** + * Due to Mixpanel HotSharding, we don't want to pass userId for production volume + */ + const segmentUserId = ['test-workflow', 'digest-playground'].includes( + command.payload.__source + ) + ? userId + : ''; + this.analyticsService.mixpanelTrack( 'Notification event trigger - [Triggers]', - '', + segmentUserId, { + name: template.name, type: template?.type || 'REGULAR', transactionId: command.transactionId, _template: template._id, diff --git a/packages/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts b/packages/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts index faba8fabda6..f8c0ae1f8ca 100644 --- a/packages/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts +++ b/packages/application-generic/src/usecases/trigger-multicast/trigger-multicast.usecase.ts @@ -1,6 +1,5 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; import * as _ from 'lodash'; - import { TopicEntity, TopicRepository, @@ -84,6 +83,11 @@ export class TriggerMulticast { ); if (!isEnabled) { + Logger.log( + `The IS_TOPIC_NOTIFICATION_ENABLED feature flag is disabled, skipping trigger multicast`, + LOG_CONTEXT + ); + return; } diff --git a/packages/cli/README.MD b/packages/cli/README.MD index c474f236c8d..9e41584d32a 100644 --- a/packages/cli/README.MD +++ b/packages/cli/README.MD @@ -89,7 +89,7 @@ Using the Novu API and admin panel you can easily add real-time notification cen
notification-center-912bb96e009fb3a69bafec23bcde00b0 - Read more about how to add a notification center to your app with the Novu API [here](https://docs.novu.co/notification-center/introduction) + Read more about how to add a notification center to your app with the Novu API [here](https://docs.novu.co/notification-center/introduction?utm_campaign=inapp-cli-readme)
@@ -140,7 +140,7 @@ Novu provides a single API to manage providers across multiple channels with a s #### 📱 In-App -- [x] [Novu](https://docs.novu.co/notification-center/introduction) +- [x] [Novu](https://docs.novu.co/notification-center/introduction?utm_campaign=inapp-cli-readme) - [ ] MagicBell #### Other (Coming Soon...) diff --git a/packages/cli/package.json b/packages/cli/package.json index 5dcc0673eda..925808c4ad3 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "novu", - "version": "0.24.0", + "version": "0.24.1", "description": "On-Boarding Cli", "main": "index.js", "scripts": { @@ -30,7 +30,7 @@ "typescript": "4.9.5" }, "dependencies": { - "@novu/shared": "^0.24.0", + "@novu/shared": "^0.24.1", "@segment/analytics-node": "^1.1.4", "axios": "^1.6.2", "chalk": "4.1.2", diff --git a/packages/client/package.json b/packages/client/package.json index 2771893939b..9bee7ec63c0 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@novu/client", - "version": "0.24.0", + "version": "0.24.1", "repository": "https://github.com/novuhq/novu", "description": "API client to be used in end user environments", "main": "dist/cjs/index.js", @@ -44,7 +44,7 @@ "node": ">=10" }, "dependencies": { - "@novu/shared": "^0.24.0" + "@novu/shared": "^0.24.1" }, "devDependencies": { "@types/jest": "29.5.2", diff --git a/packages/headless/package.json b/packages/headless/package.json index f49a2a2bd1d..ba838f4da2c 100644 --- a/packages/headless/package.json +++ b/packages/headless/package.json @@ -1,6 +1,6 @@ { "name": "@novu/headless", - "version": "0.24.0", + "version": "0.24.1", "repository": "https://github.com/novuhq/novu", "description": "Headless client package that is a thin abstraction layer over the API client + state and socket management", "keywords": [], @@ -28,8 +28,8 @@ "node": ">=10" }, "dependencies": { - "@novu/client": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/client": "^0.24.1", + "@novu/shared": "^0.24.1", "@tanstack/query-core": "^4.15.1", "socket.io-client": "4.7.2" }, diff --git a/packages/nest/package.json b/packages/nest/package.json index 62c5dd9b7d4..0a952ae52c6 100644 --- a/packages/nest/package.json +++ b/packages/nest/package.json @@ -1,6 +1,6 @@ { "name": "@novu/nest", - "version": "0.24.0", + "version": "0.24.1", "description": "A nestjs wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -36,7 +36,7 @@ }, "dependencies": { "@nestjs/common": "10.2.2", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/packages/node/README.md b/packages/node/README.md index ba07fb028fa..000ee5ab8ee 100644 --- a/packages/node/README.md +++ b/packages/node/README.md @@ -126,7 +126,7 @@ Novu provides a single API to manage providers across multiple channels with a s #### 📱 In-App -- [x] [Novu](https://docs.novu.co/notification-center/introduction) +- [x] [Novu](https://docs.novu.co/notification-center/introduction?utm_campaign=node-sdk-readme) - [ ] MagicBell #### Other (Coming Soon...) diff --git a/packages/node/jest.config.js b/packages/node/jest.config.js new file mode 100644 index 00000000000..f89c15a9700 --- /dev/null +++ b/packages/node/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + uuid: require.resolve('uuid'), + }, +}; diff --git a/packages/node/package.json b/packages/node/package.json index 54cf94f0775..cce620a51f9 100644 --- a/packages/node/package.json +++ b/packages/node/package.json @@ -1,6 +1,6 @@ { "name": "@novu/node", - "version": "0.24.0", + "version": "0.24.1", "description": "Notification Management Framework", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -42,7 +42,7 @@ "node": ">=10" }, "dependencies": { - "@novu/shared": "^0.24.0", + "@novu/shared": "^0.24.1", "axios-retry": "^3.8.0", "handlebars": "^4.7.7", "lodash.get": "^4.4.2", @@ -57,13 +57,13 @@ "@types/uuid": "^8.3.4", "axios": "^1.6.2", "codecov": "^3.5.0", - "jest": "^27.0.6", + "jest": "^29.7.0", "nock": "^13.1.3", "npm-run-all": "^4.1.5", "open-cli": "^6.0.1", "rimraf": "^3.0.2", "run-p": "0.0.0", - "ts-jest": "^27.0.5", + "ts-jest": "^29.1.2", "typedoc": "^0.24.0", "typescript": "4.9.5" }, @@ -76,13 +76,6 @@ "LICENSE", "README.md" ], - "jest": { - "preset": "ts-jest", - "testEnvironment": "node", - "moduleNameMapper": { - "^axios$": "axios/dist/node/axios.cjs" - } - }, "prettier": { "singleQuote": true } diff --git a/packages/node/src/lib/novu.interface.ts b/packages/node/src/lib/novu.interface.ts index 953238f47ec..7058f726538 100644 --- a/packages/node/src/lib/novu.interface.ts +++ b/packages/node/src/lib/novu.interface.ts @@ -9,6 +9,7 @@ export interface IRetryConfig { } export interface INovuConfiguration { + apiKey?: string; backendUrl?: string; retryConfig?: IRetryConfig; } diff --git a/packages/node/src/lib/novu.spec.ts b/packages/node/src/lib/novu.spec.ts index 210ecf57c4f..c17e0d0974a 100644 --- a/packages/node/src/lib/novu.spec.ts +++ b/packages/node/src/lib/novu.spec.ts @@ -7,6 +7,27 @@ const mockConfig = { jest.mock('axios'); +describe('test initialization of novu node package', () => { + let novu: Novu; + + const originalEnv = process.env; + + beforeEach(() => { + process.env = { + ...originalEnv, + NOVU_API_KEY: 'cafebabe', + }; + }); + + afterEach(() => { + process.env = originalEnv; + }); + + test('should use the NOVU_API_KEY when defined', async () => { + expect(new Novu().apiKey).toBe('cafebabe'); + }); +}); + describe('test use of novu node package', () => { const mockedAxios = axios as jest.Mocked; let novu: Novu; diff --git a/packages/node/src/lib/novu.ts b/packages/node/src/lib/novu.ts index 250e92388de..c847768b0b3 100644 --- a/packages/node/src/lib/novu.ts +++ b/packages/node/src/lib/novu.ts @@ -1,4 +1,5 @@ import axios, { AxiosInstance } from 'axios'; +import { getEnvVariable } from '@novu/shared/utils'; import { Subscribers } from './subscribers/subscribers'; import { EventEmitter } from 'events'; import { Changes } from './changes/changes'; @@ -21,7 +22,7 @@ import { WorkflowOverrides } from './workflow-override/workflow-override'; import { makeRetryable } from './retry'; export class Novu extends EventEmitter { - private readonly apiKey?: string; + public readonly apiKey?: string; private readonly http: AxiosInstance; readonly subscribers: Subscribers; readonly environments: Environments; @@ -40,8 +41,25 @@ export class Novu extends EventEmitter { readonly organizations: Organizations; readonly workflowOverrides: WorkflowOverrides; - constructor(apiKey: string, config?: INovuConfiguration) { + constructor(config?: INovuConfiguration); + constructor(apiKey: string, config?: INovuConfiguration); + constructor(...args: any) { super(); + + let apiKey: string | undefined; + let config: INovuConfiguration | undefined; + + if (arguments.length === 2) { + apiKey = args[0]; + config = args[1]; + } else if (arguments.length === 1) { + const { apiKey: key, ...rest } = args[0]; + apiKey = key; + config = rest; + } else { + apiKey = getEnvVariable('NOVU_API_KEY'); + } + this.apiKey = apiKey; const axiosInstance = axios.create({ baseURL: this.buildBackendUrl(config), diff --git a/packages/node/tsconfig.json b/packages/node/tsconfig.json index 5543b8179c6..0f033d8a1ab 100644 --- a/packages/node/tsconfig.json +++ b/packages/node/tsconfig.json @@ -4,7 +4,8 @@ "strictNullChecks": true, "allowSyntheticDefaultImports": true, "outDir": "build/main", - "module": "commonjs", + "module": "CommonJS", + "moduleResolution": "NodeNext", "esModuleInterop": true, "rootDir": "src", "strict": true, diff --git a/packages/notification-center-angular/package.json b/packages/notification-center-angular/package.json index 8dbc08b6ea3..dd888c168d9 100644 --- a/packages/notification-center-angular/package.json +++ b/packages/notification-center-angular/package.json @@ -1,6 +1,6 @@ { "name": "@novu/angular-workspace", - "version": "0.24.0", + "version": "0.24.1", "scripts": { "ng": "ng", "start": "ng serve", @@ -19,7 +19,7 @@ "@angular/platform-browser": "^16.2.0", "@angular/platform-browser-dynamic": "^16.2.0", "@angular/router": "^16.2.0", - "@novu/notification-center": "^0.24.0", + "@novu/notification-center": "^0.24.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "~0.13.0" diff --git a/packages/notification-center-angular/projects/notification-center-angular/README.md b/packages/notification-center-angular/projects/notification-center-angular/README.md index c774fb4d400..78770371ac7 100644 --- a/packages/notification-center-angular/projects/notification-center-angular/README.md +++ b/packages/notification-center-angular/projects/notification-center-angular/README.md @@ -16,7 +16,7 @@ This library contains a wrapper for the Novu Notification Center web component, ## 📖 Client Installation -For our client installation guide, visit our [Angular Client docs](https://docs.novu.co/notification-center/client/angular). +For our client installation guide, visit our [Angular Client docs](https://docs.novu.co/notification-center/client/angular?utm_campaign=github-notificationcenter-angular-readme). ## 🏃‍♂️ Quickstart diff --git a/packages/notification-center-angular/projects/notification-center-angular/package.json b/packages/notification-center-angular/projects/notification-center-angular/package.json index 1544c87428d..3bb2c3b296f 100644 --- a/packages/notification-center-angular/projects/notification-center-angular/package.json +++ b/packages/notification-center-angular/projects/notification-center-angular/package.json @@ -1,6 +1,6 @@ { "name": "@novu/notification-center-angular", - "version": "0.24.0", + "version": "0.24.1", "peerDependencies": { "@angular/common": "^15.0.0 || ^16.0.0 || ^17.0.0", "@angular/core": "^15.0.0 || ^16.0.0 || ^17.0.0", @@ -8,7 +8,7 @@ "@angular/platform-browser-dynamic": "^15.0.0 || ^16.0.0 || ^17.0.0" }, "dependencies": { - "@novu/notification-center": "^0.24.0", + "@novu/notification-center": "^0.24.1", "@types/react": "^17.0.0", "react": "^17.0.1", "react-dom": "^17.0.1", diff --git a/packages/notification-center-vue/package.json b/packages/notification-center-vue/package.json index 4369f2ae6dc..9927b969a0e 100644 --- a/packages/notification-center-vue/package.json +++ b/packages/notification-center-vue/package.json @@ -1,7 +1,7 @@ { "name": "@novu/notification-center-vue", "sideEffects": false, - "version": "0.24.0", + "version": "0.24.1", "description": "Vue specific wrapper for notification-center", "repository": { "type": "git", @@ -22,7 +22,7 @@ "dependencies": { "@emotion/css": "^11.10.5", "@novu/floating-vue": "^2.0.3", - "@novu/notification-center": "^0.24.0", + "@novu/notification-center": "^0.24.1", "react": "^17.0.1", "react-dom": "^17.0.1" }, diff --git a/packages/notification-center/package.json b/packages/notification-center/package.json index 959093c61fa..5746b338d49 100644 --- a/packages/notification-center/package.json +++ b/packages/notification-center/package.json @@ -1,6 +1,6 @@ { "name": "@novu/notification-center", - "version": "0.24.0", + "version": "0.24.1", "repository": "https://github.com/novuhq/novu", "description": "", "scripts": { @@ -80,8 +80,8 @@ "@emotion/styled": "^11.6.0", "@mantine/core": "^5.7.1", "@mantine/hooks": "^5.7.1", - "@novu/client": "^0.24.0", - "@novu/shared": "^0.24.0", + "@novu/client": "^0.24.1", + "@novu/shared": "^0.24.1", "@tanstack/react-query": "^4.20.4", "acorn-jsx": "^5.3.2", "axios": "^1.6.2", diff --git a/packages/stateless/README.md b/packages/stateless/README.md index 5aa98cf7972..091c367a0c2 100644 --- a/packages/stateless/README.md +++ b/packages/stateless/README.md @@ -99,7 +99,7 @@ Novu provides a single API to manage providers across multiple channels with a s #### 📱 In-App -- [x] [Novu](https://docs.novu.co/notification-center/introduction) +- [x] [Novu](https://docs.novu.co/notification-center/introduction?utm_source=github-stateless-readme) - [ ] MagicBell #### Other (Coming Soon...) diff --git a/packages/stateless/package.json b/packages/stateless/package.json index daaa10e79c0..3e4b12760dd 100644 --- a/packages/stateless/package.json +++ b/packages/stateless/package.json @@ -1,6 +1,6 @@ { "name": "@novu/stateless", - "version": "0.24.0", + "version": "0.24.1", "description": "Notification Management Framework", "main": "build/main/index.js", "typings": "build/main/index.d.ts", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d1a10ce56f2..9a73594a527 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,8 +79,8 @@ importers: specifier: ^5.0.0 version: 5.0.0 '@types/inquirer': - specifier: 8.2.9 - version: 8.2.9 + specifier: 8.2.10 + version: 8.2.10 '@types/jest': specifier: 29.5.2 version: 29.5.2 @@ -233,7 +233,7 @@ importers: version: 6.1.13 ts-jest: specifier: 27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) @@ -280,22 +280,22 @@ importers: specifier: ^5.0.1 version: 5.1.1(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(reflect-metadata@0.1.13) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/node': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/node '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@sendgrid/mail': specifier: ^8.1.0 @@ -428,16 +428,16 @@ importers: version: 8.3.2 optionalDependencies: '@novu/ee-auth': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/auth '@novu/ee-billing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/billing '@novu/ee-chimera': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/chimera '@novu/ee-translation': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/translation devDependencies: '@faker-js/faker': @@ -513,10 +513,10 @@ importers: apps/inbound-mail: dependencies: '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/application-generic '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@sentry/node': specifier: ^7.12.1 @@ -568,7 +568,7 @@ importers: version: 3.10.0 devDependencies: '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@types/chai': specifier: ^4.2.11 @@ -608,7 +608,7 @@ importers: version: 9.2.4 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) @@ -629,13 +629,13 @@ importers: version: 4.6.2(react-dom@17.0.2)(react@17.0.2) '@babel/plugin-proposal-optional-chaining': specifier: ^7.20.7 - version: 7.21.0(@babel/core@7.23.2) + version: 7.21.0(@babel/core@7.24.4) '@babel/plugin-transform-react-display-name': specifier: ^7.18.6 - version: 7.18.6(@babel/core@7.23.2) + version: 7.18.6(@babel/core@7.24.4) '@babel/plugin-transform-runtime': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@cypress/react': specifier: ^7.0.3 version: 7.0.3(@types/react@17.0.53)(cypress@13.3.1)(react-dom@17.0.2)(react@17.0.2) @@ -703,16 +703,16 @@ importers: specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.45.0)(react-dom@17.0.2)(react@17.0.2) '@novu/design-system': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/design-system '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/notification-center '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/shared-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared-web '@rive-app/react-canvas': specifier: ^4.8.1 @@ -914,10 +914,13 @@ importers: version: 3.22.4 optionalDependencies: '@novu/ee-billing-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/billing-web + '@novu/ee-echo-web': + specifier: ^0.24.1 + version: link:../../enterprise/packages/web/echo '@novu/ee-translation-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/translation-web devDependencies: '@babel/polyfill': @@ -925,25 +928,28 @@ importers: version: 7.12.1 '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.18.6(@babel/core@7.23.2) + version: 7.18.6(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@babel/runtime': specifier: ^7.20.13 version: 7.21.0 '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@pandacss/dev': specifier: ^0.34.0 version: 0.34.0(jsdom@24.0.0)(typescript@4.9.5) + '@playwright/test': + specifier: ^1.42.1 + version: 1.42.1 '@storybook/addon-actions': specifier: ^7.4.2 version: 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) @@ -961,13 +967,13 @@ importers: version: 7.4.2 '@storybook/preset-create-react-app': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0) + version: 7.4.2(@babel/core@7.24.4)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/react': specifier: ^7.4.2 version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) '@testing-library/jest-dom': specifier: ^4.2.4 version: 4.2.4 @@ -1044,19 +1050,19 @@ importers: specifier: ^10.0.1 version: 10.0.1(@nestjs/axios@2.0.0)(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@sentry/node': specifier: ^7.66.0 @@ -1154,7 +1160,7 @@ importers: version: 6.3.3 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) @@ -1181,15 +1187,15 @@ importers: version: 11.10.6(@emotion/react@11.10.6)(@types/react@17.0.62)(react@17.0.2) '@mantine/core': specifier: 4.2.12 - version: 4.2.12(@babel/core@7.23.2)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + version: 4.2.12(@babel/core@7.24.4)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) '@mantine/hooks': specifier: 4.2.12 version: 4.2.12(react@17.0.2) '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/notification-center '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared antd: specifier: ^4.10.0 @@ -1248,25 +1254,25 @@ importers: devDependencies: '@babel/plugin-proposal-optional-chaining': specifier: ^7.20.7 - version: 7.21.0(@babel/core@7.23.2) + version: 7.21.0(@babel/core@7.24.4) '@babel/plugin-transform-react-display-name': specifier: ^7.18.6 - version: 7.22.5(@babel/core@7.23.2) + version: 7.22.5(@babel/core@7.24.4) '@babel/plugin-transform-runtime': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/polyfill': specifier: ^7.12.1 version: 7.12.1 '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.22.15(@babel/core@7.23.2) + version: 7.22.15(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@babel/runtime': specifier: ^7.20.13 version: 7.21.0 @@ -1277,10 +1283,10 @@ importers: specifier: ^6.0.0 version: 6.3.1 '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@types/jest': specifier: ^29.5.0 @@ -1364,19 +1370,19 @@ importers: specifier: ^10.0.1 version: 10.0.1(@nestjs/axios@2.0.0)(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@sentry/node': specifier: ^7.40.0 @@ -1452,16 +1458,16 @@ importers: version: 8.3.2 optionalDependencies: '@novu/ee-auth': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/auth '@novu/ee-billing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/billing '@novu/ee-chimera-connect': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/chimera-connect '@novu/ee-translation': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/translation devDependencies: '@faker-js/faker': @@ -1558,16 +1564,16 @@ importers: specifier: ^10.2.2 version: 10.2.2(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(@nestjs/platform-socket.io@10.2.2)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@sentry/node': specifier: ^7.30.0 @@ -1694,13 +1700,13 @@ importers: specifier: 9.0.3 version: 9.0.3(@nestjs/common@10.2.2)(passport@0.6.0) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared passport: specifier: 0.6.0 @@ -1764,16 +1770,16 @@ importers: specifier: ^5.0.1 version: 5.1.1(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(reflect-metadata@0.1.13) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/dal '@novu/ee-dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared class-transformer: specifier: ^0.5.1 @@ -1852,16 +1858,16 @@ importers: specifier: ^5.7.1 version: 5.10.5(react@17.0.2) '@novu/client': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/client '@novu/design-system': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/design-system '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared '@novu/shared-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared-web '@rjsf/chakra-ui': specifier: ^5.17.1 @@ -1946,22 +1952,22 @@ importers: specifier: 9.0.3 version: 9.0.3(@nestjs/common@10.2.2)(passport@0.6.0) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/dal '@novu/ee-dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/stateless '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/testing axios: specifier: ^1.6.2 @@ -2043,19 +2049,19 @@ importers: specifier: 9.0.3 version: 9.0.3(@nestjs/common@10.2.2)(passport@0.6.0) '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/dal '@novu/ee-dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/stateless '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/testing axios: specifier: ^1.6.2 @@ -2128,10 +2134,10 @@ importers: specifier: 10.2.2 version: 10.2.2(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../../libs/shared mongoose: specifier: ^7.5.0 @@ -2180,16 +2186,16 @@ importers: specifier: ^7.1.8 version: 7.1.9(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13) '@novu/application-generic': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/application-generic '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/dal '@novu/ee-dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../libs/dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared class-transformer: specifier: ^0.5.1 @@ -2274,16 +2280,16 @@ importers: specifier: ^4.6.0 version: 4.6.0(monaco-editor@0.45.0)(react-dom@17.0.2)(react@17.0.2) '@novu/client': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../packages/client '@novu/design-system': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/design-system '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared '@novu/shared-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../libs/shared-web '@tanstack/react-query': specifier: ^4.20.4 @@ -2341,6 +2347,67 @@ importers: specifier: 4.9.5 version: 4.9.5 + enterprise/packages/web/echo: + dependencies: + '@mantine/core': + specifier: ^5.7.1 + version: 5.10.5(@emotion/react@11.10.6)(@mantine/hooks@5.10.5)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + '@mantine/hooks': + specifier: ^5.7.1 + version: 5.10.5(react@17.0.2) + '@novu/design-system': + specifier: ^0.24.1 + version: link:../../../../libs/design-system + '@novu/shared-web': + specifier: ^0.24.1 + version: link:../../../../libs/shared-web + '@rjsf/core': + specifier: ^5.17.1 + version: 5.17.1(@rjsf/utils@5.17.1)(react@17.0.2) + '@rjsf/validator-ajv8': + specifier: ^5.17.1 + version: 5.17.1(@rjsf/utils@5.17.1) + '@tanstack/react-query': + specifier: ^4.20.4 + version: 4.29.1(react-dom@17.0.2)(react@17.0.2) + react-router-dom: + specifier: 6.2.2 + version: 6.2.2(react-dom@17.0.2)(react@17.0.2) + tslib: + specifier: ^2.3.1 + version: 2.6.2 + devDependencies: + '@types/node': + specifier: ^18.11.12 + version: 18.18.5 + '@types/react': + specifier: ^17.0.0 + version: 17.0.62 + '@types/react-dom': + specifier: ^17.0.0 + version: 17.0.20 + eslint: + specifier: ^8.33.0 + version: 8.51.0 + eslint-plugin-react-hooks: + specifier: ^4.4.0 + version: 4.6.0(eslint@8.51.0) + react: + specifier: ^17.0.1 + version: 17.0.2 + react-dom: + specifier: ^17.0.1 + version: 17.0.2(react@17.0.2) + rimraf: + specifier: ^3.0.2 + version: 3.0.2 + ts-loader: + specifier: ~9.4.0 + version: 9.4.2(typescript@4.9.5)(webpack@5.78.0) + typescript: + specifier: 4.9.5 + version: 4.9.5 + libs/dal: dependencies: '@aws-sdk/client-s3': @@ -2353,7 +2420,7 @@ importers: specifier: ^6.0.0 version: 6.3.1 '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../shared JSONStream: specifier: ^1.3.5 @@ -2471,13 +2538,13 @@ importers: specifier: ^5.7.1 version: 5.10.5(@mantine/core@5.10.5)(@mantine/hooks@5.10.5)(react-dom@17.0.2)(react@17.0.2) '@novu/client': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/client '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../shared '@novu/shared-web': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../shared-web '@segment/analytics-next': specifier: 1.59.0 @@ -2527,7 +2594,7 @@ importers: version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/theming': specifier: ^7.4.2 version: 7.4.2(react-dom@17.0.2)(react@17.0.2) @@ -2579,6 +2646,9 @@ importers: react-router-dom: specifier: 6.2.2 version: 6.2.2(react-dom@17.0.2)(react@17.0.2) + react-scanner: + specifier: ^1.1.0 + version: 1.1.0 rimraf: specifier: ^3.0.2 version: 3.0.2 @@ -2607,7 +2677,7 @@ importers: libs/embed: dependencies: '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/notification-center '@types/iframe-resizer': specifier: ^3.5.8 @@ -2696,7 +2766,7 @@ importers: version: 0.8.5 ts-jest: specifier: ^27.1.3 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -2747,7 +2817,7 @@ importers: specifier: ^5.7.1 version: 5.10.5(react@17.0.2) '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../shared '@segment/analytics-next': specifier: 1.59.0 @@ -2802,10 +2872,10 @@ importers: specifier: ^6.0.0 version: 6.3.1 '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../dal '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../shared JSONStream: specifier: ^1.3.5 @@ -2935,202 +3005,202 @@ importers: specifier: '>=10' version: 10.2.2(@nestjs/common@10.2.2)(@nestjs/core@10.2.2)(@nestjs/platform-express@10.2.2) '@novu/africas-talking': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/africas-talking '@novu/apns': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/apns '@novu/azure-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/azure-sms '@novu/bandwidth': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/bandwidth '@novu/braze': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/braze '@novu/brevo-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/brevo-sms '@novu/bulk-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/bulk-sms '@novu/burst-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/burst-sms '@novu/clickatell': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/clickatell '@novu/clicksend': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/clicksend '@novu/dal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/dal '@novu/discord': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/discord '@novu/email-webhook': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/email-webhook '@novu/emailjs': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/emailjs '@novu/expo': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/expo '@novu/fcm': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/fcm '@novu/firetext': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/firetext '@novu/forty-six-elks': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/forty-six-elks '@novu/generic-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/generic-sms '@novu/getstream': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/getstream '@novu/grafana-on-call': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/grafana-on-call '@novu/gupshup': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/gupshup '@novu/infobip': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/infobip '@novu/isend-sms': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/isend-sms '@novu/kannel': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/kannel '@novu/mailersend': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mailersend '@novu/mailgun': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mailgun '@novu/mailjet': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mailjet '@novu/mailtrap': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mailtrap '@novu/mandrill': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mandrill '@novu/maqsam': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/maqsam '@novu/mattermost': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/mattermost '@novu/messagebird': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/messagebird '@novu/ms-teams': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/ms-teams '@novu/netcore': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/netcore '@novu/nexmo': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/nexmo '@novu/nodemailer': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/nodemailer '@novu/one-signal': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/one-signal '@novu/outlook365': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/outlook365 '@novu/plivo': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/plivo '@novu/plunk': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/plunk '@novu/postmark': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/postmark '@novu/push-webhook': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/push-webhook '@novu/pusher-beams': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/pusher-beams '@novu/pushpad': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/pushpad '@novu/resend': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/resend '@novu/ring-central': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/ring-central '@novu/rocket-chat': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/rocket-chat '@novu/ryver': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/ryver '@novu/sendchamp': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sendchamp '@novu/sendgrid': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sendgrid '@novu/sendinblue': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sendinblue '@novu/ses': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/ses '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@novu/simpletexting': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/simpletexting '@novu/slack': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/slack '@novu/sms-central': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sms-central '@novu/sms77': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sms77 '@novu/sns': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sns '@novu/sparkpost': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/sparkpost '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../stateless '@novu/telnyx': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/telnyx '@novu/termii': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/termii '@novu/testing': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/testing '@novu/twilio': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/twilio '@novu/zulip': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../providers/zulip '@opentelemetry/api': specifier: ^1.7.0 @@ -3153,6 +3223,9 @@ importers: '@opentelemetry/exporter-prometheus': specifier: ^0.46.0 version: 0.46.0(@opentelemetry/api@1.7.0) + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.49.1 + version: 0.49.1(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': specifier: ^0.46.0 version: 0.46.0(@opentelemetry/api@1.7.0) @@ -3254,7 +3327,7 @@ importers: version: 7.8.1 optionalDependencies: '@novu/ee-chimera-connect': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../enterprise/packages/chimera-connect '@taskforcesh/bullmq-pro': specifier: 5.1.14 @@ -3307,7 +3380,7 @@ importers: version: 9.2.4 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -3318,7 +3391,7 @@ importers: packages/cli: dependencies: '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@segment/analytics-node': specifier: ^1.1.4 @@ -3385,7 +3458,7 @@ importers: packages/client: dependencies: '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared devDependencies: '@types/jest': @@ -3408,7 +3481,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3419,10 +3492,10 @@ importers: packages/headless: dependencies: '@novu/client': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../client '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@tanstack/query-core': specifier: ^4.15.1 @@ -3433,10 +3506,10 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@types/jest': specifier: ^29.2.3 version: 29.5.0 @@ -3451,7 +3524,7 @@ importers: version: 29.5.0 ts-jest: specifier: ^29.0.3 - version: 29.1.0(@babel/core@7.23.2)(jest@29.5.0)(typescript@4.9.5) + version: 29.1.0(@babel/core@7.24.4)(jest@29.5.0)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3465,7 +3538,7 @@ importers: specifier: 10.2.2 version: 10.2.2(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -3503,7 +3576,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -3517,7 +3590,7 @@ importers: packages/node: dependencies: '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared axios-retry: specifier: ^3.8.0 @@ -3557,8 +3630,8 @@ importers: specifier: ^3.5.0 version: 3.8.3 jest: - specifier: ^27.0.6 - version: 27.5.1(ts-node@10.9.1) + specifier: ^29.7.0 + version: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) nock: specifier: ^13.1.3 version: 13.3.0 @@ -3575,8 +3648,8 @@ importers: specifier: 0.0.0 version: 0.0.0 ts-jest: - specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + specifier: ^29.1.2 + version: 29.1.2(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3602,10 +3675,10 @@ importers: specifier: ^5.7.1 version: 5.10.5(react@17.0.2) '@novu/client': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../client '@novu/shared': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../libs/shared '@tanstack/react-query': specifier: ^4.20.4 @@ -3640,13 +3713,13 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@babel/preset-react': specifier: ^7.13.13 - version: 7.22.15(@babel/core@7.23.2) + version: 7.22.15(@babel/core@7.24.4) '@babel/preset-typescript': specifier: ^7.13.0 - version: 7.21.4(@babel/core@7.23.2) + version: 7.21.4(@babel/core@7.24.4) '@storybook/addon-actions': specifier: ^7.4.2 version: 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) @@ -3667,7 +3740,7 @@ importers: version: 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@storybook/react-webpack5': specifier: ^7.4.2 - version: 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + version: 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@testing-library/dom': specifier: ^9.3.0 version: 9.3.0 @@ -3700,7 +3773,7 @@ importers: version: 8.8.2 babel-loader: specifier: ^8.2.4 - version: 8.3.0(@babel/core@7.23.2)(webpack@5.82.1) + version: 8.3.0(@babel/core@7.24.4)(webpack@5.82.1) compression-webpack-plugin: specifier: ^10.0.0 version: 10.0.0(webpack@5.82.1) @@ -3733,7 +3806,7 @@ importers: version: 5.3.9(@swc/core@1.3.49)(esbuild@0.18.20)(webpack@5.82.1) ts-jest: specifier: ^29.0.3 - version: 29.1.0(@babel/core@7.23.2)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5) + version: 29.1.0(@babel/core@7.24.4)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5) ts-loader: specifier: ~9.4.0 version: 9.4.2(typescript@4.9.5)(webpack@5.82.1) @@ -3780,7 +3853,7 @@ importers: specifier: ^16.2.0 version: 16.2.11(@angular/common@16.2.11)(@angular/core@16.2.11)(@angular/platform-browser@16.2.11)(rxjs@7.8.1) '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../notification-center rxjs: specifier: ~7.8.0 @@ -3844,7 +3917,7 @@ importers: specifier: ^15.0.0 || ^16.0.0 || ^17.0.0 version: 16.2.11(@angular/common@16.2.11)(@angular/compiler@16.2.11)(@angular/core@16.2.11)(@angular/platform-browser@16.2.11) '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../../notification-center '@types/react': specifier: ^17.0.0 @@ -3868,7 +3941,7 @@ importers: specifier: ^2.0.3 version: 2.0.3(vue@3.2.47) '@novu/notification-center': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../notification-center react: specifier: ^17.0.1 @@ -3967,7 +4040,7 @@ importers: version: 0.0.0 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) typedoc: specifier: ^0.24.0 version: 0.24.6(typescript@4.9.5) @@ -3978,7 +4051,7 @@ importers: providers/africas-talking: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless africastalking: specifier: ^0.6.2 @@ -4010,7 +4083,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4021,7 +4094,7 @@ importers: providers/apns: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@parse/node-apn': specifier: ^5.2.3 @@ -4059,7 +4132,7 @@ importers: version: 2.8.7 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4076,7 +4149,7 @@ importers: specifier: ^1.0.0 version: 1.0.0 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -4105,7 +4178,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4119,7 +4192,7 @@ importers: specifier: ^4.1.3 version: 4.1.3 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -4148,7 +4221,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4159,7 +4232,7 @@ importers: providers/braze: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless braze-api: specifier: ^2.5.6 @@ -4191,7 +4264,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4202,7 +4275,7 @@ importers: providers/brevo-sms: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless cross-fetch: specifier: ^4.0.0 @@ -4240,7 +4313,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4251,7 +4324,7 @@ importers: providers/bulk-sms: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.0 @@ -4283,7 +4356,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4294,7 +4367,7 @@ importers: providers/burst-sms: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4338,7 +4411,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4355,7 +4428,7 @@ importers: providers/clickatell: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4393,7 +4466,7 @@ importers: version: 2.8.7 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4407,7 +4480,7 @@ importers: providers/clicksend: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.0 @@ -4439,7 +4512,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4450,7 +4523,7 @@ importers: providers/discord: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4491,7 +4564,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4505,7 +4578,7 @@ importers: providers/email-webhook: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4537,7 +4610,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4548,7 +4621,7 @@ importers: providers/emailjs: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless emailjs: specifier: ^3.6.0 @@ -4589,7 +4662,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4603,7 +4676,7 @@ importers: providers/expo: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless expo-server-sdk: specifier: ^3.6.0 @@ -4644,7 +4717,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4658,7 +4731,7 @@ importers: providers/fcm: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless firebase-admin: specifier: ^11.10.1 @@ -4708,7 +4781,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4722,7 +4795,7 @@ importers: providers/firetext: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless node-fetch: specifier: ^3.2.10 @@ -4730,7 +4803,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -4772,7 +4845,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4786,7 +4859,7 @@ importers: providers/forty-six-elks: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4818,7 +4891,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4829,7 +4902,7 @@ importers: providers/generic-sms: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4861,7 +4934,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4872,7 +4945,7 @@ importers: providers/getstream: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4904,7 +4977,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4915,7 +4988,7 @@ importers: providers/grafana-on-call: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -4950,7 +5023,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -4961,8 +5034,8 @@ importers: providers/gupshup: dependencies: '@novu/stateless': - specifier: ^0.23.1 - version: 0.23.1 + specifier: ^0.24.1 + version: link:../../packages/stateless axios: specifier: ^1.6.7 version: 1.6.7 @@ -4972,7 +5045,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -5008,7 +5081,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5025,7 +5098,7 @@ importers: specifier: ^0.3.2 version: 0.3.2 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -5063,7 +5136,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5077,7 +5150,7 @@ importers: providers/isend-sms: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.0 @@ -5109,7 +5182,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5120,7 +5193,7 @@ importers: providers/kannel: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -5161,7 +5234,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5175,7 +5248,7 @@ importers: providers/mailersend: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless mailersend: specifier: ^1.3.1 @@ -5216,7 +5289,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5230,7 +5303,7 @@ importers: providers/mailgun: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless form-data: specifier: ^4.0.0 @@ -5277,7 +5350,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5291,7 +5364,7 @@ importers: providers/mailjet: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless node-mailjet: specifier: ^6.0.5 @@ -5335,7 +5408,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5349,7 +5422,7 @@ importers: providers/mailtrap: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless mailtrap: specifier: ^3.1.1 @@ -5381,7 +5454,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5395,7 +5468,7 @@ importers: specifier: ^1.0.50 version: 1.0.50 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -5439,7 +5512,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5453,7 +5526,7 @@ importers: providers/maqsam: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -5491,7 +5564,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5502,7 +5575,7 @@ importers: providers/mattermost: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -5534,7 +5607,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5545,7 +5618,7 @@ importers: providers/messagebird: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless messagebird: specifier: ^4.0.1 @@ -5577,7 +5650,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5588,7 +5661,7 @@ importers: providers/ms-teams: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -5629,7 +5702,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5646,7 +5719,7 @@ importers: providers/netcore: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -5687,7 +5760,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5701,7 +5774,7 @@ importers: providers/nexmo: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@vonage/auth': specifier: ^1.7.0 @@ -5745,7 +5818,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5759,7 +5832,7 @@ importers: providers/nodemailer: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless nodemailer: specifier: ^6.6.5 @@ -5803,7 +5876,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.7 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5817,7 +5890,7 @@ importers: providers/one-signal: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.0 @@ -5849,7 +5922,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5860,7 +5933,7 @@ importers: providers/outlook365: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless nodemailer: specifier: ^6.6.5 @@ -5904,7 +5977,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5918,7 +5991,7 @@ importers: providers/plivo: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless plivo: specifier: ^4.60.1 @@ -5959,7 +6032,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -5973,7 +6046,7 @@ importers: providers/plunk: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@plunk/node': specifier: 2.0.0 @@ -6005,7 +6078,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6016,7 +6089,7 @@ importers: providers/postmark: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6060,7 +6133,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6074,7 +6147,7 @@ importers: providers/push-webhook: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6106,7 +6179,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6117,7 +6190,7 @@ importers: providers/pusher-beams: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.0 @@ -6149,7 +6222,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6160,7 +6233,7 @@ importers: providers/pushpad: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless pushpad: specifier: 1.0.0 @@ -6192,7 +6265,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6203,7 +6276,7 @@ importers: providers/resend: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless resend: specifier: ^2.1.0 @@ -6244,7 +6317,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6258,7 +6331,7 @@ importers: providers/ring-central: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@ringcentral/sdk': specifier: ^5.0.1 @@ -6290,7 +6363,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6301,7 +6374,7 @@ importers: providers/rocket-chat: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6333,7 +6406,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6344,7 +6417,7 @@ importers: providers/ryver: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6376,7 +6449,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6387,7 +6460,7 @@ importers: providers/sendchamp: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6419,7 +6492,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6430,7 +6503,7 @@ importers: providers/sendgrid: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless '@sendgrid/mail': specifier: ^8.1.0 @@ -6471,7 +6544,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6485,7 +6558,7 @@ importers: providers/sendinblue: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6526,7 +6599,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6543,7 +6616,7 @@ importers: specifier: 3.382.0 version: 3.382.0 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless nodemailer: specifier: ^6.6.5 @@ -6584,7 +6657,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6598,7 +6671,7 @@ importers: providers/simpletexting: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6630,7 +6703,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6641,7 +6714,7 @@ importers: providers/slack: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6682,7 +6755,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6696,7 +6769,7 @@ importers: providers/sms-central: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6728,7 +6801,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6739,7 +6812,7 @@ importers: providers/sms77: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless node-fetch: specifier: ^2.6.7 @@ -6750,7 +6823,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -6786,7 +6859,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6803,7 +6876,7 @@ importers: specifier: ^3.382.0 version: 3.388.0 '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless devDependencies: '@istanbuljs/nyc-config-typescript': @@ -6841,7 +6914,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6855,7 +6928,7 @@ importers: providers/sparkpost: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -6899,7 +6972,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6913,7 +6986,7 @@ importers: providers/telnyx: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless telnyx: specifier: ^1.23.0 @@ -6954,7 +7027,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -6968,7 +7041,7 @@ importers: providers/termii: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless node-fetch: specifier: ^3.2.10 @@ -6976,7 +7049,7 @@ importers: devDependencies: '@babel/preset-env': specifier: ^7.23.2 - version: 7.23.2(@babel/core@7.23.2) + version: 7.23.2(@babel/core@7.24.4) '@istanbuljs/nyc-config-typescript': specifier: ^1.0.1 version: 1.0.2(nyc@15.1.0) @@ -7012,7 +7085,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -7026,7 +7099,7 @@ importers: providers/twilio: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless twilio: specifier: ^4.19.3 @@ -7067,7 +7140,7 @@ importers: version: 3.0.2 ts-jest: specifier: ^27.0.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -7081,7 +7154,7 @@ importers: providers/zulip: dependencies: '@novu/stateless': - specifier: ^0.24.0 + specifier: ^0.24.1 version: link:../../packages/stateless axios: specifier: ^1.6.2 @@ -7113,7 +7186,7 @@ importers: version: 3.0.2 ts-jest: specifier: ~27.1.5 - version: 27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) + version: 27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5) ts-node: specifier: ~10.9.1 version: 10.9.1(@types/node@14.18.42)(typescript@4.9.5) @@ -9896,10 +9969,21 @@ packages: '@babel/highlight': 7.22.13 chalk: 2.4.2 + /@babel/code-frame@7.24.2: + resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/highlight': 7.24.2 + picocolors: 1.0.0 + /@babel/compat-data@7.23.2: resolution: {integrity: sha512-0S9TQMmDHlqAZ2ITT95irXKfxN9bncq8ZCoJhun3nHL/lLUxd2NKBJYoNGWH7S0hz6fRQwWlAWn/ILM0C70KZQ==} engines: {node: '>=6.9.0'} + /@babel/compat-data@7.24.4: + resolution: {integrity: sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==} + engines: {node: '>=6.9.0'} + /@babel/core@7.21.4: resolution: {integrity: sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==} engines: {node: '>=6.9.0'} @@ -9991,6 +10075,28 @@ packages: transitivePeerDependencies: - supports-color + /@babel/core@7.24.4: + resolution: {integrity: sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==} + engines: {node: '>=6.9.0'} + dependencies: + '@ampproject/remapping': 2.2.1 + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-compilation-targets': 7.23.6 + '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.4) + '@babel/helpers': 7.24.4 + '@babel/parser': 7.24.4 + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + convert-source-map: 2.0.0 + debug: 4.3.4(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + /@babel/eslint-parser@7.21.3(@babel/core@7.23.2)(eslint@8.51.0): resolution: {integrity: sha512-kfhmPimwo6k4P8zxNs8+T7yR44q1LdpsZdE1NkCsVlfiuTPRfnGgjaF8Qgug9q9Pou17u6wneYF0lDCZJATMFg==} engines: {node: ^10.13.0 || ^12.13.0 || >=14.0.0} @@ -10024,6 +10130,15 @@ packages: '@jridgewell/trace-mapping': 0.3.19 jsesc: 2.5.2 + /@babel/generator@7.24.4: + resolution: {integrity: sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/types': 7.24.0 + '@jridgewell/gen-mapping': 0.3.5 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 2.5.2 + /@babel/helper-annotate-as-pure@7.18.6: resolution: {integrity: sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==} engines: {node: '>=6.9.0'} @@ -10055,6 +10170,16 @@ packages: lru-cache: 5.1.1 semver: 6.3.1 + /@babel/helper-compilation-targets@7.23.6: + resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/compat-data': 7.24.4 + '@babel/helper-validator-option': 7.23.5 + browserslist: 4.23.0 + lru-cache: 5.1.1 + semver: 6.3.1 + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.21.4): resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} engines: {node: '>=6.9.0'} @@ -10127,6 +10252,24 @@ packages: semver: 6.3.1 dev: true + /@babel/helper-create-class-features-plugin@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-jKkwA59IXcvSaiK2UN45kKwSC9o+KuoXsBDvHvU/7BecYIp8GQ2UwrVvFgJASUT+hBnwJx6MhvMCuMzwZZ7jlg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + semver: 6.3.1 + dev: true + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} engines: {node: '>=6.9.0'} @@ -10163,6 +10306,18 @@ packages: semver: 6.3.1 dev: true + /@babel/helper-create-regexp-features-plugin@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + regexpu-core: 5.3.2 + semver: 6.3.1 + dev: true + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.22.11): resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} peerDependencies: @@ -10206,6 +10361,21 @@ packages: resolve: 1.22.2 transitivePeerDependencies: - supports-color + dev: true + + /@babel/helper-define-polyfill-provider@0.4.3(@babel/core@7.24.4): + resolution: {integrity: sha512-WBrLmuPP47n7PNwsZ57pqam6G/RGo1vw/87b0Blc53tZNGZ4x7YvZ6HgQe2vo1W/FR20OgjeZuGXzudPiXHFug==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + debug: 4.3.4(supports-color@8.1.1) + lodash.debounce: 4.0.8 + resolve: 1.22.2 + transitivePeerDependencies: + - supports-color /@babel/helper-environment-visitor@7.22.20: resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} @@ -10271,6 +10441,20 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/helper-module-transforms@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/helper-module-transforms@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} engines: {node: '>=6.9.0'} @@ -10312,6 +10496,33 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/helper-validator-identifier': 7.22.20 + /@babel/helper-module-transforms@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-WhDWw1tdrlT0gMgUJSlX0IQvoO1eN279zrAUbVB+KpV2c3Tylz8+GnKOLllCS6Z/iZQEyVYxhZVUdPTqs2YYPw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + + /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.4): + resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-simple-access': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/helper-validator-identifier': 7.22.20 + /@babel/helper-optimise-call-expression@7.22.5: resolution: {integrity: sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==} engines: {node: '>=6.9.0'} @@ -10363,6 +10574,18 @@ packages: '@babel/helper-wrap-function': 7.22.20 dev: true + /@babel/helper-remap-async-to-generator@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-wrap-function': 7.22.20 + dev: true + /@babel/helper-replace-supers@7.22.20(@babel/core@7.21.4): resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} engines: {node: '>=6.9.0'} @@ -10411,6 +10634,18 @@ packages: '@babel/helper-optimise-call-expression': 7.22.5 dev: true + /@babel/helper-replace-supers@7.22.20(@babel/core@7.24.4): + resolution: {integrity: sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-member-expression-to-functions': 7.22.15 + '@babel/helper-optimise-call-expression': 7.22.5 + dev: true + /@babel/helper-simple-access@7.22.5: resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} engines: {node: '>=6.9.0'} @@ -10433,6 +10668,10 @@ packages: resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} engines: {node: '>=6.9.0'} + /@babel/helper-string-parser@7.24.1: + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + /@babel/helper-validator-identifier@7.22.20: resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} engines: {node: '>=6.9.0'} @@ -10446,6 +10685,10 @@ packages: resolution: {integrity: sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==} engines: {node: '>=6.9.0'} + /@babel/helper-validator-option@7.23.5: + resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} + engines: {node: '>=6.9.0'} + /@babel/helper-wrap-function@7.22.20: resolution: {integrity: sha512-pms/UwkOpnQe/PDAEdV/d7dVCoBbB+R4FvYoHGZz+4VPcg7RtYy2KP7S2lbuWM6FCSgob5wshfGESbC/hzNXZw==} engines: {node: '>=6.9.0'} @@ -10476,6 +10719,16 @@ packages: transitivePeerDependencies: - supports-color + /@babel/helpers@7.24.4: + resolution: {integrity: sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/template': 7.24.0 + '@babel/traverse': 7.24.1 + '@babel/types': 7.24.0 + transitivePeerDependencies: + - supports-color + /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} engines: {node: '>=6.9.0'} @@ -10484,6 +10737,15 @@ packages: chalk: 2.4.2 js-tokens: 4.0.0 + /@babel/highlight@7.24.2: + resolution: {integrity: sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-validator-identifier': 7.22.20 + chalk: 2.4.2 + js-tokens: 4.0.0 + picocolors: 1.0.0 + /@babel/parser@7.22.16: resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==} engines: {node: '>=6.0.0'} @@ -10504,7 +10766,13 @@ packages: hasBin: true dependencies: '@babel/types': 7.23.0 - dev: true + + /@babel/parser@7.24.4: + resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.0 /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} @@ -10536,6 +10804,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-FB9iYlz7rURmRJyXRKEnalYPPdn87H5no108cyuQQyMwlpJ2SJtpIUBI27kdTin956pz+LPypkPVPUTlxOmrsg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} engines: {node: '>=6.9.0'} @@ -10572,6 +10850,18 @@ packages: '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.23.2) dev: true + /@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-Hyph9LseGvAeeXzikV88bczhsrLrIZqDPxO+sSmAunMPaGrBGhfMWzCPYTtiW9t+HzSE2wtV8e5cc5P6r1xMDQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.13.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.4) + dev: true + /@babel/plugin-proposal-async-generator-functions@7.20.7(@babel/core@7.22.9): resolution: {integrity: sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==} engines: {node: '>=6.9.0'} @@ -10656,6 +10946,18 @@ packages: '@babel/helper-plugin-utils': 7.22.5 '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.2) + dev: true + + /@babel/plugin-proposal-optional-chaining@7.21.0(@babel/core@7.24.4): + resolution: {integrity: sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) /@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.23.2): resolution: {integrity: sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==} @@ -10709,6 +11011,15 @@ packages: '@babel/core': 7.23.2 dev: true + /@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4): + resolution: {integrity: sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + dev: true + /@babel/plugin-proposal-unicode-property-regex@7.18.6(@babel/core@7.22.9): resolution: {integrity: sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==} engines: {node: '>=4'} @@ -10757,6 +11068,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.24.4): + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} peerDependencies: @@ -10811,6 +11131,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.24.4): + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.22.11): resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} engines: {node: '>=6.9.0'} @@ -10841,6 +11170,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-decorators@7.21.0(@babel/core@7.23.2): resolution: {integrity: sha512-tIoPpGBR8UuM4++ccWN3gifhVvQu7ZizuR1fklhRJrd5ewgbkUS+0KVFeWWxELtn18NTLoW32XV7zyOgIAiz+w==} engines: {node: '>=6.9.0'} @@ -10888,6 +11227,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.22.11): resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} peerDependencies: @@ -10915,6 +11263,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-export-namespace-from@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-flow@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==} engines: {node: '>=6.9.0'} @@ -10925,6 +11282,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-flow@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-9RdCl0i+q0QExayk2nOS7853w08yLucnnPML6EN9S8fgMPVtdLDCdx/cOQ/i44Lb9UeQX9A35yaqBBOMMZxPxQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} engines: {node: '>=6.9.0'} @@ -10955,6 +11322,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-assertions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} engines: {node: '>=6.9.0'} @@ -10985,6 +11362,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-attributes@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} peerDependencies: @@ -11021,6 +11408,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} peerDependencies: @@ -11057,6 +11453,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.21.4): resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} engines: {node: '>=6.9.0'} @@ -11077,6 +11482,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-jsx@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} peerDependencies: @@ -11113,6 +11528,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} peerDependencies: @@ -11149,6 +11573,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.21.4): resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} peerDependencies: @@ -11185,6 +11618,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.24.4): + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} peerDependencies: @@ -11221,6 +11663,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} peerDependencies: @@ -11257,6 +11708,15 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.21.4): resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} peerDependencies: @@ -11291,6 +11751,15 @@ packages: dependencies: '@babel/core': 7.23.2 '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.24.4): + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.22.11): resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} @@ -11322,6 +11791,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.21.4): resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} @@ -11362,23 +11841,33 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.21.4): - resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} + /@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.24.4): + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.21.4 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.23.2): + /@babel/plugin-syntax-typescript@7.21.4(@babel/core@7.24.4): resolution: {integrity: sha512-xz0D39NvhQn4t4RNsHmDnnsaQizIlUkdtYvLs8La1BlfjQ6JEwxkJGeqJMW2tAXx+q6H+WFuUTXNdYVpEya0YA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-syntax-typescript@7.22.5(@babel/core@7.21.4): + resolution: {integrity: sha512-1mS2o03i7t1c6VzH6fdQ3OA8tcEIxwG18zIPRp+UY1Ihv6W+XZzBCVxExF9upussPXJ0xE9XRHwMoNs1ep/nRQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.21.4 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -11425,6 +11914,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.24.4): + resolution: {integrity: sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} engines: {node: '>=6.9.0'} @@ -11455,6 +11955,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-arrow-functions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.22.11): resolution: {integrity: sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==} engines: {node: '>=6.9.0'} @@ -11494,6 +12004,19 @@ packages: '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-async-generator-functions@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-BBYVGxbDVHfoeXbOwcagAkOQAm9NxoTdMGfTqghu1GrvadSaw6iW3Je6IcL5PNOw8VwjxqBECXy50/iCQSY/lQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} engines: {node: '>=6.9.0'} @@ -11530,6 +12053,18 @@ packages: '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-async-to-generator@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-remap-async-to-generator': 7.22.20(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} engines: {node: '>=6.9.0'} @@ -11560,6 +12095,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-block-scoped-functions@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} engines: {node: '>=6.9.0'} @@ -11590,6 +12135,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-block-scoping@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-cOsrbmIOXmf+5YbL99/S49Y3j46k/T16b9ml8bm9lP6N9US5iQ2yBK7gpui1pg0V/WMcXdkfKbTb7HXq9u+v4g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} engines: {node: '>=6.9.0'} @@ -11623,6 +12178,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-class-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} engines: {node: '>=6.9.0'} @@ -11659,6 +12225,18 @@ packages: '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-class-static-block@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-GMM8gGmqI7guS/llMFk1bJDkKfn3v3C4KHK9Yg1ey5qcHcOlKb0QvcMrgzvxo+T03/4szNh5lghY+fEC98Kq9g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.12.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} engines: {node: '>=6.9.0'} @@ -11713,6 +12291,24 @@ packages: globals: 11.12.0 dev: true + /@babel/plugin-transform-classes@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-VbbC3PGjBdE0wAWDdHM9G8Gm977pnYI0XpqMd6LrKISj8/DJXEsWqgRuTYaNE9Bv0JGhTZUzHDlMk18IpOuoqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-optimise-call-expression': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + '@babel/helper-split-export-declaration': 7.22.6 + globals: 11.12.0 + dev: true + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} engines: {node: '>=6.9.0'} @@ -11746,6 +12342,17 @@ packages: '@babel/template': 7.22.15 dev: true + /@babel/plugin-transform-computed-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/template': 7.22.15 + dev: true + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} engines: {node: '>=6.9.0'} @@ -11776,6 +12383,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-destructuring@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-vaMdgNXFkYrB+8lbgniSYWHsgqK5gjaMNcc84bMIOMRLH0L9AqYq3hwMdvnyqj1OPqea8UtjPEuS/DCenah1wg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} engines: {node: '>=6.9.0'} @@ -11809,6 +12426,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-dotall-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} engines: {node: '>=6.9.0'} @@ -11839,6 +12467,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-duplicate-keys@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} engines: {node: '>=6.9.0'} @@ -11872,6 +12510,17 @@ packages: '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-dynamic-import@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-g/21plo58sfteWjaO0ZNVb+uEOkJNjAaHhbejrnBmu011l/eNDScmkbjCC3l4FKb10ViaGU4aOkFznSu2zRHgA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} engines: {node: '>=6.9.0'} @@ -11905,6 +12554,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-exponentiation-operator@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-builder-binary-assignment-operator-visitor': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} engines: {node: '>=6.9.0'} @@ -11938,6 +12598,17 @@ packages: '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-export-namespace-from@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-xa7aad7q7OiT8oNZ1mU7NrISjlSkVdMbNxn9IuLZyL9AJEhs1Apba3I+u5riX1dIkdptP5EKDG5XDPByWxtehw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-flow-strip-types@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==} engines: {node: '>=6.9.0'} @@ -11949,6 +12620,17 @@ packages: '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-flow-strip-types@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-tujNbZdxdG0/54g/oua8ISToaXTFBf8EnSb5PgQSciIXWOWKX3S4+JR7ZE9ol8FZwf9kxitzkGQ+QWeov/mCiA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} engines: {node: '>=6.9.0'} @@ -11979,6 +12661,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-for-of@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-me6VGeHsx30+xh9fbDLLPi0J1HzmeIIyenoOQHuw2D4m2SAU3NrspX5XxJLBpqn5yrLzrlw2Iy3RA//Bx27iOA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} engines: {node: '>=6.9.0'} @@ -12015,6 +12707,18 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-function-name@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} engines: {node: '>=6.9.0'} @@ -12048,6 +12752,17 @@ packages: '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-json-strings@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-CxT5tCqpA9/jXFlme9xIBCc5RPtdDq3JpkkhgHQqtDdiTnTI0jtZ0QzXhr5DILeYifDPp2wvY2ad+7+hLMW5Pw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} engines: {node: '>=6.9.0'} @@ -12078,6 +12793,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} engines: {node: '>=6.9.0'} @@ -12111,6 +12836,17 @@ packages: '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-logical-assignment-operators@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-qQwRTP4+6xFCDV5k7gZBF3C31K34ut0tbEcTKxlX/0KXxm9GLcO14p570aWxFvVzx6QAfPgq7gaeIHXJC8LswQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} engines: {node: '>=6.9.0'} @@ -12141,6 +12877,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-member-expression-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} engines: {node: '>=6.9.0'} @@ -12174,6 +12920,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-modules-amd@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-xWT5gefv2HGSm4QHtgc1sYPbseOyf+FFDo2JbpE25GWl5BqTGO9IMwTYJRoIdjsF85GE+VegHxSCUt5EvoYTAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-modules-commonjs@7.22.15(@babel/core@7.23.2): resolution: {integrity: sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==} engines: {node: '>=6.9.0'} @@ -12186,6 +12943,18 @@ packages: '@babel/helper-simple-access': 7.22.5 dev: true + /@babel/plugin-transform-modules-commonjs@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-jWL4eh90w0HQOTKP2MoXXUpVxilxsB2Vl4ji69rSjS3EcZ/v4sBmn+A3NpepuJzBhOaEBbR7udonlHHn5DWidg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.22.20(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} engines: {node: '>=6.9.0'} @@ -12222,6 +12991,18 @@ packages: '@babel/helper-simple-access': 7.22.5 dev: true + /@babel/plugin-transform-modules-commonjs@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-32Xzss14/UVc7k9g775yMIvkVK8xwKE0DPdP5JTapr3+Z9w4tzeOuLNY6BXDQR6BdnzIlXnCGAzsk/ICHBLVWQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-simple-access': 7.22.5 + dev: true + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} engines: {node: '>=6.9.0'} @@ -12261,6 +13042,19 @@ packages: '@babel/helper-validator-identifier': 7.22.20 dev: true + /@babel/plugin-transform-modules-systemjs@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-qBej6ctXZD2f+DhlOC9yO47yEYgUh5CZNz/aBoH4j/3NOlRfJXJbY7xDQCqQVf9KbrqGzIWER1f23doHGrIHFg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-identifier': 7.22.20 + dev: true + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} engines: {node: '>=6.9.0'} @@ -12294,6 +13088,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-transforms': 7.23.0(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} engines: {node: '>=6.9.0'} @@ -12327,6 +13132,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-named-capturing-groups-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} engines: {node: '>=6.9.0'} @@ -12357,6 +13173,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-new-target@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} engines: {node: '>=6.9.0'} @@ -12390,6 +13216,17 @@ packages: '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-nullish-coalescing-operator@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-YZWOw4HxXrotb5xsjMJUDlLgcDXSfO9eCmdl1bgW4+/lAGdkjaEvOnQ4p5WKKdUgSzO39dgPl0pTnfxm0OAXcg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} engines: {node: '>=6.9.0'} @@ -12423,6 +13260,17 @@ packages: '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-numeric-separator@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-3dzU4QGPsILdJbASKhF/V2TVP+gJya1PsueQCxIPCEcerqF21oEcrob4mzjsp2Py/1nLfF5m+xYNMDpmA8vffg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} engines: {node: '>=6.9.0'} @@ -12465,6 +13313,20 @@ packages: '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-object-rest-spread@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-fEB+I1+gAmfAyxZcX1+ZUwLeAuuf8VIg67CTznZE0MqVFumWkh8xWtn58I4dxdVf080wn7gzWoF8vndOViJe9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} engines: {node: '>=6.9.0'} @@ -12498,6 +13360,17 @@ packages: '@babel/helper-replace-supers': 7.22.20(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-object-super@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-replace-supers': 7.22.20(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} engines: {node: '>=6.9.0'} @@ -12531,6 +13404,17 @@ packages: '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-optional-catch-binding@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-rli0WxesXUeCJnMYhzAglEjLWVDF6ahb45HuprcmQuLidBJFWjNnOzssk2kuc6e33FlLaiZhG/kUIzUMWdBKaQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.22.11): resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} engines: {node: '>=6.9.0'} @@ -12567,6 +13451,18 @@ packages: '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-optional-chaining@7.23.0(@babel/core@7.24.4): + resolution: {integrity: sha512-sBBGXbLJjxTzLBF5rFWaikMnOGOk/BmK6vVByIdEggZ7Vn6CvWXZyRkkLFK6WE0IF8jSliyOkUN6SScFgzCM0g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.22.11): resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} engines: {node: '>=6.9.0'} @@ -12597,6 +13493,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-parameters@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-hjk7qKIqhyzhhUvRT683TYQOFa/4cQKwQy7ALvTpODswN40MljzNDa0YldevS6tGbxwaEKVn502JmY0dP7qEtQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} engines: {node: '>=6.9.0'} @@ -12630,6 +13536,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-private-methods@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.22.11): resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} engines: {node: '>=6.9.0'} @@ -12669,6 +13586,19 @@ packages: '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-private-property-in-object@7.22.11(@babel/core@7.24.4): + resolution: {integrity: sha512-sSCbqZDBKHetvjSwpyWzhuHkmW5RummxJBVbYLkGkaiTOWGxml7SXt0iWa03bzxFIx7wOj3g/ILRd0RcJKBeSQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} engines: {node: '>=6.9.0'} @@ -12699,6 +13629,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-property-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-react-constant-elements@7.21.3(@babel/core@7.23.2): resolution: {integrity: sha512-4DVcFeWe/yDYBLp0kBmOGFJ6N2UYg7coGid1gdxb4co62dy/xISDMaYBXBVXEDhfgMk7qkbcYiGtwd5Q/hwDDQ==} engines: {node: '>=6.9.0'} @@ -12709,13 +13649,13 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-display-name@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.20.2 /@babel/plugin-transform-react-display-name@7.22.5(@babel/core@7.23.2): @@ -12728,14 +13668,24 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-display-name@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + + /@babel/plugin-transform-react-jsx-development@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 - '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.24.4) dev: true /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.23.2): @@ -12748,6 +13698,16 @@ packages: '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.2) dev: true + /@babel/plugin-transform-react-jsx-development@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) + dev: true + /@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2): resolution: {integrity: sha512-nTh2ogNUtxbiSbxaT4Ds6aXnXEipHweN9YRgOX/oNXdf0cCrGn/+2LozFa3lnPV5D90MkjhgckCPBrsoSc1a7g==} engines: {node: '>=6.9.0'} @@ -12768,17 +13728,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true - /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.23.2): + /@babel/plugin-transform-react-jsx@7.21.0(@babel/core@7.24.4): resolution: {integrity: sha512-6OAWljMvQrZjR2DaNhVfRz6dkCAVV+ymcLUmaf8bccGOHn2v5rHJK3tTpij0BuhdYWP4LLaqj5lwcdlpAAPuvg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-module-imports': 7.22.15 '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.2) + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) '@babel/types': 7.22.19 dev: true @@ -12796,13 +13756,27 @@ packages: '@babel/types': 7.23.0 dev: true - /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.23.2): + /@babel/plugin-transform-react-jsx@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-oKckg2eZFa8771O/5vi7XeTvmM6+O9cxZu+kanTU7tD4sin5nO/G8jGJhq8Hvt2Z0kUoEDRayuZLaUlYl8QuGA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) + '@babel/types': 7.23.0 + dev: true + + /@babel/plugin-transform-react-pure-annotations@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-annotate-as-pure': 7.18.6 '@babel/helper-plugin-utils': 7.22.5 dev: true @@ -12818,6 +13792,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-react-pure-annotations@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.22.11): resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} engines: {node: '>=6.9.0'} @@ -12851,6 +13836,17 @@ packages: regenerator-transform: 0.15.2 dev: true + /@babel/plugin-transform-regenerator@7.22.10(@babel/core@7.24.4): + resolution: {integrity: sha512-F28b1mDt8KcT5bUyJc/U9nwzw6cV+UmTeRlXYIl2TNqMMJif0Jeey9/RQ3C4NOd2zp0/TRsDns9ttj2L523rsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + regenerator-transform: 0.15.2 + dev: true + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} engines: {node: '>=6.9.0'} @@ -12881,6 +13877,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-reserved-words@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-runtime@7.22.9(@babel/core@7.22.9): resolution: {integrity: sha512-9KjBH61AGJetCPYp/IEyLEp47SyybZb0nDRpBvmtEkm+rUIwxdlKpyNHI1TmsGkeuLclJdleQHRZ8XLBnnh8CQ==} engines: {node: '>=6.9.0'} @@ -12913,6 +13919,23 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true + + /@babel/plugin-transform-runtime@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-XOntj6icgzMS58jPVtQpiuF6ZFWxQiJavISGx5KGjRj+3gqZr8+N6Kx+N9BApWzgS+DOjIZfXXj0ZesenOWDyA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-module-imports': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.4) + babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.24.4) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} @@ -12944,6 +13967,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-shorthand-properties@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} engines: {node: '>=6.9.0'} @@ -12977,6 +14010,17 @@ packages: '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 dev: true + /@babel/plugin-transform-spread@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-skip-transparent-expression-wrappers': 7.22.5 + dev: true + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} engines: {node: '>=6.9.0'} @@ -13007,6 +14051,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-sticky-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} engines: {node: '>=6.9.0'} @@ -13037,6 +14091,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-template-literals@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} engines: {node: '>=6.9.0'} @@ -13067,6 +14131,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-typeof-symbol@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.21.4): resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} engines: {node: '>=6.9.0'} @@ -13077,7 +14151,7 @@ packages: '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.21.4) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.21.4) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.21.4) dev: true /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.23.2): @@ -13090,7 +14164,20 @@ packages: '@babel/helper-annotate-as-pure': 7.22.5 '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.23.2) '@babel/helper-plugin-utils': 7.22.5 - '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.23.2) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.2) + dev: true + + /@babel/plugin-transform-typescript@7.21.3(@babel/core@7.24.4): + resolution: {integrity: sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-annotate-as-pure': 7.22.5 + '@babel/helper-create-class-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + '@babel/plugin-syntax-typescript': 7.21.4(@babel/core@7.24.4) dev: true /@babel/plugin-transform-typescript@7.22.15(@babel/core@7.23.2): @@ -13136,6 +14223,16 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-escapes@7.22.10(@babel/core@7.24.4): + resolution: {integrity: sha512-lRfaRKGZCBqDlRU3UIFovdp9c9mEvlylmpod0/OatICsSfuQ9YFthRo1tpTkGsklEefZdqlEFdY4A2dwTb6ohg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} engines: {node: '>=6.9.0'} @@ -13169,6 +14266,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-property-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} engines: {node: '>=6.9.0'} @@ -13202,6 +14310,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.22.11): resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} engines: {node: '>=6.9.0'} @@ -13235,6 +14354,17 @@ packages: '@babel/helper-plugin-utils': 7.22.5 dev: true + /@babel/plugin-transform-unicode-sets-regex@7.22.5(@babel/core@7.24.4): + resolution: {integrity: sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-create-regexp-features-plugin': 7.22.15(@babel/core@7.24.4) + '@babel/helper-plugin-utils': 7.22.5 + dev: true + /@babel/polyfill@7.12.1: resolution: {integrity: sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==} deprecated: 🚨 This package has been deprecated in favor of separate inclusion of a polyfill and regenerator-runtime (when needed). See the @babel/polyfill docs (https://babeljs.io/docs/en/babel-polyfill) for more information. @@ -13516,6 +14646,97 @@ packages: - supports-color dev: true + /@babel/preset-env@7.23.2(@babel/core@7.24.4): + resolution: {integrity: sha512-BW3gsuDD+rvHL2VO2SjAUNTBe5YrjsTiDyqamPDWY723na3/yPQ65X5oQkFVJZ0o50/2d+svm1rkPoJeR1KxVQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-compilation-targets': 7.22.15 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.24.4) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.24.4) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.24.4) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-export-namespace-from': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-import-assertions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-syntax-import-attributes': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.24.4) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.24.4) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.24.4) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-arrow-functions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-async-generator-functions': 7.23.2(@babel/core@7.24.4) + '@babel/plugin-transform-async-to-generator': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-block-scoped-functions': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-block-scoping': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-class-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-class-static-block': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-classes': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-computed-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-destructuring': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-dotall-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-duplicate-keys': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-dynamic-import': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-exponentiation-operator': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-export-namespace-from': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-for-of': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-function-name': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-json-strings': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-logical-assignment-operators': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-member-expression-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-modules-amd': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-commonjs': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-systemjs': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-modules-umd': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-named-capturing-groups-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-new-target': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-nullish-coalescing-operator': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-numeric-separator': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-object-rest-spread': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-object-super': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-optional-catch-binding': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-optional-chaining': 7.23.0(@babel/core@7.24.4) + '@babel/plugin-transform-parameters': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-private-methods': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-private-property-in-object': 7.22.11(@babel/core@7.24.4) + '@babel/plugin-transform-property-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-regenerator': 7.22.10(@babel/core@7.24.4) + '@babel/plugin-transform-reserved-words': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-shorthand-properties': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-spread': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-sticky-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-template-literals': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-typeof-symbol': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-escapes': 7.22.10(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-property-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-unicode-sets-regex': 7.22.5(@babel/core@7.24.4) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.24.4) + '@babel/types': 7.23.0 + babel-plugin-polyfill-corejs2: 0.4.6(@babel/core@7.24.4) + babel-plugin-polyfill-corejs3: 0.8.5(@babel/core@7.24.4) + babel-plugin-polyfill-regenerator: 0.5.3(@babel/core@7.24.4) + core-js-compat: 3.32.2 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + dev: true + /@babel/preset-flow@7.22.15(@babel/core@7.23.2): resolution: {integrity: sha512-dB5aIMqpkgbTfN5vDdTRPzjqtWiZcRESNR88QYnoPR+bmdYoluOzMX9tQerTv0XzSgZYctPfO1oc0N5zdog1ew==} engines: {node: '>=6.9.0'} @@ -13528,6 +14749,18 @@ packages: '@babel/plugin-transform-flow-strip-types': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/preset-flow@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-dB5aIMqpkgbTfN5vDdTRPzjqtWiZcRESNR88QYnoPR+bmdYoluOzMX9tQerTv0XzSgZYctPfO1oc0N5zdog1ew==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-transform-flow-strip-types': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/preset-modules@0.1.5(@babel/core@7.22.9): resolution: {integrity: sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==} peerDependencies: @@ -13563,19 +14796,30 @@ packages: esutils: 2.0.3 dev: true - /@babel/preset-react@7.18.6(@babel/core@7.23.2): + /@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.24.4): + resolution: {integrity: sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==} + peerDependencies: + '@babel/core': ^7.0.0-0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/types': 7.23.0 + esutils: 2.0.3 + dev: true + + /@babel/preset-react@7.18.6(@babel/core@7.24.4): resolution: {integrity: sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.20.2 '@babel/helper-validator-option': 7.21.0 - '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.23.2) - '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.23.2) + '@babel/plugin-transform-react-display-name': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.21.0(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-development': 7.18.6(@babel/core@7.24.4) + '@babel/plugin-transform-react-pure-annotations': 7.18.6(@babel/core@7.24.4) dev: true /@babel/preset-react@7.22.15(@babel/core@7.23.2): @@ -13593,6 +14837,21 @@ packages: '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.23.2) dev: true + /@babel/preset-react@7.22.15(@babel/core@7.24.4): + resolution: {integrity: sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-transform-react-display-name': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-development': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-pure-annotations': 7.22.5(@babel/core@7.24.4) + dev: true + /@babel/preset-typescript@7.21.4(@babel/core@7.23.2): resolution: {integrity: sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==} engines: {node: '>=6.9.0'} @@ -13607,6 +14866,20 @@ packages: '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.23.2) dev: true + /@babel/preset-typescript@7.21.4(@babel/core@7.24.4): + resolution: {integrity: sha512-sMLNWY37TCdRH/bJ6ZeeOH1nPuanED7Ai9Y/vH31IPqalioJ6ZNFUWONsakhv4r4n+I6gm5lmoE0olkgib/j/A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.22.5 + '@babel/helper-validator-option': 7.22.15 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-modules-commonjs': 7.22.15(@babel/core@7.24.4) + '@babel/plugin-transform-typescript': 7.21.3(@babel/core@7.24.4) + dev: true + /@babel/preset-typescript@7.23.2(@babel/core@7.23.2): resolution: {integrity: sha512-u4UJc1XsS1GhIGteM8rnGiIvf9rJpiVgMEeCnwlLA7WJPC+jcXWJAGxYmeqs5hOZD8BbAfnV5ezBOxQbb4OUxA==} engines: {node: '>=6.9.0'} @@ -13682,6 +14955,14 @@ packages: '@babel/types': 7.23.0 dev: true + /@babel/template@7.24.0: + resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + /@babel/traverse@7.23.2: resolution: {integrity: sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==} engines: {node: '>=6.9.0'} @@ -13699,6 +14980,23 @@ packages: transitivePeerDependencies: - supports-color + /@babel/traverse@7.24.1: + resolution: {integrity: sha512-xuU6o9m68KeqZbQuDt2TcKSxUw/mrsvavlEqQ1leZ/B+C9tk6E4sRWy97WaXgvq5E+nU3cXMxv3WKOCanVMCmQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.24.2 + '@babel/generator': 7.24.4 + '@babel/helper-environment-visitor': 7.22.20 + '@babel/helper-function-name': 7.23.0 + '@babel/helper-hoist-variables': 7.22.5 + '@babel/helper-split-export-declaration': 7.22.6 + '@babel/parser': 7.24.4 + '@babel/types': 7.24.0 + debug: 4.3.4(supports-color@8.1.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/types@7.22.19: resolution: {integrity: sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==} engines: {node: '>=6.9.0'} @@ -13715,6 +15013,14 @@ packages: '@babel/helper-validator-identifier': 7.22.20 to-fast-properties: 2.0.0 + /@babel/types@7.24.0: + resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.1 + '@babel/helper-validator-identifier': 7.22.20 + to-fast-properties: 2.0.0 + /@bandwidth/messaging@4.1.3: resolution: {integrity: sha512-cc1qLocHGxxqV7YNGOBxt6VhO+iGLfZnIq2htMP/xCgGOHqCtOVqHlQs80AETIMNEClXapShvn4TQrakx2h1/A==} engines: {node: '>=10'} @@ -16150,7 +17456,7 @@ packages: react: 17.0.2 dev: false - /@emotion/react@11.7.1(@babel/core@7.23.2)(@types/react@17.0.62)(react@17.0.2): + /@emotion/react@11.7.1(@babel/core@7.24.4)(@types/react@17.0.62)(react@17.0.2): resolution: {integrity: sha512-DV2Xe3yhkF1yT4uAUoJcYL1AmrnO5SVsdfvu+fBuS7IbByDeTVx9+wFmvx9Idzv7/78+9Mgx2Hcmr7Fex3tIyw==} peerDependencies: '@babel/core': ^7.0.0 @@ -16162,7 +17468,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@babel/runtime': 7.23.2 '@emotion/cache': 11.10.7 '@emotion/serialize': 1.1.1 @@ -17789,6 +19095,18 @@ packages: slash: 3.0.0 dev: true + /@jest/console@29.7.0: + resolution: {integrity: sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + chalk: 4.1.2 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/core@27.5.1(ts-node@10.9.1): resolution: {integrity: sha512-AK6/UTrvQD0Cd24NSqmIA6rKsu0tKIxfiCducZvqxYdmMisOYAsdItspT+fQDQYARPf8XgjAFZi0ogW2agH5nQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17876,6 +19194,49 @@ packages: - ts-node dev: true + /@jest/core@29.7.0(ts-node@10.9.1): + resolution: {integrity: sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/console': 29.7.0 + '@jest/reporters': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 3.8.0 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 29.7.0 + jest-config: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-resolve-dependencies: 29.7.0 + jest-runner: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + jest-watcher: 29.7.0 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /@jest/environment@27.5.1: resolution: {integrity: sha512-/WQjhPJe3/ghaol/4Bq480JKXV/Rfw8nQdN7f41fM8VDHLcxKXou6QyXAh3EFr9/bVG3x74z1NWDkP87EiY8gA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17896,12 +19257,29 @@ packages: jest-mock: 29.5.0 dev: true + /@jest/environment@29.7.0: + resolution: {integrity: sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + jest-mock: 29.7.0 + dev: true + /@jest/expect-utils@29.5.0: resolution: {integrity: sha512-fmKzsidoXQT2KwnrwE0SQq3uj8Z763vzR8LnLBwC2qYWEFpjX8daRsk6rHUM1QvNlEW/UJXNXm59ztmJJWs2Mg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: jest-get-type: 29.4.3 + /@jest/expect-utils@29.7.0: + resolution: {integrity: sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + dev: true + /@jest/expect@29.5.0: resolution: {integrity: sha512-PueDR2HGihN3ciUNGr4uelropW7rqUfTiOn+8u0leg/42UhblPxHkfoh0Ruu3I9Y1962P3u2DY4+h7GVTSVU6g==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -17912,6 +19290,16 @@ packages: - supports-color dev: true + /@jest/expect@29.7.0: + resolution: {integrity: sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + expect: 29.7.0 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/fake-timers@27.5.1: resolution: {integrity: sha512-/aPowoolwa07k7/oM3aASneNeBGCmGQsc3ugN4u6s4C/+s5M64MFo/+djTdiwcbQlRfFElGuDXWzaWj6QgKObQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17936,6 +19324,18 @@ packages: jest-util: 29.5.0 dev: true + /@jest/fake-timers@29.7.0: + resolution: {integrity: sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@sinonjs/fake-timers': 10.0.2 + '@types/node': 14.18.42 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /@jest/globals@27.5.1: resolution: {integrity: sha512-ZEJNB41OBQQgGzgyInAv0UUfDDj3upmHydjieSxFvTRuZElrx7tXg/uVQ5hYVEwiXs3+aMsAeEc9X7xiSKCm4Q==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17957,6 +19357,18 @@ packages: - supports-color dev: true + /@jest/globals@29.7.0: + resolution: {integrity: sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/types': 29.6.3 + jest-mock: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/reporters@27.5.1: resolution: {integrity: sha512-cPXh9hWIlVJMQkVk84aIvXuBB4uQQmFqZiacloFuGiP3ah1sbCxCosidXFDfqG8+6fO1oR2dTJTlsOy4VFmUfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -18032,6 +19444,43 @@ packages: - supports-color dev: true + /@jest/reporters@29.7.0: + resolution: {integrity: sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + '@types/node': 14.18.42 + chalk: 4.1.2 + collect-v8-coverage: 1.0.1 + exit: 0.1.2 + glob: 7.2.3 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.0 + istanbul-lib-instrument: 6.0.2 + istanbul-lib-report: 3.0.0 + istanbul-lib-source-maps: 4.0.1 + istanbul-reports: 3.1.5 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + jest-worker: 29.7.0 + slash: 3.0.0 + string-length: 4.0.2 + strip-ansi: 6.0.1 + v8-to-istanbul: 9.1.0 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/schemas@28.1.3: resolution: {integrity: sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg==} engines: {node: ^12.13.0 || ^14.15.0 || ^16.10.0 || >=17.0.0} @@ -18069,6 +19518,15 @@ packages: graceful-fs: 4.2.11 dev: true + /@jest/source-map@29.6.3: + resolution: {integrity: sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jridgewell/trace-mapping': 0.3.19 + callsites: 3.1.0 + graceful-fs: 4.2.11 + dev: true + /@jest/test-result@27.5.1: resolution: {integrity: sha512-EW35l2RYFUcUQxFJz5Cv5MTOxlJIQs4I7gxzi2zVU7PJhOwfYq1MdC5nhSmYjX1gmMmLPvB3sIaC+BkcHRBfag==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -18099,6 +19557,16 @@ packages: collect-v8-coverage: 1.0.1 dev: true + /@jest/test-result@29.7.0: + resolution: {integrity: sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/types': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + collect-v8-coverage: 1.0.1 + dev: true + /@jest/test-sequencer@27.5.1: resolution: {integrity: sha512-LCheJF7WB2+9JuCS7VB/EmGIdQuhtqjRNI9A43idHv3E4KltCTsPsLxvdaubFHSYwY/fNjMWjl6vNRhDiN7vpQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -18121,6 +19589,16 @@ packages: slash: 3.0.0 dev: true + /@jest/test-sequencer@29.7.0: + resolution: {integrity: sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + slash: 3.0.0 + dev: true + /@jest/transform@27.5.1: resolution: {integrity: sha512-ipON6WtYgl/1329g5AIJVbUuEh0wZVbdpGwC99Jw4LwuoBNS95MVphU6zOeD9pDkon+LLbFL7lOQRapbB8SCHw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -18166,6 +19644,29 @@ packages: transitivePeerDependencies: - supports-color + /@jest/transform@29.7.0: + resolution: {integrity: sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.2 + '@jest/types': 29.6.3 + '@jridgewell/trace-mapping': 0.3.19 + babel-plugin-istanbul: 6.1.1 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + micromatch: 4.0.5 + pirates: 4.0.5 + slash: 3.0.0 + write-file-atomic: 4.0.2 + transitivePeerDependencies: + - supports-color + dev: true + /@jest/types@24.9.0: resolution: {integrity: sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==} engines: {node: '>= 6'} @@ -18212,13 +19713,25 @@ packages: resolution: {integrity: sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - '@jest/schemas': 29.4.3 + '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.4 '@types/istanbul-reports': 3.0.1 '@types/node': 14.18.42 '@types/yargs': 17.0.24 chalk: 4.1.2 + /@jest/types@29.6.3: + resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/schemas': 29.6.3 + '@types/istanbul-lib-coverage': 2.0.4 + '@types/istanbul-reports': 3.0.1 + '@types/node': 14.18.42 + '@types/yargs': 17.0.24 + chalk: 4.1.2 + dev: true + /@jonkemp/package-utils@1.0.8: resolution: {integrity: sha512-bIcKnH5YmtTYr7S6J3J86dn/rFiklwRpOqbTOQ9C0WMmR9FKHVb3bxs2UYfqEmNb93O4nbA97sb6rtz33i9SyA==} dev: false @@ -18231,6 +19744,14 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 '@jridgewell/trace-mapping': 0.3.19 + /@jridgewell/gen-mapping@0.3.5: + resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + engines: {node: '>=6.0.0'} + dependencies: + '@jridgewell/set-array': 1.2.1 + '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping': 0.3.25 + /@jridgewell/resolve-uri@3.1.0: resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} engines: {node: '>=6.0.0'} @@ -18243,6 +19764,10 @@ packages: resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} engines: {node: '>=6.0.0'} + /@jridgewell/set-array@1.2.1: + resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} + engines: {node: '>=6.0.0'} + /@jridgewell/source-map@0.3.3: resolution: {integrity: sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==} dependencies: @@ -18267,6 +19792,12 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@jridgewell/trace-mapping@0.3.25: + resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + dependencies: + '@jridgewell/resolve-uri': 3.1.1 + '@jridgewell/sourcemap-codec': 1.4.15 + /@jridgewell/trace-mapping@0.3.9: resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} dependencies: @@ -19044,7 +20575,7 @@ packages: - debug dev: false - /@mantine/core@4.2.12(@babel/core@7.23.2)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): + /@mantine/core@4.2.12(@babel/core@7.24.4)(@mantine/hooks@4.2.12)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): resolution: {integrity: sha512-PZcVUvcSZiZmLR1moKBJFdFIh6a4C+TE2ao91kzTAlH5Qb8t/V3ONbfPk3swHoYr7OSLJQM8vZ7UD5sFDiq0/g==} peerDependencies: '@mantine/hooks': 4.2.12 @@ -19052,7 +20583,7 @@ packages: react-dom: '>=16.8.0' dependencies: '@mantine/hooks': 4.2.12(react@17.0.2) - '@mantine/styles': 4.2.12(@babel/core@7.23.2)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) + '@mantine/styles': 4.2.12(@babel/core@7.24.4)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2) '@popperjs/core': 2.11.7 '@radix-ui/react-scroll-area': 0.1.4(react@17.0.2) react: 17.0.2 @@ -19193,14 +20724,14 @@ packages: react-dom: 17.0.2(react@17.0.2) dev: false - /@mantine/styles@4.2.12(@babel/core@7.23.2)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): + /@mantine/styles@4.2.12(@babel/core@7.24.4)(@types/react@17.0.62)(react-dom@17.0.2)(react@17.0.2): resolution: {integrity: sha512-9q1DzW0UNW/ORMGLHfN2XABOSEm0ZQebhNlLD757R6OQouoLuUf9elUwgGOXSyogMlsAYoy84XbJ3ZbbTm4YCA==} peerDependencies: react: '>=16.8.0' react-dom: '>=16.8.0' dependencies: '@emotion/cache': 11.7.1 - '@emotion/react': 11.7.1(@babel/core@7.23.2)(@types/react@17.0.62)(react@17.0.2) + '@emotion/react': 11.7.1(@babel/core@7.24.4)(@types/react@17.0.62)(react@17.0.2) '@emotion/serialize': 1.0.2 '@emotion/utils': 1.0.0 clsx: 1.2.1 @@ -19879,15 +21410,6 @@ packages: vue-resize: 2.0.0-alpha.1(vue@3.2.47) dev: false - /@novu/stateless@0.23.1: - resolution: {integrity: sha512-eb0uMvAHyjvabx4+7TaZQ77gfdHDAJh9UJmBQ98lilB5/2+hH30LAC8Co7sVZOpwrhBy0LUpQn5/xaTqyd8oVQ==} - engines: {node: '>=10'} - dependencies: - handlebars: 4.7.7 - lodash.get: 4.4.2 - lodash.merge: 4.6.2 - dev: false - /@npmcli/arborist@5.3.0: resolution: {integrity: sha512-+rZ9zgL1lnbl8Xbb1NQdMjveOMwj4lIYfcDtyJHHi5x4X8jtR6m8SXooJMZy5vmFVZ8w7A2Bnd/oX9eTuU8w5A==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -21031,6 +22553,13 @@ packages: '@opentelemetry/api': 1.7.0 dev: false + /@opentelemetry/api-logs@0.49.1: + resolution: {integrity: sha512-kaNl/T7WzyMUQHQlVq7q0oV4Kev6+0xFwqzofryC66jgGMacd0QH5TwfpbUwSTby+SdAdprAe5UKMvBw4tKS5Q==} + engines: {node: '>=14'} + dependencies: + '@opentelemetry/api': 1.7.0 + dev: false + /@opentelemetry/api-metrics@0.25.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-9T0c9NQAEGRujUC7HzPa2/qZ5px/UvB2sfSU5CAKFRrAlDl2gn25B0oUbDqSRHW/IG1X2rnQ3z2bBQkJyJvE4g==} engines: {node: '>=8.0.0'} @@ -21149,6 +22678,16 @@ packages: '@opentelemetry/semantic-conventions': 1.19.0 dev: false + /@opentelemetry/core@1.22.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-0VoAlT6x+Xzik1v9goJ3pZ2ppi6+xd3aUfg4brfrLkDBHRIVjMP0eBHrKrhB+NKcDyMAg8fAbGL3Npg/F6AwWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/semantic-conventions': 1.22.0 + dev: false + /@opentelemetry/exporter-collector@0.25.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-xZYstLt4hz1aTloJaepWdjMMf9305MqwqbUWjcU/X9pOxvgFWRlchO6x/HQTw7ow0i/S+ShzC+greKnb+1WvLA==} engines: {node: '>=8.0.0'} @@ -21218,6 +22757,20 @@ packages: '@opentelemetry/sdk-trace-base': 1.19.0(@opentelemetry/api@1.7.0) dev: false + /@opentelemetry/exporter-trace-otlp-http@0.49.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-KOLtZfZvIrpGZLVvblKsiVQT7gQUZNKcUUH24Zz6Xbi7LJb9Vt6xtUZFYdR5IIjvt47PIqBKDWUQlU0o1wAsRw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-exporter-base': 0.49.1(@opentelemetry/api@1.7.0) + '@opentelemetry/otlp-transformer': 0.49.1(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.22.0(@opentelemetry/api@1.7.0) + dev: false + /@opentelemetry/exporter-trace-otlp-proto@0.46.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-A7PftDM57w1TLiirrhi8ceAnCpYkpUBObELdn239IyYF67zwngImGfBLf5Yo3TTAOA2Oj1TL76L8zWVL8W+Suw==} engines: {node: '>=14'} @@ -21253,7 +22806,7 @@ packages: '@opentelemetry/api': ^1.3.0 dependencies: '@opentelemetry/api': 1.7.0 - '@opentelemetry/sdk-metrics': 1.19.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.22.0(@opentelemetry/api@1.7.0) systeminformation: 5.21.22 dev: false @@ -21266,7 +22819,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21281,7 +22834,7 @@ packages: '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) '@opentelemetry/propagator-aws-xray': 1.3.1(@opentelemetry/api@1.7.0) '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/aws-lambda': 8.10.122 transitivePeerDependencies: - supports-color @@ -21297,7 +22850,7 @@ packages: '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) '@opentelemetry/propagation-utils': 0.30.5(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21324,7 +22877,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21338,7 +22891,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/connect': 3.4.36 transitivePeerDependencies: - supports-color @@ -21352,7 +22905,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21377,7 +22930,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 semver: 7.5.4 transitivePeerDependencies: - supports-color @@ -21392,7 +22945,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21406,7 +22959,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21420,7 +22973,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21433,7 +22986,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21472,7 +23025,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/hapi__hapi': 20.0.13 transitivePeerDependencies: - supports-color @@ -21502,7 +23055,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) '@opentelemetry/redis-common': 0.36.1 - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/ioredis4': /@types/ioredis@4.28.10 transitivePeerDependencies: - supports-color @@ -21516,7 +23069,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21530,7 +23083,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/koa': 2.13.9 '@types/koa__router': 12.0.3 transitivePeerDependencies: @@ -21557,7 +23110,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/memcached': 2.2.10 transitivePeerDependencies: - supports-color @@ -21571,8 +23124,8 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/sdk-metrics': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/sdk-metrics': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21586,7 +23139,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21599,7 +23152,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@opentelemetry/sql-common': 0.40.0(@opentelemetry/api@1.7.0) transitivePeerDependencies: - supports-color @@ -21613,7 +23166,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/mysql': 2.15.22 transitivePeerDependencies: - supports-color @@ -21627,7 +23180,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21640,7 +23193,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21654,7 +23207,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@opentelemetry/sql-common': 0.40.0(@opentelemetry/api@1.7.0) '@types/pg': 8.6.1 '@types/pg-pool': 2.0.4 @@ -21683,7 +23236,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) '@opentelemetry/redis-common': 0.36.1 - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21697,7 +23250,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) '@opentelemetry/redis-common': 0.36.1 - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21711,7 +23264,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21724,7 +23277,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21737,7 +23290,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 transitivePeerDependencies: - supports-color dev: false @@ -21750,7 +23303,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/instrumentation': 0.46.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color @@ -21794,6 +23347,16 @@ packages: '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) dev: false + /@opentelemetry/otlp-exporter-base@0.49.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-z6sHliPqDgJU45kQatAettY9/eVF58qVPaTuejw9YWfSRqid9pXPYeegDCSdyS47KAUgAtm+nC28K3pfF27HWg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + dev: false + /@opentelemetry/otlp-grpc-exporter-base@0.46.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-/KB/xfZZiWIY2JknvCoT/e9paIzQO3QCBN5gR6RyxpXM/AGx3YTAOKvB/Ts9Va19jo5aE74gB7emhFaCNy4Rmw==} engines: {node: '>=14'} @@ -21834,6 +23397,21 @@ packages: '@opentelemetry/sdk-trace-base': 1.19.0(@opentelemetry/api@1.7.0) dev: false + /@opentelemetry/otlp-transformer@0.49.1(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-Z+koA4wp9L9e3jkFacyXTGphSWTbOKjwwXMpb0CxNb0kjTHGUxhYRN8GnkLFsFo5NbZPjP07hwAqeEG/uCratQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.9.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.49.1 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-logs': 0.49.1(@opentelemetry/api-logs@0.49.1)(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-metrics': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/sdk-trace-base': 1.22.0(@opentelemetry/api@1.7.0) + dev: false + /@opentelemetry/propagation-utils@0.30.5(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-9ENyx6ptmjyYzL7le3FXk/lJc3cFFTrh9Y/ubO9velQZ64BdjpF9kOMJN3Z8KLJFVt66HYoWy9xlWoSIfS/ICg==} engines: {node: '>=14'} @@ -21850,7 +23428,7 @@ packages: '@opentelemetry/api': ^1.0.0 dependencies: '@opentelemetry/api': 1.7.0 - '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) dev: false /@opentelemetry/propagator-b3@1.19.0(@opentelemetry/api@1.7.0): @@ -21886,7 +23464,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 dev: false /@opentelemetry/resource-detector-aws@1.3.5(@opentelemetry/api@1.7.0): @@ -21898,7 +23476,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 dev: false /@opentelemetry/resource-detector-container@0.3.5(@opentelemetry/api@1.7.0): @@ -21909,7 +23487,7 @@ packages: dependencies: '@opentelemetry/api': 1.7.0 '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 dev: false /@opentelemetry/resource-detector-gcp@0.29.5(@opentelemetry/api@1.7.0): @@ -21921,7 +23499,7 @@ packages: '@opentelemetry/api': 1.7.0 '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) - '@opentelemetry/semantic-conventions': 1.19.0 + '@opentelemetry/semantic-conventions': 1.22.0 gcp-metadata: 6.1.0 transitivePeerDependencies: - encoding @@ -21950,6 +23528,17 @@ packages: '@opentelemetry/semantic-conventions': 1.19.0 dev: false + /@opentelemetry/resources@1.22.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-+vNeIFPH2hfcNL0AJk/ykJXoUCtR1YaDUZM+p3wZNU4Hq98gzq+7b43xbkXjadD9VhWIUQqEwXyY64q6msPj6A==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.22.0 + dev: false + /@opentelemetry/sdk-logs@0.46.0(@opentelemetry/api-logs@0.46.0)(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-Knlyk4+G72uEzNh6GRN1Fhmrj+/rkATI5/lOrevN7zRDLgp4kfyZBGGoWk7w+qQjlYvwhIIdPVxlIcipivdZIg==} engines: {node: '>=14'} @@ -21963,6 +23552,19 @@ packages: '@opentelemetry/resources': 1.19.0(@opentelemetry/api@1.7.0) dev: false + /@opentelemetry/sdk-logs@0.49.1(@opentelemetry/api-logs@0.49.1)(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-gCzYWsJE0h+3cuh3/cK+9UwlVFyHvj3PReIOCDOmdeXOp90ZjKRoDOJBc3mvk1LL6wyl1RWIivR8Rg9OToyesw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.9.0' + '@opentelemetry/api-logs': '>=0.39.1' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/api-logs': 0.49.1 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.7.0) + dev: false + /@opentelemetry/sdk-metrics-base@0.25.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-7fwPlAFB5Xw8mnVQfq0wqKNw3RXiAMad9T1bk5Sza9LK/L6hz8RTuHWCsFMsj+1OOSAaiPFuUMYrK1J75+2IAg==} engines: {node: '>=8.0.0'} @@ -21989,6 +23591,18 @@ packages: lodash.merge: 4.6.2 dev: false + /@opentelemetry/sdk-metrics@1.22.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-k6iIx6H3TZ+BVMr2z8M16ri2OxWaljg5h8ihGJxi/KQWcjign6FEaEzuigXt5bK9wVEhqAcWLCfarSftaNWkkg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.9.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.7.0) + lodash.merge: 4.6.2 + dev: false + /@opentelemetry/sdk-node@0.46.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-BQhzdCRZXchhKjZaFkgxlgoowjOt/QXekJ1CZgfvFO9Yg5GV15LyJFUEyQkDyD8XbshGo3Cnj0WZMBnDWtWY1A==} engines: {node: '>=14'} @@ -22038,6 +23652,18 @@ packages: '@opentelemetry/semantic-conventions': 1.19.0 dev: false + /@opentelemetry/sdk-trace-base@1.22.0(@opentelemetry/api@1.7.0): + resolution: {integrity: sha512-pfTuSIpCKONC6vkTpv6VmACxD+P1woZf4q0K46nSUvXFvOFqjBYKFaAMkKD3M1mlKUUh0Oajwj35qNjMl80m1Q==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.9.0' + dependencies: + '@opentelemetry/api': 1.7.0 + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/resources': 1.22.0(@opentelemetry/api@1.7.0) + '@opentelemetry/semantic-conventions': 1.22.0 + dev: false + /@opentelemetry/sdk-trace-node@1.19.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-TCiEq/cUjM15RFqBRwWomTVbOqzndWL4ILa7ZCu0zbjU1/XY6AgHkgrgAc7vGP6TjRqH4Xryuglol8tcIfbBUQ==} engines: {node: '>=14'} @@ -22063,6 +23689,11 @@ packages: engines: {node: '>=14'} dev: false + /@opentelemetry/semantic-conventions@1.22.0: + resolution: {integrity: sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==} + engines: {node: '>=14'} + dev: false + /@opentelemetry/sql-common@0.40.0(@opentelemetry/api@1.7.0): resolution: {integrity: sha512-vSqRJYUPJVjMFQpYkQS3ruexCPSZJ8esne3LazLwtCPaPRvzZ7WG3tX44RouAn7w4wMp8orKguBqtt+ng2UTnw==} engines: {node: '>=14'} @@ -22070,7 +23701,7 @@ packages: '@opentelemetry/api': ^1.1.0 dependencies: '@opentelemetry/api': 1.7.0 - '@opentelemetry/core': 1.19.0(@opentelemetry/api@1.7.0) + '@opentelemetry/core': 1.22.0(@opentelemetry/api@1.7.0) dev: false /@pandacss/config@0.34.0: @@ -22321,6 +23952,14 @@ packages: tslib: 2.6.2 dev: true + /@playwright/test@1.42.1: + resolution: {integrity: sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.42.1 + dev: true + /@plunk/node@2.0.0: resolution: {integrity: sha512-53lgots3fWGAo1QdS18BdEpJl7A29O1F9rYVn/7DfJ07SpJ1ZlzUeeWGVrWGL7PRRZb4a9Tw7Tt8Wnw0Xorhjg==} dependencies: @@ -24482,7 +26121,7 @@ packages: rollup: optional: true dependencies: - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 estree-walker: 2.0.2 picomatch: 2.3.1 dev: true @@ -28852,13 +30491,13 @@ packages: /@storybook/postinstall@7.4.2: resolution: {integrity: sha512-L9r14KqS87HPyXw0S3pK2X29ckel/4sdBSmy9nVF8n/ADafKE0pSLKB935VL0+88eMx06aT32SMcQoqjubGKWw==} - /@storybook/preset-create-react-app@7.4.2(@babel/core@7.23.2)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0): + /@storybook/preset-create-react-app@7.4.2(@babel/core@7.24.4)(react-refresh@0.14.0)(react-scripts@5.0.1)(typescript@4.9.5)(webpack-dev-server@4.11.1)(webpack@5.78.0): resolution: {integrity: sha512-rHRaiWmNAFXVHlRBG4iQE0Vsg3n4ZUyRWqddV2NuqZnHYQYUP07Rp0c3TFigGeTqF/gNbj8rTBDawcwpc8VkqQ==} peerDependencies: '@babel/core': '*' react-scripts: '>=5.0.0' dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.14.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/react-docgen-typescript-plugin': 1.0.6--canary.9.0c3f3b7.0(typescript@4.9.5)(webpack@5.78.0) '@storybook/types': 7.4.2 @@ -28880,7 +30519,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/preset-react-webpack@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): + /@storybook/preset-react-webpack@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): resolution: {integrity: sha512-CWWiwZa3/0zHnc6zLvI9Sgj12gJDTktZO87/gfwq2VfbWqAEUYsKs6NE4Pm0Yg9O4/IG8DHoHIB+bTNlLp/lCA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -28894,9 +30533,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 - '@babel/preset-flow': 7.22.15(@babel/core@7.23.2) - '@babel/preset-react': 7.22.15(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/preset-flow': 7.22.15(@babel/core@7.24.4) + '@babel/preset-react': 7.22.15(@babel/core@7.24.4) '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/core-webpack': 7.4.2 '@storybook/docs-tools': 7.4.2 @@ -28929,7 +30568,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/preset-react-webpack@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): + /@storybook/preset-react-webpack@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): resolution: {integrity: sha512-CWWiwZa3/0zHnc6zLvI9Sgj12gJDTktZO87/gfwq2VfbWqAEUYsKs6NE4Pm0Yg9O4/IG8DHoHIB+bTNlLp/lCA==} engines: {node: '>=16.0.0'} peerDependencies: @@ -28943,9 +30582,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 - '@babel/preset-flow': 7.22.15(@babel/core@7.23.2) - '@babel/preset-react': 7.22.15(@babel/core@7.23.2) + '@babel/core': 7.24.4 + '@babel/preset-flow': 7.22.15(@babel/core@7.24.4) + '@babel/preset-react': 7.22.15(@babel/core@7.24.4) '@pmmmwh/react-refresh-webpack-plugin': 0.5.10(react-refresh@0.11.0)(webpack-dev-server@4.11.1)(webpack@5.78.0) '@storybook/core-webpack': 7.4.2 '@storybook/docs-tools': 7.4.2 @@ -29066,7 +30705,7 @@ packages: react: 17.0.2 react-dom: 17.0.2(react@17.0.2) - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29080,9 +30719,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -29106,7 +30745,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29120,9 +30759,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.19)(@types/react@17.0.53)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-dev-server@4.11.1) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -29146,7 +30785,7 @@ packages: - webpack-plugin-serve dev: true - /@storybook/react-webpack5@7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5): + /@storybook/react-webpack5@7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5): resolution: {integrity: sha512-pnl11MYKM3jRmHQz2dSnEDfDiApdH7ys3zH/FjImsTK6S8etMKlxGnZ58Puxj05qvrBRgpxnQSL+ZazfrEX/6w==} engines: {node: '>=16.0.0'} peerDependencies: @@ -29160,9 +30799,9 @@ packages: typescript: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@storybook/builder-webpack5': 7.4.2(@types/react-dom@17.0.20)(@types/react@17.0.62)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) - '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.23.2)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) + '@storybook/preset-react-webpack': 7.4.2(@babel/core@7.24.4)(@swc/core@1.3.49)(esbuild@0.18.20)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5)(webpack-cli@5.1.4) '@storybook/react': 7.4.2(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.5) '@types/node': 16.11.7 react: 17.0.2 @@ -29674,7 +31313,7 @@ packages: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} dependencies: - '@babel/code-frame': 7.22.13 + '@babel/code-frame': 7.24.2 '@babel/runtime': 7.23.2 '@types/aria-query': 5.0.2 aria-query: 5.1.3 @@ -30295,16 +31934,18 @@ packages: resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} dev: true + /@types/estree@0.0.45: + resolution: {integrity: sha512-jnqIUKDUqJbDIUxm0Uj7bnlMnRm1T/eZ9N+AVMqhPgzrba2GhGG5o/jCTwmdPK709nEZsGoMzXEDUjcXHa3W0g==} + requiresBuild: true + dev: true + optional: true + /@types/estree@0.0.51: resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} /@types/estree@1.0.0: resolution: {integrity: sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ==} - /@types/estree@1.0.1: - resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} - dev: true - /@types/estree@1.0.5: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} @@ -30423,15 +32064,15 @@ packages: resolution: {integrity: sha512-RQUBI75F+uXruB95BFpC/8V8lPgJg4MQ6HxOCtAZYBB/h0FNCfrFfb4I+u2pZJIV7sKeszZbFqy1UnGeBMrvsA==} dev: false - /@types/inquirer@8.2.6: - resolution: {integrity: sha512-3uT88kxg8lNzY8ay2ZjP44DKcRaTGztqeIvN2zHvhzIBH/uAPaL75aBtdNRKbA7xXoMbBt5kX0M00VKAnfOYlA==} + /@types/inquirer@8.2.10: + resolution: {integrity: sha512-IdD5NmHyVjWM8SHWo/kPBgtzXatwPkfwzyP3fN1jF2g9BWt5WO+8hL2F4o2GKIYsU40PpqeevuUWvkS/roXJkA==} dependencies: '@types/through': 0.0.30 rxjs: 7.8.1 dev: true - /@types/inquirer@8.2.9: - resolution: {integrity: sha512-5IEO2PwCy4NZDgj977dho4Qbdiw6dJZJzD4ZaB/9j7dfppf7z0xxFPKZz/FtTAUQbDjmWHJ6Jlz/gn0YzWHPsw==} + /@types/inquirer@8.2.6: + resolution: {integrity: sha512-3uT88kxg8lNzY8ay2ZjP44DKcRaTGztqeIvN2zHvhzIBH/uAPaL75aBtdNRKbA7xXoMbBt5kX0M00VKAnfOYlA==} dependencies: '@types/through': 0.0.30 rxjs: 7.8.1 @@ -31220,7 +32861,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.5.0 + '@eslint-community/regexpp': 4.9.1 '@typescript-eslint/parser': 5.58.0(eslint@8.51.0)(typescript@4.9.5) '@typescript-eslint/scope-manager': 5.58.0 '@typescript-eslint/type-utils': 5.58.0(eslint@8.51.0)(typescript@4.9.5) @@ -31417,6 +33058,11 @@ packages: engines: {node: ^8.10.0 || ^10.13.0 || >=11.10.1} dev: true + /@typescript-eslint/types@5.20.0: + resolution: {integrity: sha512-+d8wprF9GyvPwtoB4CxBAR/s0rpP25XKgnOvMf/gMXYDvlUC3rPFHupdTQ/ow9vn7UDe5rX02ovGYQbv/IUCbg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + /@typescript-eslint/types@5.58.0: resolution: {integrity: sha512-JYV4eITHPzVQMnHZcYJXl2ZloC7thuUHrcUmxtzvItyKPvQ50kb9QXBkgNAt90OYMqwaodQh2kHutWZl1fc+1g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -31448,6 +33094,27 @@ packages: - supports-color dev: true + /@typescript-eslint/typescript-estree@5.20.0(typescript@4.6.3): + resolution: {integrity: sha512-36xLjP/+bXusLMrT9fMMYy1KJAGgHhlER2TqpUVDYUQg4w0q/NW/sg4UGAgVwAqb8V4zYg43KMUpM8vV2lve6w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.20.0 + '@typescript-eslint/visitor-keys': 5.20.0 + debug: 4.3.4(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.5.4 + tsutils: 3.21.0(typescript@4.6.3) + typescript: 4.6.3 + transitivePeerDependencies: + - supports-color + dev: true + /@typescript-eslint/typescript-estree@5.58.0(typescript@4.9.5): resolution: {integrity: sha512-cRACvGTodA+UxnYM2uwA2KCwRL7VAzo45syNysqlMyNyjw0Z35Icc9ihPJZjIYuA5bXJYiJ2YGUB59BqlOZT1Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -31578,6 +33245,14 @@ packages: eslint-visitor-keys: 2.1.0 dev: true + /@typescript-eslint/visitor-keys@5.20.0: + resolution: {integrity: sha512-1flRpNF+0CAQkMNlTJ6L/Z5jiODG/e5+7mk6XwtPOUS3UrTz3UOiAg9jG2VtKsWI6rZQfy4C6a232QNRZTRGlg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.20.0 + eslint-visitor-keys: 3.4.3 + dev: true + /@typescript-eslint/visitor-keys@5.58.0: resolution: {integrity: sha512-/fBraTlPj0jwdyTwLyrRTxv/3lnU2H96pNTVM6z3esTWLtA5MZ9ghSMJ7Rb+TtUAdtEw9EyJzJ0EydIMKxQ9gA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -32011,7 +33686,7 @@ packages: /@vue/compiler-core@3.3.7: resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==} dependencies: - '@babel/parser': 7.23.0 + '@babel/parser': 7.23.9 '@vue/shared': 3.3.7 estree-walker: 2.0.2 source-map-js: 1.0.2 @@ -33463,6 +35138,13 @@ packages: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} + /astray@1.1.1: + resolution: {integrity: sha512-LizvbENqdc8tdvrms/YyYoTtlr43INJni4YZSFr8nNdfOgafi82Hcrfhjm0MdwLhRFBrDhRwtH/0fnntlESxsQ==} + engines: {node: '>=8'} + optionalDependencies: + '@types/estree': 0.0.45 + dev: true + /astring@1.8.6: resolution: {integrity: sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg==} hasBin: true @@ -33644,7 +35326,7 @@ packages: /axios@0.21.4: resolution: {integrity: sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) transitivePeerDependencies: - debug dev: false @@ -33652,7 +35334,7 @@ packages: /axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -33660,7 +35342,7 @@ packages: /axios@0.28.0(debug@4.3.4): resolution: {integrity: sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33670,7 +35352,7 @@ packages: /axios@1.1.3: resolution: {integrity: sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33680,7 +35362,7 @@ packages: /axios@1.6.0: resolution: {integrity: sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33690,7 +35372,7 @@ packages: /axios@1.6.2: resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33699,7 +35381,7 @@ packages: /axios@1.6.7: resolution: {integrity: sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==} dependencies: - follow-redirects: 1.15.5 + follow-redirects: 1.15.5(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -33775,6 +35457,24 @@ packages: - supports-color dev: true + /babel-jest@29.7.0(@babel/core@7.23.2): + resolution: {integrity: sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.8.0 + dependencies: + '@babel/core': 7.23.2 + '@jest/transform': 29.7.0 + '@types/babel__core': 7.20.3 + babel-plugin-istanbul: 6.1.1 + babel-preset-jest: 29.6.3(@babel/core@7.23.2) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /babel-loader@8.3.0(@babel/core@7.21.4)(webpack@5.78.0): resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} @@ -33790,14 +35490,14 @@ packages: webpack: 5.78.0(@swc/core@1.3.49)(esbuild@0.18.20)(webpack-cli@5.1.4) dev: true - /babel-loader@8.3.0(@babel/core@7.23.2)(webpack@5.82.1): + /babel-loader@8.3.0(@babel/core@7.24.4)(webpack@5.82.1): resolution: {integrity: sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==} engines: {node: '>= 8.9'} peerDependencies: '@babel/core': ^7.0.0 webpack: '>=2' dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 @@ -33886,6 +35586,16 @@ packages: '@types/babel__traverse': 7.18.3 dev: true + /babel-plugin-jest-hoist@29.6.3: + resolution: {integrity: sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/template': 7.22.15 + '@babel/types': 7.23.0 + '@types/babel__core': 7.20.3 + '@types/babel__traverse': 7.18.3 + dev: true + /babel-plugin-macros@2.8.0: resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==} dependencies: @@ -33951,6 +35661,19 @@ packages: semver: 6.3.1 transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-corejs2@0.4.6(@babel/core@7.24.4): + resolution: {integrity: sha512-jhHiWVZIlnPbEUKSSNb9YoWcQGdlTLq7z1GHL4AjFxaoOUMuuEVJ+Y4pAaQUGOGk93YsVCKPbqbfw3m0SM6H8Q==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/compat-data': 7.23.2 + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + semver: 6.3.1 + transitivePeerDependencies: + - supports-color /babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.22.11): resolution: {integrity: sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==} @@ -33986,6 +35709,18 @@ packages: core-js-compat: 3.32.2 transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-corejs3@0.8.5(@babel/core@7.24.4): + resolution: {integrity: sha512-Q6CdATeAvbScWPNLB8lzSO7fgUVBkQt6zLgNlfyeCr/EQaEQR+bWiBYYPYAFyE528BMjRhL+1QBMOI4jc/c5TA==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + core-js-compat: 3.32.2 + transitivePeerDependencies: + - supports-color /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.22.11): resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} @@ -34018,6 +35753,17 @@ packages: '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.23.2) transitivePeerDependencies: - supports-color + dev: true + + /babel-plugin-polyfill-regenerator@0.5.3(@babel/core@7.24.4): + resolution: {integrity: sha512-8sHeDOmXC8csczMrYEOf0UTNa4yE2SxV5JGeT/LP1n0OYVDUUFPxG9vdk2AlDlIit4t+Kf0xCtpgXPBwnn/9pw==} + peerDependencies: + '@babel/core': ^7.4.0 || ^8.0.0-0 <8.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-define-polyfill-provider': 0.4.3(@babel/core@7.24.4) + transitivePeerDependencies: + - supports-color /babel-plugin-react-docgen@4.2.1: resolution: {integrity: sha512-UQ0NmGHj/HAqi5Bew8WvNfCk8wSsmdgNd8ZdMjBCICtyCJCq9LiqgqvjCYe570/Wg7AQArSq1VQ60Dd/CHN7mQ==} @@ -34119,6 +35865,17 @@ packages: babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.2) dev: true + /babel-preset-jest@29.6.3(@babel/core@7.23.2): + resolution: {integrity: sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@babel/core': ^7.0.0 + dependencies: + '@babel/core': 7.23.2 + babel-plugin-jest-hoist: 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.2) + dev: true + /babel-preset-react-app@10.0.1: resolution: {integrity: sha512-b0D9IZ1WhhCWkrTXyFuIIgqGzSkRIH5D5AmB0bXbzYAB1OBAwHcUeyWW2LorutLWF5btNo/N7r/cIdmvvKJlYg==} dependencies: @@ -36201,6 +37958,25 @@ packages: readable-stream: 3.6.2 dev: false + /create-jest@29.7.0(@types/node@14.18.42)(ts-node@10.9.1): + resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-config: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + jest-util: 29.7.0 + prompts: 2.4.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} @@ -36943,7 +38719,7 @@ packages: /cypress-intellij-reporter@0.0.7: resolution: {integrity: sha512-P4A0BPz5h9TWLZFVhMJsAMktqCEdeKA0+bS+zfTGLohUtM89pmU5kAWLEGFOYRcRlVR39XbUWhyFyTjs8AoFLA==} dependencies: - mocha: 10.3.0 + mocha: 10.4.0 dev: true /cypress-localstorage-commands@2.2.4(cypress@13.3.1): @@ -37270,7 +39046,6 @@ packages: dependencies: ms: 2.1.3 supports-color: 5.5.0 - dev: true /debug@3.2.7(supports-color@8.1.1): resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} @@ -37372,6 +39147,15 @@ packages: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} dev: true + /dedent@1.5.1: + resolution: {integrity: sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + dev: true + /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} engines: {node: '>=6'} @@ -37877,7 +39661,6 @@ packages: /dset@3.1.2: resolution: {integrity: sha512-g/M9sqy3oHe477Ar4voQxWtaPIFw1jTdKZuomOjhCcBx9nHUNn0pu6NopuFFrTh/TRZIKEj+76vLWFu9BNKk+Q==} engines: {node: '>=4'} - dev: false /duplexer2@0.1.4: resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} @@ -38557,7 +40340,7 @@ packages: /eslint-import-resolver-node@0.3.7: resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==} dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.0 resolve: 1.22.2 transitivePeerDependencies: @@ -38572,7 +40355,7 @@ packages: webpack: '>=1.11.0' dependencies: array.prototype.find: 2.2.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) enhanced-resolve: 0.9.1 eslint-plugin-import: 2.28.1(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-webpack@0.13.7)(eslint@8.38.0) find-root: 1.1.0 @@ -38610,7 +40393,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.38.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -38640,7 +40423,7 @@ packages: optional: true dependencies: '@typescript-eslint/parser': 5.58.0(eslint@8.51.0)(typescript@4.9.5) - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 eslint-import-resolver-webpack: 0.13.7(eslint-plugin-import@2.28.1)(webpack@5.78.0) @@ -38675,8 +40458,8 @@ packages: '@babel/plugin-transform-react-jsx': ^7.14.9 eslint: ^8.1.0 dependencies: - '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.23.2) - '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.23.2) + '@babel/plugin-syntax-flow': 7.22.5(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx': 7.22.15(@babel/core@7.24.4) eslint: 8.51.0 lodash: 4.17.21 string-natural-compare: 3.0.1 @@ -38721,7 +40504,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.38.0 eslint-import-resolver-node: 0.3.7 @@ -38756,7 +40539,7 @@ packages: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.1 array.prototype.flatmap: 1.3.1 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.51.0 eslint-import-resolver-node: 0.3.7 @@ -38839,7 +40622,7 @@ packages: damerau-levenshtein: 1.0.8 emoji-regex: 9.2.2 eslint: 8.51.0 - has: 1.0.3 + has: 1.0.4 jsx-ast-utils: 3.3.3 language-tags: 1.0.5 minimatch: 3.1.2 @@ -39503,6 +41286,17 @@ packages: jest-message-util: 29.5.0 jest-util: 29.5.0 + /expect@29.7.0: + resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/expect-utils': 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + dev: true + /expo-server-sdk@3.7.0: resolution: {integrity: sha512-SMZuBiIWejAdMMIOTjGQlprcwvSyLfeUQlooyGB5q6GvZ8zHjp+if8Q4k7xczUBTqIqTzs5IvTZnTiqA9Oe9WA==} dependencies: @@ -39793,6 +41587,17 @@ packages: dependencies: pend: 1.2.0 + /fdir@5.2.0(picomatch@2.3.1): + resolution: {integrity: sha512-skyI2Laxtj9GYzmktPgY6DT8uswXq+VoxH26SskykvEhTSbi7tRM/787uZt/p8maxrQCJdzC90zX1btbxiJ6lw==} + peerDependencies: + picomatch: 2.x + peerDependenciesMeta: + picomatch: + optional: true + dependencies: + picomatch: 2.3.1 + dev: true + /fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -40151,7 +41956,7 @@ packages: tslib: 2.6.2 dev: false - /follow-redirects@1.15.2(debug@4.3.4): + /follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} peerDependencies: @@ -40159,10 +41964,8 @@ packages: peerDependenciesMeta: debug: optional: true - dependencies: - debug: 4.3.4(supports-color@8.1.1) - /follow-redirects@1.15.5: + /follow-redirects@1.15.5(debug@4.3.4): resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} engines: {node: '>=4.0'} peerDependencies: @@ -40170,6 +41973,8 @@ packages: peerDependenciesMeta: debug: optional: true + dependencies: + debug: 4.3.4(supports-color@8.1.1) /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} @@ -40517,6 +42322,14 @@ packages: dev: true optional: true + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -41958,7 +43771,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -43177,6 +44990,19 @@ packages: transitivePeerDependencies: - supports-color + /istanbul-lib-instrument@6.0.2: + resolution: {integrity: sha512-1WUsZ9R1lA0HtBSohTkm39WTPlNKSJ5iFk7UwqXkBLoHQT+hfqPsfsTDVuZdKGaBwn7din9bS7SsnoAr943hvw==} + engines: {node: '>=10'} + dependencies: + '@babel/core': 7.24.4 + '@babel/parser': 7.23.9 + '@istanbuljs/schema': 0.1.3 + istanbul-lib-coverage: 3.2.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /istanbul-lib-processinfo@2.0.3: resolution: {integrity: sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==} engines: {node: '>=8'} @@ -43292,6 +45118,15 @@ packages: p-limit: 3.1.0 dev: true + /jest-changed-files@29.7.0: + resolution: {integrity: sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + execa: 5.1.1 + jest-util: 29.7.0 + p-limit: 3.1.0 + dev: true + /jest-circus@27.5.1: resolution: {integrity: sha512-D95R7x5UtlMA5iBYsOHFFbMD/GVA4R/Kdq15f7xYWUfWHBto9NYRsOvnSauTgdF+ogCpJ4tyKOXhUifxS65gdw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43347,6 +45182,35 @@ packages: - supports-color dev: true + /jest-circus@29.7.0: + resolution: {integrity: sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/expect': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.5.1 + is-generator-fn: 2.1.0 + jest-each: 29.7.0 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-runtime: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + p-limit: 3.1.0 + pretty-format: 29.7.0 + pure-rand: 6.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-cli@27.5.1(ts-node@10.9.1): resolution: {integrity: sha512-Hc6HOOwYq4/74/c62dEE3r5elx8wjYqxY0r0G/nFrLDPMFRu6RA/u8qINOIkvhxG7mMQ5EJsOGfRpI8L6eFUVw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43433,6 +45297,34 @@ packages: - ts-node dev: true + /jest-cli@29.7.0(@types/node@14.18.42)(ts-node@10.9.1): + resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + chalk: 4.1.2 + create-jest: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + exit: 0.1.2 + import-local: 3.1.0 + jest-config: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + jest-util: 29.7.0 + jest-validate: 29.7.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jest-config@27.5.1(ts-node@10.9.1): resolution: {integrity: sha512-5sAsjm6tGdsVbW9ahcChPAFCk4IlkQUknH5AvKjuLTSlcO/wCZKyFdn7Rg0EkC+OGgWODEy2hDpWB1PgzH0JNA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43594,6 +45486,47 @@ packages: - supports-color dev: true + /jest-config@29.7.0(@types/node@14.18.42)(ts-node@10.9.1): + resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + peerDependencies: + '@types/node': '*' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + ts-node: + optional: true + dependencies: + '@babel/core': 7.23.2 + '@jest/test-sequencer': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + babel-jest: 29.7.0(@babel/core@7.23.2) + chalk: 4.1.2 + ci-info: 3.8.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 29.7.0 + jest-environment-node: 29.7.0 + jest-get-type: 29.6.3 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-runner: 29.7.0 + jest-util: 29.7.0 + jest-validate: 29.7.0 + micromatch: 4.0.5 + parse-json: 5.2.0 + pretty-format: 29.7.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + ts-node: 10.9.1(@types/node@16.11.7)(typescript@4.9.5) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + dev: true + /jest-diff@24.9.0: resolution: {integrity: sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==} engines: {node: '>= 6'} @@ -43623,6 +45556,16 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.7.0 + /jest-diff@29.7.0: + resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + diff-sequences: 29.6.3 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-docblock@27.5.1: resolution: {integrity: sha512-rl7hlABeTsRYxKiUfpHrQrG4e2obOiTQWfMEH3PxPjOtdsfLQO4ReWSZaQ7DETm4xu07rl4q/h4zcKXyU0/OzQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43637,6 +45580,13 @@ packages: detect-newline: 3.1.0 dev: true + /jest-docblock@29.7.0: + resolution: {integrity: sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + detect-newline: 3.1.0 + dev: true + /jest-each@27.5.1: resolution: {integrity: sha512-1Ff6p+FbhT/bXQnEouYy00bkNSY7OUpfIcmdl8vZ31A1UUaurOLPA8a8BbJOF2RDUElwJhmeaV7LnagI+5UwNQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43659,6 +45609,17 @@ packages: pretty-format: 29.7.0 dev: true + /jest-each@29.7.0: + resolution: {integrity: sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + chalk: 4.1.2 + jest-get-type: 29.6.3 + jest-util: 29.7.0 + pretty-format: 29.7.0 + dev: true + /jest-environment-jsdom@27.5.1: resolution: {integrity: sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43724,6 +45685,18 @@ packages: jest-util: 29.5.0 dev: true + /jest-environment-node@29.7.0: + resolution: {integrity: sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + jest-mock: 29.7.0 + jest-util: 29.7.0 + dev: true + /jest-fetch-mock@3.0.3: resolution: {integrity: sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==} dependencies: @@ -43747,6 +45720,11 @@ packages: resolution: {integrity: sha512-J5Xez4nRRMjk8emnTpWrlkyb9pfRQQanDrvWHhsR1+VUfbwxi30eVcZFlcdGInRibU4G5LwHXpI7IRHU0CY+gg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /jest-get-type@29.6.3: + resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-haste-map@27.5.1: resolution: {integrity: sha512-7GgkZ4Fw4NFbMSDSpZwXeBiIbx+t/46nJ2QitkOjvwPYyZmqttu2TDSimMHP1EkPOi4xUZAN1doE5Vd25H4Jng==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43785,6 +45763,25 @@ packages: optionalDependencies: fsevents: 2.3.3 + /jest-haste-map@29.7.0: + resolution: {integrity: sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/graceful-fs': 4.1.6 + '@types/node': 14.18.42 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + graceful-fs: 4.2.11 + jest-regex-util: 29.6.3 + jest-util: 29.7.0 + jest-worker: 29.7.0 + micromatch: 4.0.5 + walker: 1.0.8 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /jest-jasmine2@27.5.1: resolution: {integrity: sha512-jtq7VVyG8SqAorDpApwiJJImd0V2wv1xzdheGHRGyuT7gZm6gG47QEskOlzsN1PG/6WNaCo5pmwMHDf3AkG2pQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43826,6 +45823,14 @@ packages: pretty-format: 29.7.0 dev: true + /jest-leak-detector@29.7.0: + resolution: {integrity: sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-matcher-utils@24.9.0: resolution: {integrity: sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==} engines: {node: '>= 6'} @@ -43855,6 +45860,16 @@ packages: jest-get-type: 29.4.3 pretty-format: 29.7.0 + /jest-matcher-utils@29.7.0: + resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + pretty-format: 29.7.0 + dev: true + /jest-message-util@27.5.1: resolution: {integrity: sha512-rMyFe1+jnyAAf+NHwTclDz0eAaLkVDdKVHHBFWsBWHnnh5YeJMNWWsv7AbFYXfK3oTqvL7VTWkhNLu1jX24D+g==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43899,6 +45914,21 @@ packages: slash: 3.0.0 stack-utils: 2.0.6 + /jest-message-util@29.7.0: + resolution: {integrity: sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/code-frame': 7.22.13 + '@jest/types': 29.6.3 + '@types/stack-utils': 2.0.1 + chalk: 4.1.2 + graceful-fs: 4.2.11 + micromatch: 4.0.5 + pretty-format: 29.7.0 + slash: 3.0.0 + stack-utils: 2.0.6 + dev: true + /jest-mock@27.5.1: resolution: {integrity: sha512-K4jKbY1d4ENhbrG2zuPWaQBvDly+iZ2yAW+T1fATN78hc0sInwn7wZB8XtlNnvHug5RMwV897Xm4LqmPM4e2Og==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43916,6 +45946,15 @@ packages: jest-util: 29.5.0 dev: true + /jest-mock@29.7.0: + resolution: {integrity: sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + jest-util: 29.7.0 + dev: true + /jest-node-exports-resolver@1.1.6: resolution: {integrity: sha512-NU412Qcb6WSRetCyEGMCC7IWHzO12LhSKaF1s9cyfM+EOYs4YN2gcNUT8hgu22X0oPFYNwLSPevgstBgLbD9ig==} dev: true @@ -43944,6 +45983,18 @@ packages: jest-resolve: 29.5.0 dev: true + /jest-pnp-resolver@1.2.3(jest-resolve@29.7.0): + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + dependencies: + jest-resolve: 29.7.0 + dev: true + /jest-regex-util@27.5.1: resolution: {integrity: sha512-4bfKq2zie+x16okqDXjXn9ql2B0dScQu+vcwe4TvFVhkVyuWLqpZrZtXxLLWoXYgn0E87I6r6GRYHF7wFZBUvg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43958,6 +46009,11 @@ packages: resolution: {integrity: sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + /jest-regex-util@29.6.3: + resolution: {integrity: sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /jest-resolve-dependencies@27.5.1: resolution: {integrity: sha512-QQOOdY4PE39iawDn5rzbIePNigfe5B9Z91GDD1ae/xNDlu9kaat8QQ5EKnNmVWPV54hUdxCVwwj6YMgR2O7IOg==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -43979,6 +46035,16 @@ packages: - supports-color dev: true + /jest-resolve-dependencies@29.7.0: + resolution: {integrity: sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + jest-regex-util: 29.6.3 + jest-snapshot: 29.7.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-resolve@27.5.1: resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44010,6 +46076,21 @@ packages: slash: 3.0.0 dev: true + /jest-resolve@29.7.0: + resolution: {integrity: sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-pnp-resolver: 1.2.3(jest-resolve@29.7.0) + jest-util: 29.7.0 + jest-validate: 29.7.0 + resolve: 1.22.2 + resolve.exports: 2.0.2 + slash: 3.0.0 + dev: true + /jest-runner@27.5.1: resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44071,6 +46152,35 @@ packages: - supports-color dev: true + /jest-runner@29.7.0: + resolution: {integrity: sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/console': 29.7.0 + '@jest/environment': 29.7.0 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + chalk: 4.1.2 + emittery: 0.13.1 + graceful-fs: 4.2.11 + jest-docblock: 29.7.0 + jest-environment-node: 29.7.0 + jest-haste-map: 29.7.0 + jest-leak-detector: 29.7.0 + jest-message-util: 29.7.0 + jest-resolve: 29.7.0 + jest-runtime: 29.7.0 + jest-util: 29.7.0 + jest-watcher: 29.7.0 + jest-worker: 29.7.0 + p-limit: 3.1.0 + source-map-support: 0.5.13 + transitivePeerDependencies: + - supports-color + dev: true + /jest-runtime@27.5.1: resolution: {integrity: sha512-o7gxw3Gf+H2IGt8fv0RiyE1+r83FJBRruoA+FXrlHw6xEyBsU8ugA6IPfTdVyA0w8HClpbK+DGJxH59UrNMx8A==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44131,6 +46241,36 @@ packages: - supports-color dev: true + /jest-runtime@29.7.0: + resolution: {integrity: sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/environment': 29.7.0 + '@jest/fake-timers': 29.7.0 + '@jest/globals': 29.7.0 + '@jest/source-map': 29.6.3 + '@jest/test-result': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + chalk: 4.1.2 + cjs-module-lexer: 1.2.2 + collect-v8-coverage: 1.0.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-haste-map: 29.7.0 + jest-message-util: 29.7.0 + jest-mock: 29.7.0 + jest-regex-util: 29.6.3 + jest-resolve: 29.7.0 + jest-snapshot: 29.7.0 + jest-util: 29.7.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + dev: true + /jest-serializer@27.5.1: resolution: {integrity: sha512-jZCyo6iIxO1aqUxpuBlwTDMkzOAJS4a3eYz3YzgxxVQFwLeSA7Jfq5cbqCY+JLvTDrWirgusI/0KwxKMgrdf7w==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44200,6 +46340,34 @@ packages: - supports-color dev: true + /jest-snapshot@29.7.0: + resolution: {integrity: sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@babel/core': 7.23.2 + '@babel/generator': 7.23.0 + '@babel/plugin-syntax-jsx': 7.22.5(@babel/core@7.23.2) + '@babel/plugin-syntax-typescript': 7.22.5(@babel/core@7.23.2) + '@babel/types': 7.23.0 + '@jest/expect-utils': 29.7.0 + '@jest/transform': 29.7.0 + '@jest/types': 29.6.3 + babel-preset-current-node-syntax: 1.0.1(@babel/core@7.23.2) + chalk: 4.1.2 + expect: 29.7.0 + graceful-fs: 4.2.11 + jest-diff: 29.7.0 + jest-get-type: 29.6.3 + jest-matcher-utils: 29.7.0 + jest-message-util: 29.7.0 + jest-util: 29.7.0 + natural-compare: 1.4.0 + pretty-format: 29.7.0 + semver: 7.5.4 + transitivePeerDependencies: + - supports-color + dev: true + /jest-transform-stub@2.0.0: resolution: {integrity: sha512-lspHaCRx/mBbnm3h4uMMS3R5aZzMwyNpNIJLXj4cEsV0mIUtS4IjYJLSoyjRCtnxb6RIGJ4NL2quZzfIeNhbkg==} dev: true @@ -44239,6 +46407,18 @@ packages: graceful-fs: 4.2.11 picomatch: 2.3.1 + /jest-util@29.7.0: + resolution: {integrity: sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + chalk: 4.1.2 + ci-info: 3.8.0 + graceful-fs: 4.2.11 + picomatch: 2.3.1 + dev: true + /jest-validate@27.5.1: resolution: {integrity: sha512-thkNli0LYTmOI1tDB3FI1S1RTp/Bqyd9pTarJwL87OIBFuqEb5Apv5EaApEudYg4g86e3CT6kM0RowkhtEnCBQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44263,6 +46443,18 @@ packages: pretty-format: 29.7.0 dev: true + /jest-validate@29.7.0: + resolution: {integrity: sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/types': 29.6.3 + camelcase: 6.3.0 + chalk: 4.1.2 + jest-get-type: 29.6.3 + leven: 3.1.0 + pretty-format: 29.7.0 + dev: true + /jest-watch-typeahead@1.1.0(jest@27.5.1): resolution: {integrity: sha512-Va5nLSJTN7YFtC2jd+7wsoe1pNe5K4ShLux/E5iHEwlB9AxaxmggY7to9KUqKojhaJw3aXqt5WAb4jGPOolpEw==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -44320,6 +46512,20 @@ packages: string-length: 4.0.2 dev: true + /jest-watcher@29.7.0: + resolution: {integrity: sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@jest/test-result': 29.7.0 + '@jest/types': 29.6.3 + '@types/node': 14.18.42 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 29.7.0 + string-length: 4.0.2 + dev: true + /jest-worker@26.6.2: resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} engines: {node: '>= 10.13.0'} @@ -44355,6 +46561,16 @@ packages: merge-stream: 2.0.0 supports-color: 8.1.1 + /jest-worker@29.7.0: + resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@types/node': 14.18.42 + jest-util: 29.7.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + dev: true + /jest@27.5.1(ts-node@10.9.1): resolution: {integrity: sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -44416,6 +46632,27 @@ packages: - ts-node dev: true + /jest@29.7.0(@types/node@14.18.42)(ts-node@10.9.1): + resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + dependencies: + '@jest/core': 29.7.0(ts-node@10.9.1) + '@jest/types': 29.6.3 + import-local: 3.1.0 + jest-cli: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - supports-color + - ts-node + dev: true + /jira-prepare-commit-msg@1.7.2: resolution: {integrity: sha512-vPmwqPoi5TfMF1rXh9XN6u7TiSG+FwdcbeL01nMBUbRRxTMXvIqQZoJSRoNoprgY1JUpYXplc3HGRSVsV22rLg==} engines: {node: '>=14'} @@ -44547,7 +46784,7 @@ packages: hasBin: true requiresBuild: true dependencies: - '@babel/parser': 7.23.0 + '@babel/parser': 7.23.9 '@jsdoc/salty': 0.2.5 '@types/markdown-it': 12.2.3 bluebird: 3.7.2 @@ -47296,8 +49533,8 @@ packages: yargs-unparser: 2.0.0 dev: true - /mocha@10.3.0: - resolution: {integrity: sha512-uF2XJs+7xSLsrmIvn37i/wnc91nw7XjOQB8ccyx5aEgdnohr7n+rEiZP23WkCYHjilR6+EboEnbq/ZQDz4LSbg==} + /mocha@10.4.0: + resolution: {integrity: sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==} engines: {node: '>= 14.0.0'} hasBin: true dependencies: @@ -47655,7 +49892,7 @@ packages: hasBin: true requiresBuild: true dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) iconv-lite: 0.6.3 sax: 1.2.4 transitivePeerDependencies: @@ -47994,6 +50231,7 @@ packages: /node-gyp-build@4.6.0: resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==} hasBin: true + requiresBuild: true /node-gyp@9.3.1: resolution: {integrity: sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==} @@ -49952,6 +52190,22 @@ packages: find-up: 3.0.0 dev: true + /playwright-core@1.42.1: + resolution: {integrity: sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==} + engines: {node: '>=16'} + hasBin: true + dev: true + + /playwright@1.42.1: + resolution: {integrity: sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.42.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + /please-upgrade-node@3.2.0: resolution: {integrity: sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==} dependencies: @@ -50030,7 +52284,7 @@ packages: engines: {node: '>= 0.12.0'} dependencies: async: 2.6.4 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) mkdirp: 0.5.6 transitivePeerDependencies: - supports-color @@ -52644,6 +54898,24 @@ packages: history: 5.3.0 react: 17.0.2 + /react-scanner@1.1.0: + resolution: {integrity: sha512-27G8K1TJ4FUp9+Ix5mkwhWnFdoIwYj95Ffwom0gWh3V+7zds1SRfbbRzi89EXMtZj9IiLhgEyBOBy/k6IysGNg==} + engines: {node: '>=14.x'} + hasBin: true + dependencies: + '@typescript-eslint/typescript-estree': 5.20.0(typescript@4.6.3) + astray: 1.1.1 + dlv: 1.1.3 + dset: 3.1.2 + fdir: 5.2.0(picomatch@2.3.1) + is-plain-object: 5.0.0 + picomatch: 2.3.1 + sade: 1.8.1 + typescript: 4.6.3 + transitivePeerDependencies: + - supports-color + dev: true + /react-scripts@5.0.1(@babel/plugin-syntax-flow@7.22.5)(@babel/plugin-transform-react-jsx@7.22.15)(@swc/core@1.3.49)(esbuild@0.18.20)(eslint-import-resolver-webpack@0.13.7)(eslint@8.51.0)(react@17.0.2)(ts-node@10.9.1)(typescript@4.9.5): resolution: {integrity: sha512-8VAmEm/ZAwQzJ+GOMLbBsTdDKOpuZh7RPs0UymvBR2vRk4iZWCskjbFnxqjrzoIvlNNRZ3QJFx6/qDSi6zSnaQ==} engines: {node: '>=14.0.0'} @@ -56665,7 +58937,7 @@ packages: tslib: 1.14.1 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@27.5.2)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -56686,7 +58958,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 27.5.2 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -56700,7 +58972,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@29.5.1)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -56721,7 +58993,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 29.5.1 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -56735,7 +59007,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@27.1.5(@babel/core@7.23.2)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5): + /ts-jest@27.1.5(@babel/core@7.24.4)(@types/jest@29.5.2)(jest@27.5.1)(typescript@4.9.5): resolution: {integrity: sha512-Xv6jBQPoBEvBq/5i2TeSG9tt/nqkbpcurrEG1b+2yfBrcJelOZF9Ml6dmyMh7bcW9JyFbRYpR5rxROSlBLTZHA==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} hasBin: true @@ -56756,7 +59028,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 '@types/jest': 29.5.2 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 @@ -56770,7 +59042,7 @@ packages: yargs-parser: 20.2.9 dev: true - /ts-jest@29.1.0(@babel/core@7.23.2)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.1.0(@babel/core@7.24.4)(esbuild@0.18.20)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -56791,7 +59063,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 esbuild: 0.18.20 fast-json-stable-stringify: 2.1.0 @@ -56805,7 +59077,7 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.1.0(@babel/core@7.23.2)(jest@29.5.0)(typescript@4.9.5): + /ts-jest@29.1.0(@babel/core@7.24.4)(jest@29.5.0)(typescript@4.9.5): resolution: {integrity: sha512-ZhNr7Z4PcYa+JjMl62ir+zPiNJfXJN6E8hSLnaUKhOgqcn8vb3e537cpkd0FuAfRK3sR1LSqM1MOhliXNgOFPA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} hasBin: true @@ -56826,7 +59098,7 @@ packages: esbuild: optional: true dependencies: - '@babel/core': 7.23.2 + '@babel/core': 7.24.4 bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 jest: 29.5.0(@types/node@14.18.42)(ts-node@10.9.1) @@ -56839,6 +59111,40 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.1.2(@babel/core@7.24.4)(jest@29.7.0)(typescript@4.9.5): + resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==} + engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3 <6' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + '@babel/core': 7.24.4 + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.7.0(@types/node@14.18.42)(ts-node@10.9.1) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.5.4 + typescript: 4.9.5 + yargs-parser: 21.1.1 + dev: true + /ts-loader@9.4.2(typescript@4.9.5)(webpack@5.78.0): resolution: {integrity: sha512-OmlC4WVmFv5I0PpaxYb+qGeGOdm5giHU7HwDDUjw59emP2UYMHy9fFSDcYgSNoH8sXcj4hGCSEhlDZ9ULeDraA==} engines: {node: '>=12.0.0'} @@ -56926,8 +59232,8 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 '@types/node': 16.11.7 - acorn: 8.11.3 - acorn-walk: 8.3.2 + acorn: 8.10.0 + acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -56957,8 +59263,8 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 '@types/node': 16.11.7 - acorn: 8.11.3 - acorn-walk: 8.3.2 + acorn: 8.10.0 + acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -56988,8 +59294,8 @@ packages: '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 '@types/node': 20.5.1 - acorn: 8.11.3 - acorn-walk: 8.3.2 + acorn: 8.10.0 + acorn-walk: 8.2.0 arg: 4.1.3 create-require: 1.1.1 diff: 4.0.2 @@ -57113,6 +59419,16 @@ packages: /tslib@2.6.2: resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + /tsutils@3.21.0(typescript@4.6.3): + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.6.3 + dev: true + /tsutils@3.21.0(typescript@4.9.5): resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} engines: {node: '>= 6'} @@ -57323,6 +59639,12 @@ packages: resolution: {integrity: sha512-GQ90TcKpIH4XxYTI2F98yEQYZgjNMOGPpOgdjIBhaLaWji5HPWlRnZ4AeA1hfBxtY7bCGDJsqDDHk/KaHOl5bA==} dev: true + /typescript@4.6.3: + resolution: {integrity: sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + /typescript@4.9.5: resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} engines: {node: '>=4.2.0'} @@ -58970,7 +61292,7 @@ packages: optional: true dependencies: '@types/eslint-scope': 3.7.4 - '@types/estree': 1.0.1 + '@types/estree': 1.0.5 '@webassemblyjs/ast': 1.11.5 '@webassemblyjs/wasm-edit': 1.11.5 '@webassemblyjs/wasm-parser': 1.11.5 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8118e6c86c2..ed81fa8c04c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,3 +11,4 @@ packages: # all packages in enterprise modules - 'enterprise/packages/*' - 'enterprise/packages/libs/*' + - 'enterprise/packages/web/*' diff --git a/providers/africas-talking/package.json b/providers/africas-talking/package.json index 35b0e05d9bc..e300e549d66 100644 --- a/providers/africas-talking/package.json +++ b/providers/africas-talking/package.json @@ -1,6 +1,6 @@ { "name": "@novu/africas-talking", - "version": "0.24.0", + "version": "0.24.1", "description": "An Africa's Talking wrapper for Novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -30,7 +30,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "africastalking": "^0.6.2" }, "devDependencies": { diff --git a/providers/apns/package.json b/providers/apns/package.json index f40a042636c..ae81adcf168 100644 --- a/providers/apns/package.json +++ b/providers/apns/package.json @@ -1,6 +1,6 @@ { "name": "@novu/apns", - "version": "0.24.0", + "version": "0.24.1", "description": "A apns wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -36,7 +36,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "@parse/node-apn": "^5.2.3" }, "devDependencies": { diff --git a/providers/azure-sms/package.json b/providers/azure-sms/package.json index b55e4e30229..39095c3d3ee 100644 --- a/providers/azure-sms/package.json +++ b/providers/azure-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/azure-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A azure-sms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -30,7 +30,7 @@ }, "dependencies": { "@azure/communication-sms": "^1.0.0", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "~1.0.1", diff --git a/providers/bandwidth/package.json b/providers/bandwidth/package.json index 1298b0865c4..1cc174eb1c7 100644 --- a/providers/bandwidth/package.json +++ b/providers/bandwidth/package.json @@ -1,6 +1,6 @@ { "name": "@novu/bandwidth", - "version": "0.24.0", + "version": "0.24.1", "description": "A bandwidth wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -30,7 +30,7 @@ }, "dependencies": { "@bandwidth/messaging": "^4.1.3", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "~1.0.1", diff --git a/providers/braze/package.json b/providers/braze/package.json index 33cb2d16b05..154aa48027c 100644 --- a/providers/braze/package.json +++ b/providers/braze/package.json @@ -1,6 +1,6 @@ { "name": "@novu/braze", - "version": "0.24.0", + "version": "0.24.1", "description": "A braze wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "braze-api": "^2.5.6" }, "devDependencies": { diff --git a/providers/brevo-sms/package.json b/providers/brevo-sms/package.json index ac8f1983999..c8bd4cfe6f4 100644 --- a/providers/brevo-sms/package.json +++ b/providers/brevo-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/brevo-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A brevo-sms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "cross-fetch": "^4.0.0", "proxy-agent": "^6.3.1" }, diff --git a/providers/bulk-sms/package.json b/providers/bulk-sms/package.json index 3d5fc356637..fc8eb084fa4 100644 --- a/providers/bulk-sms/package.json +++ b/providers/bulk-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/bulk-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A bulk-sms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.0" }, "devDependencies": { diff --git a/providers/burst-sms/package.json b/providers/burst-sms/package.json index 040e3f55eef..6e9961ca5a6 100644 --- a/providers/burst-sms/package.json +++ b/providers/burst-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/burst-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A burstSms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -37,7 +37,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2", "qs": "^6.11.0" }, diff --git a/providers/clickatell/package.json b/providers/clickatell/package.json index 8881aa0a46e..089a2f852be 100644 --- a/providers/clickatell/package.json +++ b/providers/clickatell/package.json @@ -1,6 +1,6 @@ { "name": "@novu/clickatell", - "version": "0.24.0", + "version": "0.24.1", "description": "A clickatell SMS provider wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -31,7 +31,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/clicksend/package.json b/providers/clicksend/package.json index b9e9595fb59..f226ac8c0b4 100644 --- a/providers/clicksend/package.json +++ b/providers/clicksend/package.json @@ -1,6 +1,6 @@ { "name": "@novu/clicksend", - "version": "0.24.0", + "version": "0.24.1", "description": "A clicksend wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.0" }, "devDependencies": { diff --git a/providers/discord/package.json b/providers/discord/package.json index 06ce40c7d44..708cf1f5ece 100644 --- a/providers/discord/package.json +++ b/providers/discord/package.json @@ -1,6 +1,6 @@ { "name": "@novu/discord", - "version": "0.24.0", + "version": "0.24.1", "description": "A discord wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/email-webhook/package.json b/providers/email-webhook/package.json index 2e3c22a3bce..d6ea4f4f7aa 100644 --- a/providers/email-webhook/package.json +++ b/providers/email-webhook/package.json @@ -1,6 +1,6 @@ { "name": "@novu/email-webhook", - "version": "0.24.0", + "version": "0.24.1", "description": "An email channel webhook provider wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -38,7 +38,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/emailjs/package.json b/providers/emailjs/package.json index 2cd95ef271a..beced3a5f00 100644 --- a/providers/emailjs/package.json +++ b/providers/emailjs/package.json @@ -1,6 +1,6 @@ { "name": "@novu/emailjs", - "version": "0.24.0", + "version": "0.24.1", "description": "An emailjs provider for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -45,7 +45,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "emailjs": "^3.6.0" }, "devDependencies": { diff --git a/providers/expo/package.json b/providers/expo/package.json index 6bec3ccb42e..6badb7d6ecf 100644 --- a/providers/expo/package.json +++ b/providers/expo/package.json @@ -1,6 +1,6 @@ { "name": "@novu/expo", - "version": "0.24.0", + "version": "0.24.1", "description": "A expo wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "expo-server-sdk": "^3.6.0" }, "devDependencies": { diff --git a/providers/fcm/package.json b/providers/fcm/package.json index 2dca1c24a56..965488faf6d 100644 --- a/providers/fcm/package.json +++ b/providers/fcm/package.json @@ -1,6 +1,6 @@ { "name": "@novu/fcm", - "version": "0.24.0", + "version": "0.24.1", "description": "A fcm wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "firebase-admin": "^11.10.1" }, "devDependencies": { diff --git a/providers/firetext/package.json b/providers/firetext/package.json index 6769a3c36f7..78bcc0b611d 100644 --- a/providers/firetext/package.json +++ b/providers/firetext/package.json @@ -1,6 +1,6 @@ { "name": "@novu/firetext", - "version": "0.24.0", + "version": "0.24.1", "description": "A firetext wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "node-fetch": "^3.2.10" }, "devDependencies": { diff --git a/providers/forty-six-elks/package.json b/providers/forty-six-elks/package.json index 6d385317390..e86e824bc55 100644 --- a/providers/forty-six-elks/package.json +++ b/providers/forty-six-elks/package.json @@ -1,6 +1,6 @@ { "name": "@novu/forty-six-elks", - "version": "0.24.0", + "version": "0.24.1", "description": "A 46elks wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/generic-sms/package.json b/providers/generic-sms/package.json index 82ab3f58830..1efe38bace5 100644 --- a/providers/generic-sms/package.json +++ b/providers/generic-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/generic-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A generic-sms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/getstream/package.json b/providers/getstream/package.json index 9836c49c4ca..63cc99ab47c 100644 --- a/providers/getstream/package.json +++ b/providers/getstream/package.json @@ -1,6 +1,6 @@ { "name": "@novu/getstream", - "version": "0.24.0", + "version": "0.24.1", "description": "A getstream wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/grafana-on-call/package.json b/providers/grafana-on-call/package.json index 4c0dcdf3fb2..d2d98da757e 100644 --- a/providers/grafana-on-call/package.json +++ b/providers/grafana-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@novu/grafana-on-call", - "version": "0.24.0", + "version": "0.24.1", "description": "A grafana-on-call wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2", "uuid": "^9.0.0" }, diff --git a/providers/gupshup/package.json b/providers/gupshup/package.json index d8ba602eb21..f30ae30d27a 100644 --- a/providers/gupshup/package.json +++ b/providers/gupshup/package.json @@ -1,6 +1,6 @@ { "name": "@novu/gupshup", - "version": "0.24.0", + "version": "0.24.1", "description": "A gupshup wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.23.1", + "@novu/stateless": "^0.24.1", "axios": "^1.6.7", "node-fetch": "^3.2.10" }, diff --git a/providers/infobip/package.json b/providers/infobip/package.json index 06fe1b6a831..b8cd73f1784 100644 --- a/providers/infobip/package.json +++ b/providers/infobip/package.json @@ -1,6 +1,6 @@ { "name": "@novu/infobip", - "version": "0.24.0", + "version": "0.24.1", "description": "A infobip wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -33,7 +33,7 @@ }, "dependencies": { "@infobip-api/sdk": "^0.3.2", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/providers/isend-sms/package.json b/providers/isend-sms/package.json index 853ddea826e..66c0d01bcee 100644 --- a/providers/isend-sms/package.json +++ b/providers/isend-sms/package.json @@ -1,6 +1,6 @@ { "name": "@novu/isend-sms", - "version": "0.24.0", + "version": "0.24.1", "description": "A isend-sms wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.0" }, "devDependencies": { diff --git a/providers/kannel/package.json b/providers/kannel/package.json index 6813616f61c..1bffe067f58 100644 --- a/providers/kannel/package.json +++ b/providers/kannel/package.json @@ -1,6 +1,6 @@ { "name": "@novu/kannel", - "version": "0.24.0", + "version": "0.24.1", "description": "A kannel wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/mailersend/package.json b/providers/mailersend/package.json index 941a53d9b85..825f3660153 100644 --- a/providers/mailersend/package.json +++ b/providers/mailersend/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mailersend", - "version": "0.24.0", + "version": "0.24.1", "description": "A mailersend wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "mailersend": "^1.3.1" }, "devDependencies": { diff --git a/providers/mailgun/package.json b/providers/mailgun/package.json index 3db7080f890..c77b139918e 100644 --- a/providers/mailgun/package.json +++ b/providers/mailgun/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mailgun", - "version": "0.24.0", + "version": "0.24.1", "description": "A mailgun wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "form-data": "^4.0.0", "mailgun.js": "^8.0.1", "nock": "^13.1.3" diff --git a/providers/mailjet/package.json b/providers/mailjet/package.json index c851934a804..2e3aaf9d9d5 100644 --- a/providers/mailjet/package.json +++ b/providers/mailjet/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mailjet", - "version": "0.24.0", + "version": "0.24.1", "description": "A mailjet wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "node-mailjet": "^6.0.5" }, "devDependencies": { diff --git a/providers/mailtrap/package.json b/providers/mailtrap/package.json index 167309cfe9d..1a1a18846dd 100644 --- a/providers/mailtrap/package.json +++ b/providers/mailtrap/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mailtrap", - "version": "0.24.0", + "version": "0.24.1", "description": "A mailtrap wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "mailtrap": "^3.1.1" }, "devDependencies": { diff --git a/providers/mandrill/package.json b/providers/mandrill/package.json index 2b6e4428a25..53f210275ea 100644 --- a/providers/mandrill/package.json +++ b/providers/mandrill/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mandrill", - "version": "0.24.0", + "version": "0.24.1", "description": "A mandrill wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -42,7 +42,7 @@ }, "dependencies": { "@mailchimp/mailchimp_transactional": "^1.0.50", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "1.0.2", diff --git a/providers/maqsam/package.json b/providers/maqsam/package.json index 25171149609..3a2ae95544a 100644 --- a/providers/maqsam/package.json +++ b/providers/maqsam/package.json @@ -1,6 +1,6 @@ { "name": "@novu/maqsam", - "version": "0.24.0", + "version": "0.24.1", "description": "A maqsam wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2", "date-fns": "2.29.3", "moment": "^2.29.4" diff --git a/providers/mattermost/package.json b/providers/mattermost/package.json index a007ead7890..280bf7342b1 100644 --- a/providers/mattermost/package.json +++ b/providers/mattermost/package.json @@ -1,6 +1,6 @@ { "name": "@novu/mattermost", - "version": "0.24.0", + "version": "0.24.1", "description": "A mattermost wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/messagebird/package.json b/providers/messagebird/package.json index 2160998722f..95a4974c00c 100644 --- a/providers/messagebird/package.json +++ b/providers/messagebird/package.json @@ -1,6 +1,6 @@ { "name": "@novu/messagebird", - "version": "0.24.0", + "version": "0.24.1", "description": "A messagebird wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "messagebird": "^4.0.1" }, "devDependencies": { diff --git a/providers/ms-teams/package.json b/providers/ms-teams/package.json index 82134b4bd80..734a1b4731a 100644 --- a/providers/ms-teams/package.json +++ b/providers/ms-teams/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ms-teams", - "version": "0.24.0", + "version": "0.24.1", "description": "A MS-Teams wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/netcore/package.json b/providers/netcore/package.json index 4d68a3be5d7..2c4081216f5 100644 --- a/providers/netcore/package.json +++ b/providers/netcore/package.json @@ -1,6 +1,6 @@ { "name": "@novu/netcore", - "version": "0.24.0", + "version": "0.24.1", "description": "A netcore wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -45,7 +45,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/nexmo/package.json b/providers/nexmo/package.json index aa9b3a29f16..28c67cf8655 100644 --- a/providers/nexmo/package.json +++ b/providers/nexmo/package.json @@ -1,6 +1,6 @@ { "name": "@novu/nexmo", - "version": "0.24.0", + "version": "0.24.1", "description": "A nexmo wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "@vonage/auth": "^1.7.0", "@vonage/server-sdk": "^3.10.0" }, diff --git a/providers/nodemailer/package.json b/providers/nodemailer/package.json index f433de729e7..e8bce50edeb 100644 --- a/providers/nodemailer/package.json +++ b/providers/nodemailer/package.json @@ -1,6 +1,6 @@ { "name": "@novu/nodemailer", - "version": "0.24.0", + "version": "0.24.1", "description": "A nodemailer wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -45,7 +45,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "nodemailer": "^6.6.5" }, "devDependencies": { diff --git a/providers/one-signal/package.json b/providers/one-signal/package.json index 3601f286935..31449464e64 100644 --- a/providers/one-signal/package.json +++ b/providers/one-signal/package.json @@ -1,6 +1,6 @@ { "name": "@novu/one-signal", - "version": "0.24.0", + "version": "0.24.1", "description": "A OneSignal wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -30,7 +30,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.0" }, "devDependencies": { diff --git a/providers/outlook365/package.json b/providers/outlook365/package.json index a2ddcfedaf1..2cd2517cff3 100644 --- a/providers/outlook365/package.json +++ b/providers/outlook365/package.json @@ -1,6 +1,6 @@ { "name": "@novu/outlook365", - "version": "0.24.0", + "version": "0.24.1", "description": "A outlook365 wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "nodemailer": "^6.6.5" }, "devDependencies": { diff --git a/providers/plivo/package.json b/providers/plivo/package.json index 837524ff6d0..e3e346b955e 100644 --- a/providers/plivo/package.json +++ b/providers/plivo/package.json @@ -1,6 +1,6 @@ { "name": "@novu/plivo", - "version": "0.24.0", + "version": "0.24.1", "description": "A plivo wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -44,7 +44,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "plivo": "^4.60.1" }, "devDependencies": { diff --git a/providers/plunk/package.json b/providers/plunk/package.json index 413aa47a2ec..b19a4770192 100644 --- a/providers/plunk/package.json +++ b/providers/plunk/package.json @@ -1,6 +1,6 @@ { "name": "@novu/plunk", - "version": "0.24.0", + "version": "0.24.1", "description": "A plunk wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "@plunk/node": "2.0.0" }, "devDependencies": { diff --git a/providers/postmark/package.json b/providers/postmark/package.json index b3f87c5d763..7e13168c222 100644 --- a/providers/postmark/package.json +++ b/providers/postmark/package.json @@ -1,6 +1,6 @@ { "name": "@novu/postmark", - "version": "0.24.0", + "version": "0.24.1", "description": "A postmark wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -45,7 +45,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2", "postmark": "^4.0.2" }, diff --git a/providers/push-webhook/package.json b/providers/push-webhook/package.json index 23c206cf7ee..7e408eebf1e 100644 --- a/providers/push-webhook/package.json +++ b/providers/push-webhook/package.json @@ -1,6 +1,6 @@ { "name": "@novu/push-webhook", - "version": "0.24.0", + "version": "0.24.1", "description": "A push-webhook wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -30,7 +30,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/pusher-beams/package.json b/providers/pusher-beams/package.json index a012b38e446..6cfcd1d9cc2 100644 --- a/providers/pusher-beams/package.json +++ b/providers/pusher-beams/package.json @@ -1,6 +1,6 @@ { "name": "@novu/pusher-beams", - "version": "0.24.0", + "version": "0.24.1", "description": "A pusher-beams wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.0" }, "devDependencies": { diff --git a/providers/pushpad/package.json b/providers/pushpad/package.json index 739592e6c44..d9fb0e1163f 100644 --- a/providers/pushpad/package.json +++ b/providers/pushpad/package.json @@ -1,6 +1,6 @@ { "name": "@novu/pushpad", - "version": "0.24.0", + "version": "0.24.1", "description": "A pushpad wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "pushpad": "1.0.0" }, "devDependencies": { diff --git a/providers/resend/package.json b/providers/resend/package.json index a98584db6f0..7488c247c0b 100644 --- a/providers/resend/package.json +++ b/providers/resend/package.json @@ -1,6 +1,6 @@ { "name": "@novu/resend", - "version": "0.24.0", + "version": "0.24.1", "description": "A resend wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "resend": "^2.1.0" }, "devDependencies": { diff --git a/providers/resend/src/lib/resend.provider.ts b/providers/resend/src/lib/resend.provider.ts index cf8061ab72b..597fba28220 100644 --- a/providers/resend/src/lib/resend.provider.ts +++ b/providers/resend/src/lib/resend.provider.ts @@ -44,6 +44,10 @@ export class ResendEmailProvider implements IEmailProvider { bcc: options.bcc, }); + if (response.error) { + throw new Error(response.error.message); + } + return { id: response.data?.id, date: new Date().toISOString(), diff --git a/providers/ring-central/package.json b/providers/ring-central/package.json index 81818e56f61..d1c1d1f2247 100644 --- a/providers/ring-central/package.json +++ b/providers/ring-central/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ring-central", - "version": "0.24.0", + "version": "0.24.1", "description": "A ring-central wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "@ringcentral/sdk": "^5.0.1" }, "devDependencies": { diff --git a/providers/rocket-chat/package.json b/providers/rocket-chat/package.json index a5a02e9ae52..09120302ea9 100644 --- a/providers/rocket-chat/package.json +++ b/providers/rocket-chat/package.json @@ -1,6 +1,6 @@ { "name": "@novu/rocket-chat", - "version": "0.24.0", + "version": "0.24.1", "description": "A rocket-chat wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/ryver/package.json b/providers/ryver/package.json index c75a5fe1ff9..811e3f7c7dc 100644 --- a/providers/ryver/package.json +++ b/providers/ryver/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ryver", - "version": "0.24.0", + "version": "0.24.1", "description": "A ryver wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/sendchamp/package.json b/providers/sendchamp/package.json index e631725c983..9afcc397364 100644 --- a/providers/sendchamp/package.json +++ b/providers/sendchamp/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sendchamp", - "version": "0.24.0", + "version": "0.24.1", "description": "A sendchamp wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/sendgrid/package.json b/providers/sendgrid/package.json index 69c506fbb23..1a7fbf0c4aa 100644 --- a/providers/sendgrid/package.json +++ b/providers/sendgrid/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sendgrid", - "version": "0.24.0", + "version": "0.24.1", "description": "A sendgrid wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -45,7 +45,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "@sendgrid/mail": "^8.1.0" }, "devDependencies": { diff --git a/providers/sendinblue/package.json b/providers/sendinblue/package.json index ea99eb94808..dbc2b35edee 100644 --- a/providers/sendinblue/package.json +++ b/providers/sendinblue/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sendinblue", - "version": "0.24.0", + "version": "0.24.1", "description": "A sendinblue wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/ses/package.json b/providers/ses/package.json index 01e6e661f4d..3b6e1c6220b 100644 --- a/providers/ses/package.json +++ b/providers/ses/package.json @@ -1,6 +1,6 @@ { "name": "@novu/ses", - "version": "0.24.0", + "version": "0.24.1", "description": "A ses wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -33,7 +33,7 @@ }, "dependencies": { "@aws-sdk/client-ses": "3.382.0", - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "nodemailer": "^6.6.5" }, "devDependencies": { diff --git a/providers/simpletexting/package.json b/providers/simpletexting/package.json index 595ca17b46f..5ed46b58d56 100644 --- a/providers/simpletexting/package.json +++ b/providers/simpletexting/package.json @@ -1,6 +1,6 @@ { "name": "@novu/simpletexting", - "version": "0.24.0", + "version": "0.24.1", "description": "A simpletexting wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/slack/package.json b/providers/slack/package.json index 6d82c82d8a9..9758843e149 100644 --- a/providers/slack/package.json +++ b/providers/slack/package.json @@ -1,6 +1,6 @@ { "name": "@novu/slack", - "version": "0.24.0", + "version": "0.24.1", "description": "A slack wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/sms-central/package.json b/providers/sms-central/package.json index 6856e96c8dc..e672a807584 100644 --- a/providers/sms-central/package.json +++ b/providers/sms-central/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sms-central", - "version": "0.24.0", + "version": "0.24.1", "description": "A sms-central wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -29,7 +29,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/sms77/package.json b/providers/sms77/package.json index 2cb6ea5009d..27a32e93b8e 100644 --- a/providers/sms77/package.json +++ b/providers/sms77/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sms77", - "version": "0.24.0", + "version": "0.24.1", "description": "A sms77 wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "node-fetch": "^2.6.7", "sms77-client": "^2.14.0" }, diff --git a/providers/sns/package.json b/providers/sns/package.json index 23409071195..72d06680f6c 100644 --- a/providers/sns/package.json +++ b/providers/sns/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sns", - "version": "0.24.0", + "version": "0.24.1", "description": "A sns wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -33,7 +33,7 @@ }, "dependencies": { "@aws-sdk/client-sns": "^3.382.0", - "@novu/stateless": "^0.24.0" + "@novu/stateless": "^0.24.1" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/providers/sparkpost/package.json b/providers/sparkpost/package.json index cb2181f3a70..dbfb40440af 100644 --- a/providers/sparkpost/package.json +++ b/providers/sparkpost/package.json @@ -1,6 +1,6 @@ { "name": "@novu/sparkpost", - "version": "0.24.0", + "version": "0.24.1", "description": "A sparkpost wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/providers/telnyx/package.json b/providers/telnyx/package.json index 3e307f824a8..dfc676f3771 100644 --- a/providers/telnyx/package.json +++ b/providers/telnyx/package.json @@ -1,6 +1,6 @@ { "name": "@novu/telnyx", - "version": "0.24.0", + "version": "0.24.1", "description": "A telnyx wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "telnyx": "^1.23.0" }, "devDependencies": { diff --git a/providers/termii/package.json b/providers/termii/package.json index cd7bef3f559..38a375a0691 100644 --- a/providers/termii/package.json +++ b/providers/termii/package.json @@ -1,6 +1,6 @@ { "name": "@novu/termii", - "version": "0.24.0", + "version": "0.24.1", "description": "A termii wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "node-fetch": "^3.2.10" }, "devDependencies": { diff --git a/providers/twilio/package.json b/providers/twilio/package.json index 9f2e318a390..1eae0e282d8 100644 --- a/providers/twilio/package.json +++ b/providers/twilio/package.json @@ -1,6 +1,6 @@ { "name": "@novu/twilio", - "version": "0.24.0", + "version": "0.24.1", "description": "A twilio wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -44,7 +44,7 @@ "access": "public" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "twilio": "^4.19.3" }, "devDependencies": { diff --git a/providers/zulip/package.json b/providers/zulip/package.json index c8c952a9353..d496a95c45d 100644 --- a/providers/zulip/package.json +++ b/providers/zulip/package.json @@ -1,6 +1,6 @@ { "name": "@novu/zulip", - "version": "0.24.0", + "version": "0.24.1", "description": "A zulip wrapper for novu", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -32,7 +32,7 @@ "node": ">=10" }, "dependencies": { - "@novu/stateless": "^0.24.0", + "@novu/stateless": "^0.24.1", "axios": "^1.6.2" }, "devDependencies": { diff --git a/scripts/github-conventional-comments.js b/scripts/github-conventional-comments.js new file mode 100644 index 00000000000..ce84cdc6921 --- /dev/null +++ b/scripts/github-conventional-comments.js @@ -0,0 +1,77 @@ +/* eslint-disable max-len */ + +/** + * Novu Conventional Comments as GitHub Saved Replies + * Based on https://gist.github.com/ifyoumakeit/4148a8c3e61b7868935651272c03f793 + * + * To install: + * 1. Go to https://github.com/settings/replies + * 2. Open Developer Tools + * 3. Paste and run below code in JavaScript console + */ + +(async function generateReplies(document) { + // https://conventionalcomments.org/#labels + const LABEL = { + praise: 'praise', + note: 'note', + nitpick: 'nitpick', + thought: 'thought', + suggestion: 'suggestion', + question: 'question', + chore: 'chore', + issue: 'issue', + }; + + const NON_BLOCKING = [LABEL.nitpick, LABEL.thought, LABEL.note]; + + const COMMENT = { + [LABEL.praise]: + 'Praises highlight something positive. Try to leave at least one of these comments per review. Do not leave false praise (which can actually be damaging). Do look for something to sincerely praise.', + [LABEL.note]: 'Notes are always non-blocking and simply highlight something the reader should take note of.', + [LABEL.nitpick]: 'Nitpicks are trivial preference-based requests. These should be non-blocking by nature.', + [LABEL.thought]: + 'Thoughts represent an idea that popped up from reviewing. These comments are non-blocking by nature, but they are extremely valuable and can lead to more focused initiatives and mentoring opportunities.', + [LABEL.suggestion]: + "Suggestions propose improvements to the current subject. It's important to be explicit and clear on what is being suggested and why it is an improvement. Consider using patches and the blocking or non-blocking decorations to further communicate your intent.", + [LABEL.question]: + "Questions are appropriate if you have a potential concern but are not quite sure if it's relevant or not. Asking the author for clarification or investigation can lead to a quick resolution.", + [LABEL.chore]: + 'Chores are simple tasks that must be done before the subject can be “officially” accepted. Usually, these comments reference some common process. Try to leave a link to the process description so that the reader knows how to resolve the chore.', + [LABEL.issue]: + 'Issues highlight specific problems with the subject under review. These problems can be user-facing or behind the scenes. It is strongly recommended to pair this comment with a suggestion. If you are not sure if a problem exists or not, consider leaving a question.', + }; + + const EMOJI = { + [LABEL.praise]: '\u{1F44F}', + [LABEL.note]: '\u{1F5D2}', + [LABEL.nitpick]: '\u{1F913}', + [LABEL.thought]: '\u{1F4AD}', + [LABEL.suggestion]: '\u{270F}', + [LABEL.question]: '\u{2753}', + [LABEL.chore]: '\u{2611}', + [LABEL.issue]: '\u{26A0}', + }; + + function post(key, token) { + const blockingText = NON_BLOCKING.includes(key) ? ' (non-blocking)' : ''; + + return fetch('replies', { + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + method: 'POST', + mode: 'cors', + credentials: 'include', + body: new URLSearchParams({ + body: `\n${EMOJI[key]} **${key}${blockingText}:** ‏`, + authenticity_token: token, + title: `${key[0].toUpperCase()}${key.slice(1)}${blockingText}`, + }).toString(), + }); + } + + const form = document.querySelector('.new_saved_reply'); + const token = form.querySelector('[name=authenticity_token]').value; + // Replies are order alphabetically, so order doesn't need to preserved. + await Promise.all(Object.keys(LABEL).map((key) => post(key, token))); + console.log('All added! Refresh the page!'); +})(window.document);