diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 000000000..1ce10389d --- /dev/null +++ b/.buildpacks @@ -0,0 +1 @@ +https://github.com/heroku/heroku-buildpack-nodejs \ No newline at end of file diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 517b7b2fe..000000000 --- a/.drone.yml +++ /dev/null @@ -1,114 +0,0 @@ -pipeline: - ping: - image: mongo:3.6 - commands: - - sleep 15 - - mongo --host mongo - - test: - image: erxes/runner:latest - environment: - - TEST_MONGO_URL=mongodb://mongo/test - commands: - - node -v - - npm -v - - yarn --version - - yarn install - - yarn lint - - mkdir src/private/xlsTemplateOutputs - - yarn test - - build: - image: erxes/runner:latest - commands: - - apt-get update && apt-get install -y git - - git checkout $DRONE_COMMIT_BRANCH - - yarn build - when: - branch: - - master - - develop - event: - - push - - build_tag: - image: erxes/runner:latest - commands: - - yarn build - - tar -zcf ${DRONE_REPO_NAME}_${DRONE_TAG}.tar.gz dist src .env.sample package.json yarn.lock db-migrate-store.js - when: - event: - - tag - - github_prerelease: - image: plugins/github-release - secrets: [ github_token ] - prerelease: true - files: - - ${DRONE_REPO_NAME}_${DRONE_TAG}.tar.gz - checksum: - - sha256 - when: - event: - - tag - ref: - include: - - "refs/tags/*rc*" - - "refs/tags/*alpha*" - - "refs/tags/*beta*" - - github_release: - image: plugins/github-release - secrets: [ github_token ] - files: - - ${DRONE_REPO_NAME}_${DRONE_TAG}.tar.gz - checksum: - - sha256 - when: - event: - - tag - ref: - include: - - "refs/tags/*" - exclude: - - "refs/tags/*rc*" - - "refs/tags/*alpha*" - - "refs/tags/*beta*" - - docker_publish: - image: plugins/docker - repo: ${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} - dockerfile: Dockerfile - secrets: - - source: docker_hub_username - target: docker_username - - source: docker_hub_password - target: docker_password - tags: - - ${DRONE_BRANCH} - when: - branch: - - master - - develop - event: - - push - - docker_publish_tag: - image: plugins/docker - repo: ${DRONE_REPO_OWNER}/${DRONE_REPO_NAME} - dockerfile: Dockerfile - secrets: - - source: docker_hub_username - target: docker_username - - source: docker_hub_password - target: docker_password - tags: - - ${DRONE_TAG} - when: - event: - - tag - -services: - mongo: - image: mongo:3.6 - command: [--smallfiles] diff --git a/.env.sample b/.env.sample index a6aa1fa84..16e2fcbac 100644 --- a/.env.sample +++ b/.env.sample @@ -1,66 +1,28 @@ +# general PORT=3300 -PORT_CRONS=3600 -PORT_WORKERS=3700 +NODE_ENV=development + +JWT_TOKEN_SECRET=token # MongoDB MONGO_URL=mongodb://localhost/erxes -TEST_MONGO_URL=mongodb://localhost/test # Redis REDIS_HOST=localhost REDIS_PORT=6379 REDIS_PASSWORD= -# erxes-api -HTTPS=false +# RabbitMQ +RABBITMQ_HOST=amqp://localhost +# ELASTICSEARCH +ELASTICSEARCH_URL=http://localhost:9200 + +# frontend domain MAIN_APP_DOMAIN=http://localhost:3000 -DOMAIN=http://localhost:3300 -INTEGRATIONS_API_DOMAIN=http://localhost:3400 -CRONS_API_DOMAIN=http://localhost:3600 -WORKERS_API_DOMAIN=http://localhost:3700 WIDGETS_DOMAIN=http://localhost:3200 -WIDGETS_API_DOMAIN=http://localhost:3100 -LOGS_API_DOMAIN=http://localhost:3800 - -NODE_ENV=development - -# Email -COMPANY_EMAIL_FROM=noreply@erxes.io -DEFAULT_EMAIL_SERVICE=sendgrid -MAIL_SERVICE=sendgrid -MAIL_PORT='' -MAIL_USER='' -MAIL_PASS='' -MAIL_HOST='' - -# Aws S3 -AWS_ACCESS_KEY_ID='' -AWS_SECRET_ACCESS_KEY='' -AWS_BUCKET='' -AWS_PREFIX='' - -# Aws SES -AWS_SES_ACCESS_KEY_ID='' -AWS_SES_SECRET_ACCESS_KEY='' -AWS_SES_CONFIG_SET='' -AWS_REGION='' -AWS_ENDPOINT='' - -# Google Cloud Storage -GOOGLE_CLIENT_ID='' -GOOGLE_CLIENT_SECRET='' -GOOGLE_APPLICATION_CREDENTIALS='' -GOOGLE_TOPIC='' -GOOGLE_SUBSCRIPTION_NAME='' -GOOGLE_PROJECT_ID='' -GOOGLE_CLOUD_STORAGE_BUCKET='' - -# AWS | GCS -UPLOAD_SERVICE_TYPE='AWS' - -# File system: true | false -FILE_SYSTEM_PUBLIC='true' +INTEGRATIONS_API_DOMAIN=http://localhost:3400 +DASHBOARD_DOMAIN=http://localhost:4200 +CLIENT_PORTAL_DOMAIN=http://localhost:4300 -# Pubsub REDIS | GOOGLE -PUBSUB_TYPE='REDIS' +ELK_SYNCER=false \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..1524586d5 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# workflow owner +/.github/workflows/ @Jason-2020 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index be6cd8c28..36849fc42 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,4 +1,4 @@ # These are supported funding model platforms +github: erxes open_collective: erxes -patreon: erxes diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 0babc9b68..ed70f6011 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,7 +1,7 @@ --- name: Bug report about: Create a report to help us improve -title: "[Bug]" +title: '' labels: bug assignees: '' diff --git a/.github/ISSUE_TEMPLATE/enhancement-request.md b/.github/ISSUE_TEMPLATE/enhancement-request.md index 967685452..ada4fc8d7 100644 --- a/.github/ISSUE_TEMPLATE/enhancement-request.md +++ b/.github/ISSUE_TEMPLATE/enhancement-request.md @@ -7,14 +7,11 @@ assignees: '' --- -**Is your feature request related to a problem? Please describe.** +**Is your enhancement request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - **Additional context** -Add any other context or screenshots about the feature request here. +Add any other context or screenshots about the enhancement request here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..df81be2be --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,17 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 000000000..43a754520 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,58 @@ +# Configuration for probot-stale - https://github.com/probot/stale + +# Number of days of inactivity before an Issue or Pull Request becomes stale +daysUntilStale: 30 + +# Number of days of inactivity before an Issue or Pull Request with the stale label is closed. +# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. +daysUntilClose: 5 + +# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) +onlyLabels: [] + +# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable +exemptLabels: + - pinned + - security + - "[Status] Maybe Later" + +# Set to true to ignore issues in a project (defaults to false) +exemptProjects: false + +# Set to true to ignore issues in a milestone (defaults to false) +exemptMilestones: false + +# Set to true to ignore issues with an assignee (defaults to false) +exemptAssignees: false + +# Label to use when marking as stale +staleLabel: stale + +# Comment to post when marking as stale. Set to `false` to disable +markComment: > + +# Limit the number of actions per hour, from 1-30. Default is 30 +limitPerRun: 30 + +# Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': +pulls: + markComment: > + Thank you for your contribution! However, it has + not had any activity on it in the past 30 days and will be closed in + 5 days if no updates occur. If you believe the changes are still valid then please verify your + branch has no conflicts with target and rebase if needed. If you + are awaiting a (re-)review then please let us know. + +issues: + markComment: > + Thank you for your contribution! However, it has + not had any activity on it in the past 30 days and will be closed in + 5 days if no updates occur. If you would like this issue to remain open: + + 1. Verify that you can still reproduce the issue in the latest version of erxes + 1. Comment that the issue is still reproducible and include: + * What version of erxes you reproduced the issue on + * What OS and version you reproduced the issue on + * What steps you followed to reproduce the issue +# exemptLabels: +# - confirmed diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml new file mode 100644 index 000000000..4c18974d0 --- /dev/null +++ b/.github/workflows/api.yaml @@ -0,0 +1,105 @@ +name: Api CI + +on: + push: + branches: + - "**" + paths: + - "**" + - "!.github/**" + - ".github/workflows/api.yaml" + - "!elkSyncer/**" + - "!email-verifier/**" + - "!engages-email-sender/**" + - "!logger/**" + - "!**.md" + - "!base.Dockerfile" + - "!base.Dockerfile.dockerignore" + pull_request: + branches: + - master + - develop + - crons + - workers + paths: + - "**" + - "!.github/**" + - ".github/workflows/api.yaml" + - "!elkSyncer/**" + - "!email-verifier/**" + - "!engages-email-sender/**" + - "!logger/**" + - "!**.md" + - "!base.Dockerfile" + - "!base.Dockerfile.dockerignore" + +jobs: + api: + runs-on: ubuntu-18.04 + + services: + mongodb: + image: mongo:3.6 + ports: + - 27017:27017 + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.x + uses: actions/setup-node@v1 + with: + node-version: 12.18.x + + # https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-api-${{ hashFiles('yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-api- + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: | + yarn install + + - name: Lint + run: | + yarn lint + + - name: Tsc + run: | + yarn tsc -p tsconfig.prod.json + + - name: Test + run: | + yarn test + env: + MONGO_URL: mongodb://localhost/erxes + TEST_MONGO_URL: mongodb://localhost/test + JWT_TOKEN_SECRET: token + MAIN_APP_DOMAIN: http://localhost:3000 + RABBITMQ_HOST: amqp://localhost + PORT: 3300 + + - name: Build + if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'push' + run: | + rm -rf node_modules + yarn install --production + yarn build + + - name: Build docker image + if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/crons' || github.ref == 'refs/heads/workers') + env: + BASE_IMAGE: erxes/erxes-api:base-12.18.3-slim + run: | + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + DOCKER_BUILDKIT=1 docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t erxes/erxes-api:${GITHUB_REF#refs/heads/} -f api.Dockerfile . + docker push erxes/erxes-api:${GITHUB_REF#refs/heads/} diff --git a/.github/workflows/api_base.yaml b/.github/workflows/api_base.yaml new file mode 100644 index 000000000..e135d3b78 --- /dev/null +++ b/.github/workflows/api_base.yaml @@ -0,0 +1,24 @@ +name: Api Base Image CI + +on: + push: + branches: + - develop + paths: + - '.github/workflows/api_base.yaml' + - 'base.Dockerfile' + - 'base.Dockerfile.dockerignore' + +jobs: + build_base_docker_image: + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@v2 + - name: Build base docker image + env: + BASE_IMAGE: node:12.18.3-slim + TAG_VERSION: base-12.18.3-slim + run: | + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + DOCKER_BUILDKIT=1 docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t erxes/erxes-api:$TAG_VERSION -f base.Dockerfile . + docker push erxes/erxes-api:$TAG_VERSION diff --git a/.github/workflows/elk-syncer.yaml b/.github/workflows/elk-syncer.yaml new file mode 100644 index 000000000..db2ffd656 --- /dev/null +++ b/.github/workflows/elk-syncer.yaml @@ -0,0 +1,25 @@ +name: ElkSyncer CI + +on: + push: + branches: + - master + - develop + paths: + - 'elkSyncer/**' + - '.github/workflows/elk-syncer.yaml' + +jobs: + elkSyncer: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Build docker image + if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' ) + run: | + cd elkSyncer + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-elksyncer:${GITHUB_REF#refs/heads/} -f Dockerfile . + docker push erxes/erxes-elksyncer:${GITHUB_REF#refs/heads/} diff --git a/.github/workflows/email-verifier.yaml b/.github/workflows/email-verifier.yaml new file mode 100644 index 000000000..69afa7c32 --- /dev/null +++ b/.github/workflows/email-verifier.yaml @@ -0,0 +1,54 @@ +name: Email Verifier CI + +on: + push: + branches: + - master + - develop + paths: + - "email-verifier/**" + - ".github/workflows/email-verifier.yaml" + +jobs: + email-verifier: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.x + uses: actions/setup-node@v1 + with: + node-version: 12.18.x + + # https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-email-verifier-${{ hashFiles('email-verifier/**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-email-verifier- + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: | + cd email-verifier + yarn install + + - name: Build + run: | + cd email-verifier + yarn build + + - name: Build docker image + if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' ) + run: | + cd email-verifier + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-email-verifier:${GITHUB_REF#refs/heads/} -f Dockerfile . + docker push erxes/erxes-email-verifier:${GITHUB_REF#refs/heads/} diff --git a/.github/workflows/engages.yaml b/.github/workflows/engages.yaml new file mode 100644 index 000000000..81d6ad9d1 --- /dev/null +++ b/.github/workflows/engages.yaml @@ -0,0 +1,62 @@ +name: Engages CI + +on: + push: + branches: + - "**" + paths: + - "engages-email-sender/**" + - ".github/workflows/engages.yaml" + pull_request: + branches: + - master + - develop + paths: + - "engages-email-sender/**" + - ".github/workflows/engages.yaml" + +jobs: + engages: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.x + uses: actions/setup-node@v1 + with: + node-version: 12.18.x + + # https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-engages-email-sender-${{ hashFiles('engages-email-sender/**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-engages-email-sender- + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: | + cd engages-email-sender + yarn install + + - name: Build + run: | + cd engages-email-sender + rm -rf node_modules + yarn install --production + yarn build + + - name: Build docker image + if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' ) + run: | + cd engages-email-sender + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-engages-email-sender:${GITHUB_REF#refs/heads/} -f Dockerfile . + docker push erxes/erxes-engages-email-sender:${GITHUB_REF#refs/heads/} diff --git a/.github/workflows/logger.yaml b/.github/workflows/logger.yaml new file mode 100644 index 000000000..c009a7e56 --- /dev/null +++ b/.github/workflows/logger.yaml @@ -0,0 +1,54 @@ +name: Logger CI + +on: + push: + branches: + - master + - develop + paths: + - "logger/**" + - ".github/workflows/logger.yaml" + +jobs: + logger: + runs-on: ubuntu-18.04 + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.x + uses: actions/setup-node@v1 + with: + node-version: 12.18.x + + # https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-logger-${{ hashFiles('logger/**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn-logger- + ${{ runner.os }}-yarn- + + - name: Install dependencies + run: | + cd logger + yarn install --production + + - name: Build + run: | + cd logger + yarn build + + - name: Build docker image + if: github.event_name == 'push' && ( github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' ) + run: | + cd logger + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-logger:${GITHUB_REF#refs/heads/} -f Dockerfile . + docker push erxes/erxes-logger:${GITHUB_REF#refs/heads/} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..74a49adb7 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,157 @@ +name: Publish Release + +on: + push: + tags: + - '*' + +jobs: + release: + runs-on: ubuntu-18.04 + + services: + mongodb: + image: mongo:3.6 + ports: + - 27017:27017 + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js 12.18.x + uses: actions/setup-node@v1 + with: + node-version: 12.18.x + + - name: Get release version + id: get_release_version + run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} + + # https://github.com/actions/cache/blob/master/examples.md#node---yarn + - name: Get yarn cache directory path + id: yarn-cache-dir-path + run: echo "::set-output name=dir::$(yarn cache dir)" + + - uses: actions/cache@v2 + id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) + with: + path: ${{ steps.yarn-cache-dir-path.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + ${{ runner.os }}-yarn- + + - name: Api - Install dependencies + run: | + yarn install + + - name: Api - Lint + run: | + yarn lint + + - name: Api - Tsc + run: | + yarn tsc -p tsconfig.prod.json + + - name: Api - Test + run: | + yarn test + env: + MONGO_URL: mongodb://localhost/erxes + TEST_MONGO_URL: mongodb://localhost/test + JWT_TOKEN_SECRET: token + MAIN_APP_DOMAIN: http://localhost:3000 + RABBITMQ_HOST: amqp://localhost + + - name: Api - Build + run: | + rm -rf node_modules + yarn install --production + yarn build + + - name: Api - Build docker image + env: + BASE_IMAGE: erxes/erxes-api:base-12.18.3-slim + run: | + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + DOCKER_BUILDKIT=1 docker build --build-arg BASE_IMAGE=$BASE_IMAGE -t erxes/erxes-api:${GITHUB_REF#refs/tags/} -f api.Dockerfile . + docker push erxes/erxes-api:${GITHUB_REF#refs/tags/} + + - name: elkSyncer - Build docker image + run: | + cd elkSyncer + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-elksyncer:${GITHUB_REF#refs/tags/} -f Dockerfile . + docker push erxes/erxes-elksyncer:${GITHUB_REF#refs/tags/} + + - name: Email Verifier - Install dependencies + run: | + cd email-verifier + yarn install --production + + - name: Email Verifier - Build + run: | + cd email-verifier + yarn build + + - name: Email Verifier - Build docker image + run: | + cd email-verifier + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-email-verifier:${GITHUB_REF#refs/tags/} -f Dockerfile . + docker push erxes/erxes-email-verifier:${GITHUB_REF#refs/tags/} + + - name: Engages - Install dependencies + run: | + cd engages-email-sender + yarn install --production + + - name: Engages - Build + run: | + cd engages-email-sender + yarn build + + - name: Engages - Build docker image + run: | + cd engages-email-sender + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-engages-email-sender:${GITHUB_REF#refs/tags/} -f Dockerfile . + docker push erxes/erxes-engages-email-sender:${GITHUB_REF#refs/tags/} + + - name: Logger - Install dependencies + run: | + cd logger + yarn install --production + + - name: Logger - Build + run: | + cd logger + yarn build + + - name: Logger - Build docker image + run: | + cd logger + echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin + docker build -t erxes/erxes-logger:${GITHUB_REF#refs/tags/} -f Dockerfile . + docker push erxes/erxes-logger:${GITHUB_REF#refs/tags/} + + - name: Prepare release assets + run: | + mkdir -p erxes-api + mv dist node_modules db-migrate-store.js package.json erxes-api + mkdir -p erxes-email-verifier + mv email-verifier/dist email-verifier/node_modules email-verifier/package.json erxes-email-verifier + mkdir -p erxes-engages-email-sender + mv engages-email-sender/dist engages-email-sender/node_modules engages-email-sender/package.json erxes-engages-email-sender + mkdir -p erxes-logger + mv logger/dist logger/node_modules logger/package.json erxes-logger + mv elkSyncer erxes-elkSyncer + tar -zcf erxes-api-${GITHUB_REF#refs/tags/}.tar.gz erxes-api erxes-email-verifier erxes-engages-email-sender erxes-logger erxes-elkSyncer + + - name: Upload release assets + uses: softprops/action-gh-release@v1 + with: + files: | + erxes-api-${{ steps.get_release_version.outputs.VERSION }}.tar.gz + name: Release ${{ steps.get_release_version.outputs.VERSION }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 442eacf3d..56afba277 100755 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules dist static src/private/xlsTemplateOutputs +src/private/uploads .DS_Store npm-debug.log* .env @@ -12,5 +13,7 @@ dump.rdb *.swo .migrate +oplog.timestamp + # Google config file google_cred.json \ No newline at end of file diff --git a/.snyk b/.snyk deleted file mode 100644 index b551584ec..000000000 --- a/.snyk +++ /dev/null @@ -1,178 +0,0 @@ -# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. -version: v1.13.5 -# ignores vulnerabilities until expiry date; change duration by modifying expiry date -ignore: - 'npm:chownr:20180731': - - chownr: - reason: Development only - expires: '2018-12-13T02:45:09.866Z' - - node-pre-gyp > tar > chownr: - reason: Development only - expires: '2018-12-13T02:45:09.866Z' - - bcrypt > node-pre-gyp > tar > chownr: - reason: Development only - expires: '2018-12-13T02:45:09.866Z' - SNYK-JS-AXIOS-174505: - - '@google-cloud/pubsub > google-gax > google-auto-auth > google-auth-library > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.953Z' - - '@google-cloud/pubsub > google-auto-auth > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-auto-auth > google-auth-library > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > google-auto-auth > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-auto-auth > google-auth-library > gtoken > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-auto-auth > google-auth-library > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > google-auto-auth > google-auth-library > axios': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > @google-cloud/common > google-auto-auth > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > @google-cloud/common > google-auto-auth > google-auth-library > axios': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > google-auto-auth > google-auth-library > gtoken > axios': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > @google-cloud/common > google-auto-auth > google-auth-library > gtoken > axios': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > @google-cloud/common > google-auto-auth > google-auth-library > gcp-metadata > axios': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - googleapis > google-auth-library > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > googleapis-common > google-auth-library > gcp-metadata > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > googleapis-common > google-auth-library > gtoken > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > google-auth-library > gtoken > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > googleapis-common > google-auth-library > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > google-auth-library > gcp-metadata > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - googleapis > googleapis-common > axios: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - SNYK-JS-SETVALUE-450213: - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > expand-brackets > snapdragon > base > cache-base > union-value > set-value': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > nanomatch > snapdragon > base > cache-base > union-value > set-value': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > snapdragon > base > cache-base > union-value > set-value': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > braces > snapdragon > base > cache-base > union-value > set-value': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > snapdragon > base > cache-base > union-value > set-value': - reason: None given - expires: '2019-08-08T03:54:50.954Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > expand-brackets > snapdragon > base > cache-base > set-value': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > nanomatch > snapdragon > base > cache-base > set-value': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > snapdragon > base > cache-base > set-value': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > snapdragon > base > cache-base > set-value': - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > braces > snapdragon > base > cache-base > set-value': - reason: None given - expires: '2019-08-08T03:54:50.956Z' - SNYK-JS-MIXINDEEP-450212: - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > snapdragon > base > mixin-deep': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > braces > snapdragon > base > mixin-deep': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > snapdragon > base > mixin-deep': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > nanomatch > snapdragon > base > mixin-deep': - reason: None given - expires: '2019-08-08T03:54:50.955Z' - - '@google-cloud/pubsub > google-gax > globby > fast-glob > micromatch > extglob > expand-brackets > snapdragon > base > mixin-deep': - reason: None given - expires: '2019-08-08T03:54:50.956Z' - SNYK-JS-JQUERY-174006: - - requestify > jquery: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - SNYK-JS-LODASH-450202: - - '@google-cloud/pubsub > google-gax > google-auto-auth > async > lodash': - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - xlsx-populate > lodash: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - apollo-server-express > apollo-server-core > lodash: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - '@google-cloud/storage > async > lodash': - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - mongoose > async > lodash: - reason: None given - expires: '2019-08-08T03:54:50.956Z' - - '@google-cloud/pubsub > google-auto-auth > async > lodash': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - firebase-admin > @google-cloud/storage > async > lodash: - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - apollo-server-express > apollo-server-core > apollo-engine-reporting > lodash: - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - '@google-cloud/pubsub > google-gax > lodash': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - '@google-cloud/pubsub > @google-cloud/common > split-array-stream > async > lodash': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - '@google-cloud/pubsub > @google-cloud/common > google-auto-auth > async > lodash': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - SNYK-JS-LODASHMERGE-173732: - - '@google-cloud/pubsub > lodash.merge': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - '@axelspringer/graphql-google-pubsub > @google-cloud/pubsub > lodash.merge': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - firebase-admin > @google-cloud/firestore > lodash.merge: - reason: None given - expires: '2019-08-08T03:54:50.957Z' - SNYK-JS-LODASHMERGE-173733: - - '@google-cloud/pubsub > lodash.merge': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - '@axelspringer/graphql-google-pubsub > @google-cloud/pubsub > lodash.merge': - reason: None given - expires: '2019-08-08T03:54:50.957Z' - - firebase-admin > @google-cloud/firestore > lodash.merge: - reason: None given - expires: '2019-08-08T03:54:50.957Z' -patch: {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 625955d7b..c8f2e1029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,459 @@ +* Merge branch 'develop' (46f0430f) +* make sms engage from text optional (2d909c73) + +* Merge branch 'develop' (0427f6a8) +* erxes/erxes#2359 (fdd1416b) +* update load test data (34196c3c) +* erxes/erxes#2348 (2b8d7953) +* fixed email field sort (aefb344c) +* updated mapping (9d4e6a09) +* fix(contacts): fixed contacts sort (226ceedf) +* fixed email delivery status (a98bd329) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (529322e0) +* close #2315 (7956fb8d) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (1e7d3a8c) +* added some checks in conversation cron (aafd18c2) +* show ses status inn logs (be884543) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (a4cd3ea7) +* fix delivery report #856 (819d5fb2) +* fixed robot completed steps (134a00a9) +* tsc fix (e77b06fa) +* readded engage webhook message parse (74adde70) +* readded engage webhook message parse (28c142f5) +* remove unnessacery parse from engage webhook (fe2b72ce) +* moved engage tracker to top (eab6f34d) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (2c5f5dc7) +* moved pipes to top (4ab2ed29) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (bdf1ad9c) +* remove console.log (ece04d1e) +* Update loadTestData.ts (b1198527) +* feat(test): added loadTestData command (a2e65162) +* erxes/erxes#2323 (b6150c12) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (674ef32e) +* added new company industry types (068eb5ca) +* fix sort engage (c293d9b1) +* rm unused files (cb07e8c5) +* fix sort engage #856 (1ffa4bd6) +* add search to email delivery logs #856 (1c0580f8) +* fix test coverage (50a264cf) +* fixed country code rule (d3e391c2) +* add search in transaction email (b6f5e1ef) +* fix engageTracker #856 (5a91fdb9) +* fix email delivery #856 (b952eea4) +* remove dashboard (86313122) +* fix test coverage engage #856 (8a6ca610) +* fix test #856 (7374da29) +* add log to engage tracker (649bfcb4) +* show engage title #856 (0c733c8b) +* erxes/erxes#2305 (a7ae95f2) +* erxes/erxes#2303 (26c5b45a) +* logged subscription result (fedee0ac) +* possible to export tracked data (aa5b8267) +* add perma link on facebook post (e75a3d90) +* fix export (49ed3159) +* remove un used import (8d5533e5) +* possible to import pronoun (be146de9) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (00ba4f59) +* erxes/erxes#2292 (84498cd5) +* erxes/erxes#2291 (83b99aea) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (f04ce454) +* erxes/erxes#2290 (e63ce8af) +* added engages status (fc619869) +* added engages status (b754389a) +* fix(customer): fixed customer contains search (04228616) +* fix(elasticsearch): fixed tracked segmentation (bb24830b) +* fix(activity-logs): made activity log attachments object list (f6bc60d1) +* Add company field on customer import (a6db0359) +* Add customer extend fields for export & import (805acfc0) +* remove tracked data from import export properties (16ab922c) +* ci: restructure elkSyncer release package (926ef9f7) + +* Merge branch 'develop' (67beadb3) +* fixed engages workflow (e84e0839) +* fixed engages workflow (3b1f270d) +* ci: rename packages inside release assets (9d405bf6) + + + +* added change log (cc44ab2e) +* Merge branch 'develop' (4e8ab5db) +* able to send datas to hosted elasticsearch server only for production (738c7955) +* erxes/erxes#2275 (bb17a4de) +* changed default file upload type to local (ba7565c5) +* refactor watchers checks (7f566ef1) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (42f980bd) +* refactor dependencies (43e21813) +* add await for email util (3b704994) +* fix to send admin messages by email to customer (36ad8cde) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (c496ae24) +* fixed message broker rpc queue error (70e93b96) +* erxes/erxes#2265 (454b0608) +* add await for forgotPassword mutation (2c3aa67f) +* change subscribe url (dda23daa) +* improve initial setup page (12ca92fc) +* refactor dependencies (d27a3c55) +* Product board. facebook fix (3ec74840) +* telemetry update (262e8258) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (840d2cc6) +* refactor dependencies (36fb1ef4) +* ci: fix indent (9feb5214) +* ci: combine release assets in a single archive, install production packages only, use nodejs 12.18 (2c4be4f9) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (eca717a4) +* remove some unused packages (5028ee09) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (b380c4bf) +* config of client portal (772cc73e) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (d933c197) +* check replicaset in collection watchers (59eb9ab8) +* ci(api): use nodejs 12.18.3 (6d14b0fe) +* fix test erxes/erxes#2253 (b91386fe) +* add permission type for update time track erxes/erxes#2253 (026ff2dd) +* erxes/erxes#2254 (bd74d842) +* Merge branch 'develop' of github.com:erxes/erxes-api into develop (bb14f91b) +* fix test (1b498155) +* Update FUNDING.yml (#775) (5e54c4d3) +* erxes/erxes#2250 (1713847b) +* execute some functions only for production (cd764b2f) +* feat(elasticsearch): added ability to use https://elasticsearch.erxes.io (2b6c4e18) +* erxes/erxes#2251 (b443c35c) +* Merge branch 'develop' of https://github.com/erxes/erxes-api into develop (6c152dec) +* fixed pubsub redis error (bba3bae7) +* fix(emailVerifier) verify email or phone after filling popup (215ede40) +* fix test erxes #2242 (c3498cb9) +* add test case for convert in activity log (758010e7) +* fix test erxes/erxes#2242 (f918b516) +* perf(task): show brief conversation in task log Close#2242 (18ae2592) +* conversion view on pipeline resolvers refactor (#843) (15fb3698) + +## [0.17.6](https://github.com/erxes/erxes-api/compare/0.17.5...0.17.6) (2020-08-20) + +## [0.17.5](https://github.com/erxes/erxes-api/compare/0.17.4...0.17.5) (2020-08-20) + +## [0.17.4](https://github.com/erxes/erxes-api/compare/0.17.3...0.17.4) (2020-08-20) + +## [0.17.3](https://github.com/erxes/erxes-api/compare/0.17.2...0.17.3) (2020-08-20) + + +### Performance Improvements + +* **message-broker:** added http client ([957b2d9](https://github.com/erxes/erxes-api/commit/957b2d9a578170f0f678626d7cf15d8826e24c72)) + +## [0.17.2](https://github.com/erxes/erxes-api/compare/0.17.1...0.17.2) (2020-08-20) + +## [0.17.1](https://github.com/erxes/erxes-api/compare/0.17.0...0.17.1) (2020-08-20) + +# [0.17.0](https://github.com/erxes/erxes-api/compare/0.16.2...0.17.0) (2020-08-17) + + +### Bug Fixes + +* **script-manager:** fix cors issue in script-manager url ([c18e5a9](https://github.com/erxes/erxes-api/commit/c18e5a9b1ce6d38df0ce5a0b2221aeba230013d3)), closes [erxes/erxes#2223](https://github.com/erxes/erxes/issues/2223) + + +### Features + +* **widget:** show server time & online status ([5d04366](https://github.com/erxes/erxes-api/commit/5d043662de6852761205866adbba43f2d2f5dd8c)), closes [erxes/erxes#2191](https://github.com/erxes/erxes/issues/2191) + + +### Performance Improvements + +* **dependencies:** made rabbitmq, redis optional ([1aa671f](https://github.com/erxes/erxes-api/commit/1aa671f2ba2c1c5ddca9d519bb0e911d6a41fd67)) + +## [0.16.2](https://github.com/erxes/erxes-api/compare/0.16.1...0.16.2) (2020-07-28) + +## [0.16.1](https://github.com/erxes/erxes-api/compare/0.16.0...0.16.1) (2020-07-28) + +# [0.16.0](https://github.com/erxes/erxes-api/compare/0.15.5...0.16.0) (2020-07-28) + +## [0.15.5](https://github.com/erxes/erxes-api/compare/0.15.4...0.15.5) (2020-07-15) + +## [0.15.4](https://github.com/erxes/erxes-api/compare/0.5.13...0.15.4) (2020-07-08) + +## [0.15.3](https://github.com/erxes/erxes-api/compare/0.5.13...0.15.3) (2020-07-08) + +## [0.5.13](https://github.com/erxes/erxes-api/compare/0.15.2...0.5.13) (2020-07-08) + +## [0.15.2](https://github.com/erxes/erxes-api/compare/0.15.1...0.15.2) (2020-07-07) + +## [0.15.1](https://github.com/erxes/erxes-api/compare/0.15.0...0.15.1) (2020-07-07) + +# [0.15.0](https://github.com/erxes/erxes-api/compare/0.14.3...0.15.0) (2020-07-07) + + +### Bug Fixes + +* **brands:** not using email config ([dd88de8](https://github.com/erxes/erxes-api/commit/dd88de8b37039d5f2df535f70a1a7069b9ea40fd)), closes [#789](https://github.com/erxes/erxes-api/issues/789) +* **companies:** custom properties data is being cleared when update ([3decf6f](https://github.com/erxes/erxes-api/commit/3decf6f4188398b94201f344b339e4435b4bc21b)), closes [erxes/erxes#2019](https://github.com/erxes/erxes/issues/2019) +* **customers:** clearing customFieldsData when update ([baa9d91](https://github.com/erxes/erxes-api/commit/baa9d915180051acc3a99aa95ba934998a7cacc5)), closes [#784](https://github.com/erxes/erxes-api/issues/784) +* **customers:** clearing customFieldsData when update ([d7f51c9](https://github.com/erxes/erxes-api/commit/d7f51c9062342862c50d353f816b64880e6aec18)), closes [erxes/erxes#784](https://github.com/erxes/erxes/issues/784) +* **email-verifier:** add request dependency ([ddc2ef3](https://github.com/erxes/erxes-api/commit/ddc2ef3713a7282f8765766c5307186b0467ce90)) +* **field:** validate empty string in required field ([f3474d8](https://github.com/erxes/erxes-api/commit/f3474d810dbebe204890942188bbee7222c0f9f0)), closes [erxes/erxes#2041](https://github.com/erxes/erxes/issues/2041) +* **import:** Not filling the names field on the company with the primaryName in import ([43ae29e](https://github.com/erxes/erxes-api/commit/43ae29ec29fc21b9e03c39693b08ab9a32478309)), closes [#788](https://github.com/erxes/erxes-api/issues/788) +* **segments:** fixed customer custom fields segment form ([f88c4dd](https://github.com/erxes/erxes-api/commit/f88c4dd3a75ba74ba936ed53aa1aafeb3c6560bf)) +* **users:** forgot password link bug ([86e7256](https://github.com/erxes/erxes-api/commit/86e72563b07a0133589dcd09f57a31fc5a023b6a)), closes [#786](https://github.com/erxes/erxes-api/issues/786) + + +### Features + +* **import:** added companiesPrimaryNames column in customer import excel ([a452a5c](https://github.com/erxes/erxes-api/commit/a452a5cbd2c4d4c073dc846f5ba653ba6b093fd9)), closes [#787](https://github.com/erxes/erxes-api/issues/787) + + +### Performance Improvements + +* **ci:** upload compiled version to github release assets ([add7d68](https://github.com/erxes/erxes-api/commit/add7d68e92f0bb01212a6e35fe5d5259a517dea6)) +* **engages:** used streams ([606d072](https://github.com/erxes/erxes-api/commit/606d0728a7fb5f844585adcdfa1cbe6f9173cc36)), closes [#801](https://github.com/erxes/erxes-api/issues/801) +* **healthcheck:** add mongodb redis and rabbitmq healthcheck ([30da525](https://github.com/erxes/erxes-api/commit/30da525040b7e91f0f2280daa2d1db727a05280f)), closes [erxes/erxes#2073](https://github.com/erxes/erxes/issues/2073) +* **import:** stream excel file import ([2cf2f53](https://github.com/erxes/erxes-api/commit/2cf2f53859cecd2aecef4d96a8ffa6dbc24e600f)), closes [erxes/erxes#2075](https://github.com/erxes/erxes/issues/2075) +* **integrations:** add brand restriction option to integrations ([ff534a0](https://github.com/erxes/erxes-api/commit/ff534a02bf426fba33a53d324c0c44b3104ced24)), closes [erxes/erxes#2050](https://github.com/erxes/erxes/issues/2050) +* **integrations:** add brand restriction option to integrations ([b79d178](https://github.com/erxes/erxes-api/commit/b79d17832866435abfed9ae386a17a9d7c130f0a)), closes [#2050](https://github.com/erxes/erxes-api/issues/2050) +* **phoneValidation:** add phone validation to customer schema ([b61df74](https://github.com/erxes/erxes-api/commit/b61df74080aa5935e730b1fe044c67560461d729)), closes [#796](https://github.com/erxes/erxes-api/issues/796) + +## [0.14.3](https://github.com/erxes/erxes-api/compare/0.14.2...0.14.3) (2020-06-17) + + +### Bug Fixes + +* **initial-script:** wrapped connection string in double quote ([8fe761e](https://github.com/erxes/erxes-api/commit/8fe761e5d94db58123f7425d04fadd77564d04f6)) + +## [0.14.2](https://github.com/erxes/erxes-api/compare/0.14.1...0.14.2) (2020-06-17) + + +### Bug Fixes + +* **commands:** passed uri option to loadInitialData command ([d2296e0](https://github.com/erxes/erxes-api/commit/d2296e02b54b88a92c04d87d28fc41c6bcefd31c)) + +## [0.14.1](https://github.com/erxes/erxes-api/compare/0.14.0...0.14.1) (2020-05-19) + + +### Bug Fixes + +* **emails:** sending empty auth info ([a0fcc5f](https://github.com/erxes/erxes-api/commit/a0fcc5fd1dd967af562ba8d5f606178e635041cc)), closes [#777](https://github.com/erxes/erxes-api/issues/777) +* **emailTemplate:** revert change in email template query ([f19b35e](https://github.com/erxes/erxes-api/commit/f19b35e1e4960a91096de342151a40081e4fa618)) +* **engages:** added unverifed emails limit config ([5312fae](https://github.com/erxes/erxes-api/commit/5312fae15cd2f3febbfbeaf00538f0c612022a9c)), closes [erxes/erxes#1931](https://github.com/erxes/erxes/issues/1931) +* **test:** change random string to enum in factory ([535c803](https://github.com/erxes/erxes-api/commit/535c8036cdec9fbac8f9e828f0be6f30512879d4)) + + +### Performance Improvements + +* **customer:** added uriVisits field on schema ([c1e39ce](https://github.com/erxes/erxes-api/commit/c1e39ce7d257f6da351b0fc97001cb19288c41db)), closes [#776](https://github.com/erxes/erxes-api/issues/776) +* **customers:** flatten customFieldsData, trackedData fields ([934970b](https://github.com/erxes/erxes-api/commit/934970b6160f1a849ead3b23923c6b3ff2871462)), closes [#774](https://github.com/erxes/erxes-api/issues/774) +* **docker:** upgrade dockerfile nodejs version ([5e7ea88](https://github.com/erxes/erxes-api/commit/5e7ea88b66eec6b9f936ec61ff01f5b1796b0d2d)), closes [erxes/erxes#1993](https://github.com/erxes/erxes/issues/1993) +* **node:** update package.json for node v12 ([15e498a](https://github.com/erxes/erxes-api/commit/15e498ab0efbb1704100ba227d4007f51fd5cbb4)) +* **schema:** add select options to enum fields ([4af015b](https://github.com/erxes/erxes-api/commit/4af015b513d5ee349b167928678758043d71b4fc)) +* **schema:** add select options to some field in customer erxes/erxes[#1959](https://github.com/erxes/erxes-api/issues/1959) ([2720987](https://github.com/erxes/erxes-api/commit/2720987c028ded43bd541ac0e221e72434fa6b7a)) +* **stage:** count archived cards ([3700afa](https://github.com/erxes/erxes-api/commit/3700afaa20912286e12d442ebb0047d398abb081)), closes [#778](https://github.com/erxes/erxes-api/issues/778) + +# [0.14.0](https://github.com/erxes/erxes-api/compare/0.13.0...0.14.0) (2020-04-25) + + +### Bug Fixes + +* add nylas-exchange type in mail integrations ([4863734](https://github.com/erxes/erxes-api/commit/4863734b5d8e8c17bb5706f3a4fec625b0de3781)) +* add userId in upload-file and mail-attachment ([7e86c2d](https://github.com/erxes/erxes-api/commit/7e86c2dc5bd2e934097123ec89da07086539d2ba)) +* send userId for middleware in integration ([4be0628](https://github.com/erxes/erxes-api/commit/4be062874665bb4cad0932666dfd9c147abfbfcb)), closes [#751](https://github.com/erxes/erxes-api/issues/751) +* **popups:** resetting stats when update ([0d71b70](https://github.com/erxes/erxes-api/commit/0d71b704c763e8cfd3309e16b6b1148b518010ab)), closes [#763](https://github.com/erxes/erxes-api/issues/763) + + +### Features + +* add nylas exchange provider ([6394f3e](https://github.com/erxes/erxes-api/commit/6394f3ee2c54df47d167ae675b1871bc7659441c)) +* **messenger:** tracking all possible customer fields ([1b82095](https://github.com/erxes/erxes-api/commit/1b820952cd938615eb4de4ebf5b8c418a501d179)), closes [#764](https://github.com/erxes/erxes-api/issues/764) + + +### Performance Improvements + +* **engage:** refactor cronjobs ([176e3ca](https://github.com/erxes/erxes-api/commit/176e3ca305482f21d521dde71399968d8129d0fe)), closes [erxes/erxes#1940](https://github.com/erxes/erxes/issues/1940) +* **env:** remove DOMAIN variable ([7da2d8c](https://github.com/erxes/erxes-api/commit/7da2d8cc92d7d70aeca9234f168ae1c3803dca49)), closes [#747](https://github.com/erxes/erxes-api/issues/747) +* **env:** using rabbitmq instead of WORKERS_API_DOMAIN ([5ee39d8](https://github.com/erxes/erxes-api/commit/5ee39d8001291e2b4c39d23700936aedff3813e2)), closes [#767](https://github.com/erxes/erxes-api/issues/767) + + +### BREAKING CHANGES + +* **env:** rabbitmq env is required in workers service. + +# [1.0.0](https://github.com/erxes/erxes-api/compare/0.13.0...1.0.0) (2020-04-25) + + +### Bug Fixes + +* add nylas-exchange type in mail integrations ([4863734](https://github.com/erxes/erxes-api/commit/4863734b5d8e8c17bb5706f3a4fec625b0de3781)) +* add userId in upload-file and mail-attachment ([7e86c2d](https://github.com/erxes/erxes-api/commit/7e86c2dc5bd2e934097123ec89da07086539d2ba)) +* send userId for middleware in integration ([4be0628](https://github.com/erxes/erxes-api/commit/4be062874665bb4cad0932666dfd9c147abfbfcb)), closes [#751](https://github.com/erxes/erxes-api/issues/751) +* **popups:** resetting stats when update ([0d71b70](https://github.com/erxes/erxes-api/commit/0d71b704c763e8cfd3309e16b6b1148b518010ab)), closes [#763](https://github.com/erxes/erxes-api/issues/763) + + +### Features + +* add nylas exchange provider ([6394f3e](https://github.com/erxes/erxes-api/commit/6394f3ee2c54df47d167ae675b1871bc7659441c)) +* **messenger:** tracking all possible customer fields ([1b82095](https://github.com/erxes/erxes-api/commit/1b820952cd938615eb4de4ebf5b8c418a501d179)), closes [#764](https://github.com/erxes/erxes-api/issues/764) + + +### Performance Improvements + +* **engage:** refactor cronjobs ([176e3ca](https://github.com/erxes/erxes-api/commit/176e3ca305482f21d521dde71399968d8129d0fe)), closes [erxes/erxes#1940](https://github.com/erxes/erxes/issues/1940) +* **env:** remove DOMAIN variable ([7da2d8c](https://github.com/erxes/erxes-api/commit/7da2d8cc92d7d70aeca9234f168ae1c3803dca49)), closes [#747](https://github.com/erxes/erxes-api/issues/747) +* **env:** using rabbitmq instead of WORKERS_API_DOMAIN ([5ee39d8](https://github.com/erxes/erxes-api/commit/5ee39d8001291e2b4c39d23700936aedff3813e2)), closes [#767](https://github.com/erxes/erxes-api/issues/767) + + +### BREAKING CHANGES + +* **env:** rabbitmq env is required in workers service. + +# [0.13.0](https://github.com/erxes/erxes-api/compare/0.12.5...0.13.0) (2020-03-17) + + +### Bug Fixes + +* **activity-log:** checked empty content ([ce3daea](https://github.com/erxes/erxes-api/commit/ce3daeac23631dceb745ec4be20127b46097f7c9)) +* **conversation:** counting left, joined messages in messsageCount field ([9f8201d](https://github.com/erxes/erxes-api/commit/9f8201d9e4e57e07a5b07ea030e929139bd76ddd)), closes [#694](https://github.com/erxes/erxes-api/issues/694) +* **importHistory:** cannot remove contacts if there are too many contacts ([be6ee64](https://github.com/erxes/erxes-api/commit/be6ee64d0602b230b0230de12ce3686a287729fd)), closes [erxes/erxes#1681](https://github.com/erxes/erxes/issues/1681) +* remove account only when there is no integration ([1e33a60](https://github.com/erxes/erxes-api/commit/1e33a60256d2d2ee10534803e57dea7172383281)) + + +### Features + +* **board:** add archive functionality ([49e09f7](https://github.com/erxes/erxes-api/commit/49e09f7eb23dde1c5c0211b9b20240d52feecca3)), closes [erxes/erxes#1625](https://github.com/erxes/erxes/issues/1625) +* **email-verification:** added email verification service ([d509e99](https://github.com/erxes/erxes-api/commit/d509e999f04b961ef38e0eb9c98cf64cdf656d3d)), closes [#1662](https://github.com/erxes/erxes-api/issues/1662) +* **users:** filter by brand ([9dca98e](https://github.com/erxes/erxes-api/commit/9dca98e9e4ff369fea01c2a3272b534a077396d3)), closes [#681](https://github.com/erxes/erxes-api/issues/681) +* **videoCall:** add video call integration using daily.co ([bb25bf9](https://github.com/erxes/erxes-api/commit/bb25bf96e70df43b0ee572f4b1e9eef6d490980d)), closes [erxes/erxes#1638](https://github.com/erxes/erxes/issues/1638) + + +### Performance Improvements + +* **customer:** export pop-ups data for customer list when filtering by pop ups ([9fb2574](https://github.com/erxes/erxes-api/commit/9fb25749c0c75b35b5968aa29acbd05bc084a787)), closes [erxes/erxes#1674](https://github.com/erxes/erxes/issues/1674) +* **merge-repos:** merged logger & engage-mail-sender repos ([6d2d8dd](https://github.com/erxes/erxes-api/commit/6d2d8dd85510f475d13ad49ea986fa4d9d89f276)), closes [#736](https://github.com/erxes/erxes-api/issues/736) + +## [0.12.5](https://github.com/erxes/erxes-api/compare/0.12.4...0.12.5) (2020-03-06) + + +### Bug Fixes + +* **account:** remove account ([#725](https://github.com/erxes/erxes-api/issues/725)) ([fb477c3](https://github.com/erxes/erxes-api/commit/fb477c33277f74fe09e28d786e310bc72168ab80)) + +## [0.12.4](https://github.com/erxes/erxes-api/compare/0.12.3...0.12.4) (2020-03-06) + +## [0.12.3](https://github.com/erxes/erxes-api/compare/0.12.2...0.12.3) (2020-01-28) + +## [0.12.2](https://github.com/erxes/erxes-api/compare/0.12.1...0.12.2) (2020-01-28) + + +### Bug Fixes + +* **jwt:** checked JWT_TOKEN_SECRET config on startup ([30daf11](https://github.com/erxes/erxes-api/commit/30daf11)) + +## [0.12.1](https://github.com/erxes/erxes-api/compare/0.12.0...0.12.1) (2020-01-28) + + +### Bug Fixes + +* **user:** jwt token secret issue ([76f2474](https://github.com/erxes/erxes-api/commit/76f2474)), closes [#704](https://github.com/erxes/erxes-api/issues/704) + +# [0.12.0](https://github.com/erxes/erxes-api/compare/0.11.2...0.12.0) (2020-01-08) + + +### Bug Fixes + +* **deal/ticket/task:** move card labels bug ([35136a1](https://github.com/erxes/erxes-api/commit/35136a1)), closes [#676](https://github.com/erxes/erxes-api/issues/676) +* **mail:** not receiving updates in realtime ([2e4a382](https://github.com/erxes/erxes-api/commit/2e4a382)), closes [#683](https://github.com/erxes/erxes-api/issues/683) + + +### Performance Improvements + +* **integrations:** using rabbitmq for erxes-integrations communication ([6fd595e](https://github.com/erxes/erxes-api/commit/6fd595e)), closes [#690](https://github.com/erxes/erxes-api/issues/690) +* **widgets:** merge erxes-widgets-api with erxes-api ([71e89ba](https://github.com/erxes/erxes-api/commit/71e89ba)), closes [erxes/erxes#1542](https://github.com/erxes/erxes/issues/1542) +* **widgets:** merge erxes-widgets-api with erxes-api ([81e3880](https://github.com/erxes/erxes-api/commit/81e3880)), closes [#1542](https://github.com/erxes/erxes-api/issues/1542) + + +### BREAKING CHANGES + +* **widgets:** erxes-widgets-api is making code duplication difficult to maintain and we decided that it is an unnecessary abstraction. +1. Remove MAIN_API_URL env variable +2. Point API_GRAPHQL_URL env variable to http://localhost:3300/graphql AKA erxes-api +* **widgets:** erxes-widgets-api is making code duplication difficult to maintain and we decided that it is an unnecessary abstraction. +1. Remove MAIN_API_URL env variable +2. Point API_GRAPHQL_URL env variable to http://localhost:3300/graphql AKA erxes-api + +## [0.11.2](https://github.com/erxes/erxes-api/compare/0.11.1...0.11.2) (2019-12-15) + + +### Bug Fixes + +* **conversation:** cleaning content ([710d6e7](https://github.com/erxes/erxes-api/commit/710d6e7)), closes [#641](https://github.com/erxes/erxes-api/issues/641) +* **conversation:** read conversation becoming unread sometimes ([a6b1254](https://github.com/erxes/erxes-api/commit/a6b1254)), closes [#664](https://github.com/erxes/erxes-api/issues/664) +* **deal/ticket/task:** copy customer, companies when copy ([50969f0](https://github.com/erxes/erxes-api/commit/50969f0)), closes [#626](https://github.com/erxes/erxes-api/issues/626) +* **integration:** remove related integration when account remove ([72c9cba](https://github.com/erxes/erxes-api/commit/72c9cba)), closes [#617](https://github.com/erxes/erxes-api/issues/617) +* **user:** invalid regex in login ([6cc2dc0](https://github.com/erxes/erxes-api/commit/6cc2dc0)), closes [#634](https://github.com/erxes/erxes-api/issues/634) + + +### Features + +* **activity-log:** reimplement activity log ([dd68af5](https://github.com/erxes/erxes-api/commit/dd68af5)), closes [#665](https://github.com/erxes/erxes-api/issues/665) +* **customer/company:** merge customFieldsData ([b4f3e46](https://github.com/erxes/erxes-api/commit/b4f3e46)) +* **customers:** added code field ([7a696d4](https://github.com/erxes/erxes-api/commit/7a696d4)), closes [#631](https://github.com/erxes/erxes-api/issues/631) +* **exports:** add some exporters ([bfd892f](https://github.com/erxes/erxes-api/commit/bfd892f)), closes [#662](https://github.com/erxes/erxes-api/issues/662) +* **integration:** archive ([0f0ae7a](https://github.com/erxes/erxes-api/commit/0f0ae7a)), closes [#624](https://github.com/erxes/erxes-api/issues/624) +* **ticket/task/deal:** add no labels assigned filter ([38e3373](https://github.com/erxes/erxes-api/commit/38e3373)), closes [erxes/erxes#1387](https://github.com/erxes/erxes/issues/1387) +* **user:** now users can unsubscribe from notification emails ([c9896ad](https://github.com/erxes/erxes-api/commit/c9896ad)), closes [#655](https://github.com/erxes/erxes-api/issues/655) [#654](https://github.com/erxes/erxes-api/issues/654) + + + +## [0.11.1](https://github.com/erxes/erxes-api/compare/0.11.0...0.11.1) (2019-11-01) + +# [0.11.0](https://github.com/erxes/erxes-api/compare/0.10.1...0.11.0) (2019-11-01) + + +### Bug Fixes + +* **deal:** fixed search ([139ea97](https://github.com/erxes/erxes-api/commit/139ea97)), closes [#1251](https://github.com/erxes/erxes-api/issues/1251) +* **deal/task/ticket/growthHack:** fix next day filter ([1bfcbdc](https://github.com/erxes/erxes-api/commit/1bfcbdc)), closes [#567](https://github.com/erxes/erxes-api/issues/567) +* **inbox:** Updating conversation last content when internal note ([fa421bb](https://github.com/erxes/erxes-api/commit/fa421bb)), closes [#568](https://github.com/erxes/erxes-api/issues/568) +* **permission:** add permission cache empty check ([47bb198](https://github.com/erxes/erxes-api/commit/47bb198)), closes [erxes/erxes#1231](https://github.com/erxes/erxes/issues/1231) + +## [0.10.1](https://github.com/erxes/erxes-api/compare/0.10.0...0.10.1) (2019-08-31) + + +### Bug Fixes + +* **board:** show a warning message if stage has a item && remove unused functions ([1ed1727](https://github.com/erxes/erxes-api/commit/1ed1727)), closes [erxes/erxes#1205](https://github.com/erxes/erxes/issues/1205) +* **deepcode:** apply deepcode fixes ([5ade279](https://github.com/erxes/erxes-api/commit/5ade279)), closes [#521](https://github.com/erxes/erxes-api/issues/521) +* **engage:** auto message ([8904a30](https://github.com/erxes/erxes-api/commit/8904a30)), closes [#1197](https://github.com/erxes/erxes-api/issues/1197) +* **notification:** sending notification to current user ([86db029](https://github.com/erxes/erxes-api/commit/86db029)), closes [#1198](https://github.com/erxes/erxes-api/issues/1198) +* **permission:** add permission cache empty check ([cb74519](https://github.com/erxes/erxes-api/commit/cb74519)), closes [#1231](https://github.com/erxes/erxes-api/issues/1231) +* **teambers:** not searchable by username ([69ee5e9](https://github.com/erxes/erxes-api/commit/69ee5e9)), closes [erxes/erxes#1213](https://github.com/erxes/erxes/issues/1213) + + +### Features + +* **notification:** show stage names on notifications ([ed239cc](https://github.com/erxes/erxes-api/commit/ed239cc)), closes [#1124](https://github.com/erxes/erxes-api/issues/1124) + +# [0.10.0](https://github.com/erxes/erxes-api/compare/0.9.17...0.10.0) (2019-08-15) + + +### Bug Fixes + +* **customer/company:** creating extra log when merge ([b59575f](https://github.com/erxes/erxes-api/commit/b59575f)), closes [#520](https://github.com/erxes/erxes-api/issues/520) +* **emailTemplate:** Fix notification template link ([#516](https://github.com/erxes/erxes-api/issues/516)) ([46eee51](https://github.com/erxes/erxes-api/commit/46eee51)) +* **permission:** bug in permission cache in userEdit ([589cee2](https://github.com/erxes/erxes-api/commit/589cee2)), closes [#1139](https://github.com/erxes/erxes-api/issues/1139) +* **usersQuery:** invalid users query filter ([8e500f1](https://github.com/erxes/erxes-api/commit/8e500f1)), closes [erxes/erxes#1118](https://github.com/erxes/erxes/issues/1118) [erxes/erxes#1077](https://github.com/erxes/erxes/issues/1077) + + +### Features + +* **command:** add loadPermission command ([d2e8ffc](https://github.com/erxes/erxes-api/commit/d2e8ffc)), closes [#505](https://github.com/erxes/erxes-api/issues/505) +* **message-queue:** used rabbitmq ([fdb26ab](https://github.com/erxes/erxes-api/commit/fdb26ab)), closes [#223](https://github.com/erxes/erxes-api/issues/223) +* **permission:** restrict user permissions by brand ([03f785f](https://github.com/erxes/erxes-api/commit/03f785f)), closes [#517](https://github.com/erxes/erxes-api/issues/517) + + +### Performance Improvements + +* **deal/ticket/task:** add attachment field ([df1420e](https://github.com/erxes/erxes-api/commit/df1420e)), closes [erxes/erxes#1029](https://github.com/erxes/erxes/issues/1029) +* **engage:** extract engage email sender logic ([b6f10b3](https://github.com/erxes/erxes-api/commit/b6f10b3)), closes [#510](https://github.com/erxes/erxes-api/issues/510) + + +### BREAKING CHANGES + +* **engage:** https://github.com/erxes/erxes-engages-email-sender is required in order to run engage properly +* **message-queue:** No longer using redis as message broker + ## [0.9.17](https://github.com/erxes/erxes-api/compare/0.9.16...0.9.17) (2019-07-09) diff --git a/Dockerfile.dev b/Dockerfile.dev index bf2ba66e3..16e23c8cf 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -3,3 +3,5 @@ WORKDIR /erxes-api COPY yarn.lock package.json ./ RUN yarn install CMD ["yarn", "dev"] + + diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 000000000..93067229f --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,691 @@ +“Commons Clause” License Condition v1.0 + +The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. + +Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. + +For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. + +Software: erxes + +License: GNU General Public License v3.0 + +Licensor: erxes Inc + + +--------------------------------------------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..b9fef054d --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +cron: node dist/cronJobs +worker: node dist/workers \ No newline at end of file diff --git a/README.md b/README.md index aa1c284b7..275f7bd59 100644 --- a/README.md +++ b/README.md @@ -1,69 +1,77 @@ -# erxes Inc - erxes API +## Deprecated: erxes-api code repository is now merged into [erxes/erxes](https://github.com/erxes/erxes/tree/develop/api) repository. -erxes is an open source growth marketing platform. Marketing, sales, and customer service platform designed to help your business attract more engaged customers. Replace Hubspot with the mission and community-driven ecosystem. -View demo | Download ZIP | Join us on Slack +# erxes -## Status
+erxes is a free and open fair-code licensed all-in-one growth marketing & management software. We offer an all-in-one solution for sales, marketing, and customer service teams, with a focus on the entire customer experience. Replace Hubspot with the mission and community-driven ecosystem. +Live demo | Join our community + +![Docker Pulls](https://img.shields.io/docker/pulls/erxes/erxes-api) ![Build Status](https://drone.erxes.io/api/badges/erxes/erxes-api/status.svg?branch=master) [![Coverage Status](https://coveralls.io/repos/github/erxes/erxes-api/badge.svg?branch=master)](https://coveralls.io/github/erxes/erxes-widgets-api?branch=master) [![Known Vulnerabilities](https://snyk.io/test/github/erxes/erxes-api/badge.svg)](https://snyk.io/test/github/erxes/erxes-api) -## Running the server +Open Source Growth Marketing Platform + +## Features + + + +erxes helps you attract and engage more customers while giving you high lead conversion. With erxes, all your marketing, sales and customer service tools are merged into one platform for greater output. -#### 1. Node (version >= 4) and NPM need to be installed. +* **Growth Hacking:** Managing your entire growth operation made easy. From ideas to actual performance, making sure everything recorded, prioritized and centralized in the single platform to get tested with pool of analysis and learnings, which made the growing as pleasure. +* **Email & SMS Marketing:** Reach your customer with personalized messaging. Keeping your customers hooked is definitely a challenge. Start converting your prospects into potential customers through email, SMS, Live chat, and In-app-messaging or more interactions to drive them to a successful close. You can connect to your customers in a whole new way with Erxes! +* **Pop-ups & Forms:** Create Stylish Pop-ups and Forms that Bring Leads. Turn regular visitors into qualified leads by capturing them with a customizable pop-ups, forms, and embedded placements. Erxes helps you to create stylish and contextual pop-ups, banners and bars fit all your marketing needs. +* **Sales Pipeline:** Track your entire sales pipeline from one dashboard. All your customer information and sales process in one board to follow up flawlessly. Have your sales managers to know everything needed to deliver increased levels of personalization before they contact customers. +* **Contact Management:** Manage Visitors, Customers, and Companies. Access our all-in-one CRM system in one go so that it’s easier to coordinate and manage your contacts and interactions with your customers. Erxes Contacts provides whole segmentation tools for you to work more effiecently. +* **Lead Scoring:** Identify and Target Sales-Ready Leads. +* **Shared Team Inbox:** Communicate faster and easier with your customers via one truly omnichannel platform. Combine real-time client and team communication with in-app messaging, live chat, email and form, so your customers can reach you however and wherever they want. +* **Messenger:** Talk to Your Customers in Continuous Omnichannel Conversations. Enable businesses to capture every single customer feedback and communicate in real time. You can educate your customers through knowledge-base from the erxes Messenger. +* **Knowledge base:** Create Help Articles for Customer Self-service. Educate both your customers and staff by creating a help center related to your brands, products and services to reach higher level of satisfactions. +* **Task Management:** Work More Collaboratively and Get More Done. Save time, manage your projects, monitor your team and increase your productivity in just a few clicks. Erxes helps to turn chaos into clarity and make everything perfect. +## Documentation + * Install erxes
+ * erxes documentation
+ * Contributing to erxes
-Make sure your MongoDB and Redis server is running. +## Deployment -#### 2. Clone and install dependencies. +### Ubuntu 16.04/18.04 LTS +Follow these deployment instructions. -```Shell -git clone https://github.com/erxes/erxes-api.git -cd erxes-api -yarn install -``` +[![ubuntu](https://erxes-os.s3-us-west-2.amazonaws.com/github/ubuntu-logo.png)](https://docs.erxes.io/installation/ubuntu) -#### 3. Create configuration from sample file. We use [dotenv](https://github.com/motdotla/dotenv) for this. +### Debian 10 +Follow these deployment instructions. -```Shell -cp .env.sample .env -``` +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/debian-logo.png)](https://docs.erxes.io/installation/debian10) -.env file description +### CentOS 8 +Follow these deployment instructions. -```.env -NODE_ENV=development (Node environment: development | production) -PORT=3300 (Server port) +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/centos-logo.png)](https://docs.erxes.io/installation/centos8) -MONGO_URL=mongodb://localhost/erxes (MongoDB url) -TEST_MONGO_URL=mongodb://localhost/test +### Docker +Follow these deployment instructions. -REDIS_HOST=localhost (Redis server url) -REDIS_PORT=6379 (Redis server port) +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/docker-logo.png)](https://docs.erxes.io/installation/docker) -MAIN_APP_DOMAIN=http://localhost:3000 (erxes project url) -DOMAIN='http://localhost:3300' (erxes-api project url) -``` +### Heroku +Host your own erxes server with One-Click Deploy. -#### 4. Start the server. +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/heroku.png)](https://heroku.com/deploy?template=https://github.com/erxes/erxes/tree/develop) -For development: +### AWS Marketplace +Launch an EC2 instance using erxes in the AWS Marketplace. -```Shell -yarn dev -``` +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/aws-logo.png)](https://aws.amazon.com/marketplace/pp/B086MZ9FVS/) -For production: +### DigitalOcean Droplet +Deploy to a DigitalOcean droplet with our one-click install listing from the DigitalOcean Marketplace. -```Shell -yarn build -yarn start -``` +[![debian](https://erxes-os.s3-us-west-2.amazonaws.com/github/droplet.png)](https://marketplace.digitalocean.com/apps/erxes) -#### 5. Running servers -- GraphQL server: [http://localhost:3300/graphql](http://localhost:3300/graphql) -- Websocket subscriptions server: [ws://localhost:3300/subscriptions](ws://localhost:3300/subscriptions) ## Contributors @@ -77,7 +85,6 @@ Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com - ## Sponsors Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/erxes#sponsor)] @@ -87,11 +94,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l - - - ## In-kind sponsors @@ -101,5 +105,12 @@ Support this project by becoming a sponsor. Your logo will show up here with a l       -## Copyright & License -Copyright (c) 2018 erxes Inc - Released under the [MIT license.](https://github.com/erxes/erxes/blob/develop/LICENSE.md) +## Which license does erxes use? +erxes is a free and open fair-code licensed under GNU General Public License v3.0 with Commons Clause. + +## Is erxes open-source? +No. The Commons Clause that is attached to the GNU General Public License v3.0 license takes away some rights. Hence, according to the definition of the Open Source Initiative (OSI), erxes is not open-source. Nonetheless, the source code is open and everyone (individuals and companies) can use it for free. However, it is not allowed to make money directly with erxes. +For instance, one cannot charge others to host or support erxes. However, to make things simpler, we grant everyone (individuals and companies) the right to offer consulting or support without prior permission as long as it is less than 30,000 USD ($30k) per annum. If your revenue from services based on erxes is greater than $30k per annum, we'd invite you to become a partner agency and apply for a license. If you have any questions about this, feel free to reach out to us at info@erxes.io + +## Why is erxes not open-source but fair-code licensed instead? +We love open-source and the idea that everybody can freely use and extend what we wrote. Our community is at the heart of everything that we do and we understand that people who contribute to a project are the main drivers that push a project forward. So to make sure that the project continues to evolve and stay alive in the longer run, we decided to attach the Commons Clause. This ensures that no other person or company can make money directly with erxes. Especially if it competes with how we plan to finance our further development. For the greater majority of the people, it will not make any difference at all. At the same time, it protects the project. As erxes itself depends on and uses a lot of other open-source projects, it is only fair that we support them back. That is why we have planned to contribute a certain percentage of revenue/profit every month to these projects. diff --git a/Dockerfile b/api.Dockerfile similarity index 84% rename from Dockerfile rename to api.Dockerfile index 15bc787f8..49467c98f 100644 --- a/Dockerfile +++ b/api.Dockerfile @@ -1,4 +1,5 @@ -FROM erxes/runner:latest +ARG BASE_IMAGE +FROM $BASE_IMAGE WORKDIR /erxes-api/ RUN chown -R node:node /erxes-api COPY --chown=node:node . /erxes-api diff --git a/api.Dockerfile.dockerignore b/api.Dockerfile.dockerignore new file mode 100644 index 000000000..725160722 --- /dev/null +++ b/api.Dockerfile.dockerignore @@ -0,0 +1,17 @@ +*.md +*.yml +*.yaml +.env* +.git* +.prettierrc +.snyk +Dockerfile* +google_cred.json.sample +scripts +*.tar.gz +automations +elkSyncer +email-verifier +logger +engages-email-sender +dashboard diff --git a/app.json b/app.json new file mode 100644 index 000000000..391328bd4 --- /dev/null +++ b/app.json @@ -0,0 +1,92 @@ +{ + "name": "Erxes API on Heroku", + "description": "GraphQL API for erxes main project", + "keywords": [ + "Marketing", + "sales", + "customer engagement", + "customer support", + "CRM", + "node", + "express", + "graphql", + "apollo" + ], + "website": "https://erxes.io", + "repository": "https://github.com/batnasan/erxes-api", + "logo": "https://raw.githubusercontent.com/erxes/erxes/master/ui/public/images/logo-dark.png", + "success_url": "/", + "env": { + "PORT": { + "description": "A port number that erxes api will be running on", + "value": "3300" + }, + "PORT_CRONS": { + "description": "A port number that erxes api crons will be running on", + "value": "3600" + }, + "PORT_WORKERS": { + "description": "A port number that erxes api workers will be running on", + "value": "3700" + }, + "MAIN_APP_DOMAIN": { + "description": "Erxes URL", + "value": "https://erxes.herokuapp.com" + }, + "MONGO_URL": { + "description": "MONGO_URL", + "value": "MONGO_URL" + }, + "REDIS_HOST": { + "description": "REDIS_HOST", + "value": "REDIS_HOST" + }, + "REDIS_PORT": { + "description": "REDIS_PORT", + "value": "28229" + }, + "REDIS_PASSWORD": { + "description": "REDIS_PASSWORD", + "value": "REDIS_PASSWORD" + }, + "RABBITMQ_HOST": { + "description": "RABBITMQ_HOST", + "value": "amqp://localhost" + }, + "JWT_TOKEN_SECRET": { + "description": "JWT TOKEN SECRET", + "value": "replace it with your token" + }, + "LOGS_API_DOMAIN": { + "description": "LOGS_API_DOMAIN", + "value": "https://LOGS_API_DOMAIN.herokuapp.com" + }, + "INTEGRATIONS_API_DOMAIN": { + "description": "INTEGRATIONS_API_DOMAIN", + "value": "https://INTEGRATIONS_API_DOMAIN.herokuapp.com" + } + }, + "addons": [ + { + "plan": "mongolab:sandbox", + "as": "MONGO" + }, + { + "plan": "heroku-redis:hobby-dev", + "as": "REDIS" + }, + { + "plan": "cloudamqp:lemur", + "as": "RABBITMQ" + }, + { + "plan": "bonsai:sandbox-6", + "as": "ELASTICSEARCH" + } + ], + "buildpacks": [ + { + "url": "https://github.com/batnasan/heroku-buildpack-subdir" + } + ] +} diff --git a/base.Dockerfile b/base.Dockerfile new file mode 100644 index 000000000..41c6f7b83 --- /dev/null +++ b/base.Dockerfile @@ -0,0 +1,10 @@ +ARG BASE_IMAGE +ARG DEBIAN_FRONTEND=noninteractive +FROM $BASE_IMAGE +RUN apt-get update && \ + apt-get install --no-install-recommends -yq gnupg2 wget ca-certificates python build-essential && \ + wget -qO - https://www.mongodb.org/static/pgp/server-3.6.asc | apt-key add - && \ + echo "deb http://repo.mongodb.org/apt/debian stretch/mongodb-org/3.6 main" | tee /etc/apt/sources.list.d/mongodb-org-3.6.list && \ + apt-get update && \ + apt-get install --no-install-recommends -yq mongodb-org-shell=3.6.18 mongodb-org-tools=3.6.18 && \ + rm -rf /var/lib/apt/lists/* diff --git a/base.Dockerfile.dockerignore b/base.Dockerfile.dockerignore new file mode 100644 index 000000000..1d085cacc --- /dev/null +++ b/base.Dockerfile.dockerignore @@ -0,0 +1 @@ +** diff --git a/db-migrate-store.js b/db-migrate-store.js index 061f20639..6edbad3f3 100644 --- a/db-migrate-store.js +++ b/db-migrate-store.js @@ -63,4 +63,5 @@ class dbStore { } } -module.exports = dbStore \ No newline at end of file +module.exports = dbStore + diff --git a/.dockerignore b/elkSyncer/.dockerignore similarity index 100% rename from .dockerignore rename to elkSyncer/.dockerignore diff --git a/elkSyncer/.env.sample b/elkSyncer/.env.sample new file mode 100644 index 000000000..8b28c36ff --- /dev/null +++ b/elkSyncer/.env.sample @@ -0,0 +1,2 @@ +MONGO_URL=mongodb://localhost/erxes +ELASTICSEARCH_URL=http://localhost:9200 \ No newline at end of file diff --git a/elkSyncer/Dockerfile b/elkSyncer/Dockerfile new file mode 100644 index 000000000..f02b9caba --- /dev/null +++ b/elkSyncer/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.7.6-slim + +WORKDIR /elkSyncer/ + +RUN pip install mongo-connector==3.1.1 \ + && pip install elasticsearch==7.5.1 \ + && pip install elastic2-doc-manager==1.0.0 \ + && pip install python-dotenv==0.11.0 \ + && pip install certifi==0.0.8 + +COPY mongo-connector-config.json . +COPY main.py . + +CMD ["python", "main.py"] \ No newline at end of file diff --git a/elkSyncer/main.py b/elkSyncer/main.py new file mode 100644 index 000000000..5e71260a7 --- /dev/null +++ b/elkSyncer/main.py @@ -0,0 +1,170 @@ +#!/usr/bin/python + +import os +import subprocess + +import pymongo +from dotenv import load_dotenv +from elasticsearch import Elasticsearch + +load_dotenv() + +MONGO_URL = os.getenv('MONGO_URL') +ELASTICSEARCH_URL = os.getenv('ELASTICSEARCH_URL') + +client = Elasticsearch([ELASTICSEARCH_URL]) + +nestedType = { + "type" : "nested", + "properties" : { + "field" : { "type" : "keyword"}, + "value" : { "type" : "text" }, + "stringValue" : { "type" : "text" }, + "numberValue" : { "type" : "float" }, + "dateValue" : { "type" : "date" } + } +} + +customer_mapping = { + 'state': { + 'type': 'keyword', + }, + 'primaryEmail': { + 'type': 'text', + 'analyzer': 'uax_url_email_analyzer', + }, + 'integrationId': { + 'type': 'keyword', + }, + 'relatedIntegrationIds': { + 'type': 'keyword', + }, + 'scopeBrandIds': { + 'type': 'keyword', + }, + 'ownerId': { + 'type': 'keyword', + }, + 'position': { + 'type': 'keyword', + }, + 'leadStatus': { + 'type': 'keyword', + }, + 'tagIds': { + 'type': 'keyword', + }, + 'companyIds': { + 'type': 'keyword', + }, + 'mergedIds': { + 'type': 'keyword', + }, + 'status': { + 'type': 'keyword', + }, + 'emailValidationStatus': { + 'type': 'keyword', + }, + "customFieldsData" : nestedType, + "trackedData" : nestedType, +} + +company_mapping = { + 'primaryEmail': { + 'type': 'text', + 'analyzer': 'uax_url_email_analyzer', + }, + 'scopeBrandIds': { + 'type': 'keyword', + }, + 'plan': { + 'type': 'keyword', + }, + 'industry': { + 'type': 'keyword', + }, + 'parentCompanyId': { + 'type': 'keyword', + }, + 'ownerId': { + 'type': 'keyword', + }, + 'tagIds': { + 'type': 'keyword', + }, + 'mergedIds': { + 'type': 'keyword', + }, + 'status': { + 'type': 'keyword', + }, + 'businessType': { + 'type': 'keyword', + }, + "customFieldsData" : nestedType +} + +event_mapping = { + 'type': { + 'type': 'keyword', + }, + 'name': { + 'type': 'keyword', + }, + 'customerId': { + 'type': 'keyword', + }, + "attributes" : nestedType +} + +analysis = { + 'analyzer': { + 'uax_url_email_analyzer': { + 'tokenizer': 'uax_url_email_tokenizer', + }, + }, + 'tokenizer': { + 'uax_url_email_tokenizer': { + 'type': 'uax_url_email', + }, + }, +} + +def put_mappings(index, mapping): + """ + Create mappings + """ + + response = client.indices.exists(index=index) + + print('Create index for %s' % index, response) + + if not response: + response = client.indices.create(index=index, body={'settings': {'analysis': analysis}}) + + print('Response', response) + + try: + response = client.indices.put_mapping(index=index, body = {'properties': mapping}) + + print(response) + except Exception as e: + print(e) + + +db_name = pymongo.uri_parser.parse_uri(MONGO_URL)['database'] + +put_mappings('%s__customers' % db_name, customer_mapping) +put_mappings('%s__companies' % db_name, company_mapping) +put_mappings('%s__events' % db_name, event_mapping) + +command = 'mongo-connector -m "%s" -c mongo-connector-config.json --target-url %s' % (MONGO_URL, ELASTICSEARCH_URL) + +print('Starting connector ....', command) + +process = subprocess.Popen(command, shell=True) + +process.wait() + +print('End connector') diff --git a/elkSyncer/mongo-connector-config.json b/elkSyncer/mongo-connector-config.json new file mode 100644 index 000000000..cfb4c11d6 --- /dev/null +++ b/elkSyncer/mongo-connector-config.json @@ -0,0 +1,31 @@ +{ + "oplogFile": "oplog.timestamp", + "noDump": false, + "batchSize": 5000, + "verbosity": 3, + "continueOnError": true, + "logging": { + "type": "stream" + }, + "namespaces": { + "erxes*.customers": { + "rename": "erxes*__customers._doc", + "excludeFields": ["urlVisits", "messengerData"] + }, + "erxes*.companies": { + "rename": "erxes*__companies._doc" + } + }, + "docManagers": [ + { + "docManager": "elastic2_doc_manager", + "bulkSize": 10, + "uniqueKey": "_id", + "args": { + "clientOptions": { + "timeout": 5000 + } + } + } + ] +} diff --git a/elkSyncer/requirements.txt b/elkSyncer/requirements.txt new file mode 100644 index 000000000..75853c001 --- /dev/null +++ b/elkSyncer/requirements.txt @@ -0,0 +1,5 @@ +mongo-connector==3.1.1 +elasticsearch==7.5.1 +elastic2-doc-manager==1.0.0 +python-dotenv==0.11.0 +certifi==0.0.8 \ No newline at end of file diff --git a/elkSyncer/runtime.txt b/elkSyncer/runtime.txt new file mode 100644 index 000000000..257b314f5 --- /dev/null +++ b/elkSyncer/runtime.txt @@ -0,0 +1 @@ +python-3.7.6 \ No newline at end of file diff --git a/elkSyncer/wait-for.sh b/elkSyncer/wait-for.sh new file mode 100755 index 000000000..92cbdbb3c --- /dev/null +++ b/elkSyncer/wait-for.sh @@ -0,0 +1,182 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi \ No newline at end of file diff --git a/email-verifier/.env.sample b/email-verifier/.env.sample new file mode 100644 index 000000000..8e713b499 --- /dev/null +++ b/email-verifier/.env.sample @@ -0,0 +1,11 @@ +# general +NODE_ENV=development +PORT=4100 + +# MongoDB +MONGO_URL=mongodb://localhost/erxes-email-verifier + +TRUE_MAIL_API_KEY= + +CLEAR_OUT_PHONE_API_KEY= + diff --git a/email-verifier/Dockerfile b/email-verifier/Dockerfile new file mode 100644 index 000000000..8df46edda --- /dev/null +++ b/email-verifier/Dockerfile @@ -0,0 +1,7 @@ +FROM node:12.18-slim +WORKDIR /erxes-email-verifier +RUN chown -R node:node /erxes-email-verifier +COPY --chown=node:node . /erxes-email-verifier +USER node +EXPOSE 4100 +ENTRYPOINT ["node", "--max_old_space_size=8192", "dist"] diff --git a/email-verifier/package.json b/email-verifier/package.json new file mode 100644 index 000000000..1294bab8a --- /dev/null +++ b/email-verifier/package.json @@ -0,0 +1,38 @@ +{ + "name": "erxes-email-verifier", + "private": true, + "scripts": { + "start": "node dist", + "dev": "NODE_ENV=development DEBUG=erxes-email-verifier:* node_modules/.bin/ts-node-dev --experimental-worker --respawn src", + "build": "tsc -p tsconfig.prod.json" + }, + "dependencies": { + "body-parser": "^1.19.0", + "csv-writer": "^1.6.0", + "debug": "^4.1.1", + "dotenv": "^4.0.0", + "email-deep-validator": "^3.3.0", + "express": "^4.16.4", + "mongoose": "5.7.5", + "node-schedule": "^1.3.2", + "redis": "^3.0.2", + "request": "^2.88.2", + "request-promise": "^4.2.5", + "requestify": "^0.2.5", + "xss": "^1.0.6" + }, + "peerOptionalDependencies": { + "kerberos": "^1.0.0" + }, + "devDependencies": { + "@types/dotenv": "^4.0.3", + "@types/express": "^4.16.0", + "@types/mongodb": "^3.1.2", + "@types/mongoose": "^5.2.1", + "@types/node": "^10.12.18", + "@types/q": "^1.5.0", + "ts-node": "8.0.3", + "ts-node-dev": "^1.0.0-pre.32", + "typescript": "^3.7.2" + } +} diff --git a/email-verifier/src/api.ts b/email-verifier/src/api.ts new file mode 100644 index 000000000..a080f4e02 --- /dev/null +++ b/email-verifier/src/api.ts @@ -0,0 +1,242 @@ +import * as EmailValidator from 'email-deep-validator'; +import { EMAIL_VALIDATION_SOURCES, EMAIL_VALIDATION_STATUSES, Emails } from './models'; +import { getArray, setArray } from './redisClient'; +import { debugBase, sendRequest } from './utils'; + +const { TRUE_MAIL_API_KEY, EMAIL_VERIFICATION_TYPE = 'truemail' } = process.env; + +const singleTrueMail = async (email: string) => { + try { + const url = `https://truemail.io/api/v1/verify/single?access_token=${TRUE_MAIL_API_KEY}&email=${email}`; + + const response = await sendRequest({ + url, + method: 'GET', + }); + + if (typeof response === 'string') { + return JSON.parse(response); + } + + return response; + } catch (e) { + debugBase(`Error occured during single true mail validation ${e.message}`); + throw e; + } +}; + +const bulkTrueMail = async (unverifiedEmails: string[], hostname: string) => { + const url = `https://truemail.io/api/v1/tasks/bulk?access_token=${TRUE_MAIL_API_KEY}`; + + try { + const result = await sendRequest({ + url, + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: { + file: unverifiedEmails.map(e => ({ email: e })), + }, + }); + + let data; + let error; + + if (typeof result === 'string') { + data = JSON.parse(result).data; + error = JSON.parse(result).error; + } else { + data = result.data; + error = result.error; + } + + if (data) { + const taskIds = await getArray('erxes_email_verifier_task_ids'); + + taskIds.push({ taskId: data.task_id, hostname }); + + setArray('erxes_email_verifier_task_ids', taskIds); + } else if (error) { + throw new Error(error.message); + } + } catch (e) { + // request may fail + throw e; + } +}; + +export const single = async (email: string, hostname: string) => { + const emailOnDb = await Emails.findOne({ email }); + + if (emailOnDb) { + debugBase(`This email is already verified`); + + return sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + email: { email, status: emailOnDb.status }, + source: EMAIL_VALIDATION_SOURCES.ERXES, + }, + }); + } + + const emailValidator = new EmailValidator(); + const { validDomain, validMailbox } = await emailValidator.verify(email); + + if (validDomain && validMailbox) { + return sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + email: { email, status: EMAIL_VALIDATION_STATUSES.VALID }, + source: EMAIL_VALIDATION_SOURCES.ERXES, + }, + }); + } + + let response: { status?: string; result?: string } = {}; + + if (EMAIL_VERIFICATION_TYPE === 'truemail') { + try { + debugBase(`Email is not found on verifier DB. Sending request to truemail`); + response = await singleTrueMail(email); + + debugBase(`Received single email validation status`); + } catch (e) { + // request may fail + throw e; + } + } + + if (response.status === 'success') { + const doc = { email, status: response.result }; + + if (doc.status === EMAIL_VALIDATION_STATUSES.VALID || doc.status === EMAIL_VALIDATION_STATUSES.INVALID) { + await Emails.createEmail(doc); + } + + debugBase(`Sending single email validation status to erxes-api`); + + return sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + email: doc, + source: EMAIL_VALIDATION_SOURCES.TRUEMAIL, + }, + }); + } + + // if status is not success + return sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + email: { email, status: EMAIL_VALIDATION_STATUSES.UNKNOWN }, + source: EMAIL_VALIDATION_SOURCES.TRUEMAIL, + }, + }); +}; + +export const bulk = async (emails: string[], hostname: string) => { + const emailsOnDb = await Emails.find({ email: { $in: emails } }); + + const emailsMap: Array<{ email: string; status: string }> = emailsOnDb.map(({ email, status }) => ({ + email, + status, + })); + + const verifiedEmails = emailsMap.map(verified => ({ email: verified.email, status: verified.status })); + + const unverifiedEmails = emails.filter(email => !verifiedEmails.some(e => e.email === email)); + + if (verifiedEmails.length > 0) { + try { + debugBase(`Sending already verified emails to erxes-api`); + + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + emails: verifiedEmails, + source: EMAIL_VALIDATION_SOURCES.ERXES, + }, + }); + } catch (e) { + // request may fail + throw e; + } + } + + if (unverifiedEmails.length > 0) { + debugBase(`Sending unverified email to truemail`); + + return bulkTrueMail(unverifiedEmails, hostname); + } +}; + +export const checkTask = async (taskId: string) => { + const url = `https://truemail.io/api/v1/tasks/${taskId}/status?access_token=${TRUE_MAIL_API_KEY}`; + + const response = await sendRequest({ + url, + method: 'GET', + }); + + return JSON.parse(response).data; +}; + +export const getTrueMailBulk = async (taskId: string, hostname: string) => { + debugBase(`Downloading bulk email validation result`); + + const url = `https://truemail.io/api/v1/tasks/${taskId}/download?access_token=${TRUE_MAIL_API_KEY}&timeout=30000`; + + const response = await sendRequest({ + url, + method: 'GET', + }); + + const rows = response.split('\n'); + const emails: Array<{ email: string; status: string }> = []; + + for (const row of rows) { + const rowArray = row.split(','); + + if (rowArray.length > 2) { + const email = rowArray[0]; + const status = rowArray[2]; + + emails.push({ + email, + status, + }); + + if (status === EMAIL_VALIDATION_STATUSES.VALID || status === EMAIL_VALIDATION_STATUSES.INVALID) { + const found = await Emails.findOne({ email }); + + if (!found) { + const doc = { + email, + status, + created: new Date(), + }; + + await Emails.createEmail(doc); + } + } + } + } + + debugBase(`Sending bulk email validation result to erxes-api`); + + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + emails, + source: EMAIL_VALIDATION_SOURCES.TRUEMAIL, + }, + }); +}; diff --git a/email-verifier/src/apiPhoneVerifier.ts b/email-verifier/src/apiPhoneVerifier.ts new file mode 100644 index 000000000..a2f19a3fc --- /dev/null +++ b/email-verifier/src/apiPhoneVerifier.ts @@ -0,0 +1,327 @@ +import * as csv from 'csv-writer'; +import * as dotenv from 'dotenv'; +import * as fs from 'fs'; +import * as request from 'request-promise'; +import { PHONE_VALIDATION_STATUSES, Phones } from './models'; +import { getArray, setArray } from './redisClient'; +import { debugBase, getEnv, sendRequest } from './utils'; + +dotenv.config(); + +const CLEAR_OUT_PHONE_API_KEY = getEnv({ name: 'CLEAR_OUT_PHONE_API_KEY' }); + +const savePhone = async (doc: { + phone: string; + status: string; + lineType?: string; + carrier?: string; + internationalFormat?: string; + localFormat?: string; +}) => { + if (doc.lineType === 'mobile') { + doc.status = PHONE_VALIDATION_STATUSES.RECEIVES_SMS; + } + await Phones.createPhone(doc); +}; + +const singleClearOut = async (phone: string): Promise => { + try { + const response = await sendRequest({ + url: 'https://api.clearoutphone.io/v1/phonenumber/validate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer:${CLEAR_OUT_PHONE_API_KEY}`, + }, + body: { number: phone }, + }); + + if (typeof response === 'string') { + return JSON.parse(response); + } + + return response; + } catch (e) { + debugBase(`Error occured during single phone validation ${e.message}`); + throw e; + } +}; + +const bulkClearOut = async (unverifiedPhones: string[], hostname: string) => { + const fileName = + Math.random() + .toString(36) + .substring(2, 15) + + Math.random() + .toString(36) + .substring(2, 15); + + const csvWriter = csv.createObjectCsvWriter({ + path: `./${fileName}.csv`, + header: [{ id: 'number', title: 'Phone' }], + }); + + await csvWriter.writeRecords(unverifiedPhones.map(phone => ({ number: phone }))); + + try { + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + await sendFile(fileName, hostname); + } catch (e) { + debugBase(`Error occured during bulk phone validation ${e.message}`); + throw e; + } +}; + +export const sendFile = async (fileName: string, hostname: string) => { + try { + const result = await request({ + method: 'POST', + url: 'https://api.clearoutphone.io/v1/phonenumber/bulk', + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer:${CLEAR_OUT_PHONE_API_KEY}`, + }, + formData: { + file: fs.createReadStream(`./${fileName}.csv`), + }, + }); + + let data; + let error; + + if (typeof result === 'string') { + data = JSON.parse(result).data; + error = JSON.parse(result).error; + } else { + data = result.data; + error = result.error; + } + + if (data) { + const listIds = await getArray('erxes_phone_verifier_list_ids'); + + listIds.push({ listId: data.list_id, hostname }); + + setArray('erxes_phone_verifier_list_ids', listIds); + + await fs.unlinkSync(`./${fileName}.csv`); + } else if (error) { + throw new Error(error.message); + } + } catch (e) { + // request may fail + throw e; + } +}; + +export const getStatus = async (listId: string) => { + const url = `https://api.clearoutphone.io/v1/phonenumber/bulk/progress_status?list_id=${listId}`; + try { + const result = await request({ + method: 'GET', + url, + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: `Bearer:${CLEAR_OUT_PHONE_API_KEY}`, + }, + }); + + if (typeof result === 'string') { + return JSON.parse(result); + } + + return result; + } catch (e) { + // request may fail + throw e; + } +}; + +export const validateSinglePhone = async (phone: string, hostname: string) => { + const phoneOnDb = await Phones.findOne({ phone }).lean(); + + if (phoneOnDb) { + debugBase(`This phone number is already verified`); + + return sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + phone: { phone, status: phoneOnDb.status }, + }, + }); + } + + if (!phone.includes('+')) { + debugBase('Phone number must include country code for verification!'); + throw new Error('Phone number must include country code for verification!'); + } + + let response: { status?: string; data?: any } = {}; + + try { + debugBase(`Phone number is not found on verifier DB. Sending request to clearoutphone`); + response = await singleClearOut(phone); + debugBase(`Received single phone validation status`); + } catch (e) { + return { phone, status: PHONE_VALIDATION_STATUSES.UNKNOWN }; + } + + if (response.status === 'success') { + const data = response.data; + await savePhone({ + phone, + status: data.status, + lineType: data.lineType, + carrier: data.carrier, + internationalFormat: data.internationalFormat, + localFormat: data.localFormat, + }); + + debugBase(`Sending single phone validation status to erxes-api`); + + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + phone: { phone, status: data.status }, + }, + }); + } else { + // if status is not success + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + phone: { phone, status: PHONE_VALIDATION_STATUSES.UNKNOWN }, + }, + }); + } +}; + +export const validateBulkPhones = async (phones: string[], hostname: string) => { + const phonesOnDb = await Phones.find({ phone: { $in: phones } }); + + const phonesMap: Array<{ phone: string; status: string }> = phonesOnDb.map(({ phone, status }) => ({ + phone, + status, + })); + + const verifiedPhones = phonesMap.map(verified => ({ phone: verified.phone, status: verified.status })); + + const unverifiedPhones: string[] = phones.filter(phone => !verifiedPhones.some(p => p.phone === phone)); + + if (verifiedPhones.length > 0) { + try { + debugBase(`Sending already verified phones to erxes-api`); + + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + phones: verifiedPhones, + }, + }); + } catch (e) { + // request may fail + throw e; + } + } + + if (unverifiedPhones.length > 0) { + try { + debugBase(`Sending unverified phones to clearoutphone`); + await bulkClearOut(unverifiedPhones, hostname); + } catch (e) { + // request may fail + throw e; + } + } +}; + +export const getBulkResult = async (listId: string, hostname: string) => { + const url = 'https://api.clearoutphone.io/v1/download/result'; + const headers = { + 'Content-Type': 'application/json', + Authorization: `Bearer:${CLEAR_OUT_PHONE_API_KEY}`, + }; + + try { + debugBase(`Downloading bulk phone validation result`); + const response = await sendRequest({ + url, + method: 'POST', + headers, + body: { list_id: listId }, + }); + + try { + const resp = await sendRequest({ + url: response.data.url, + method: 'GET', + }); + + const rows = resp.split('\n'); + const phones: Array<{ phone: string; status: string }> = []; + + for (const [index, row] of rows.entries()) { + if (index !== 0) { + const rowArray = row.split(','); + + if (rowArray.length > 12) { + const phone = rowArray[0]; + let status = rowArray[1].toLowerCase(); + const lineType = rowArray[2]; + const carrier = rowArray[3]; + const internationalFormat = rowArray[8]; + const localFormat = rowArray[9]; + + if (lineType === 'mobile') { + status = PHONE_VALIDATION_STATUSES.RECEIVES_SMS; + } + + phones.push({ + phone, + status, + }); + + const found = await Phones.findOne({ phone }); + + if (!found) { + const doc = { + phone, + status, + created: new Date(), + lineType, + carrier, + internationalFormat, + localFormat, + }; + + await savePhone(doc); + } + } + } + } + + debugBase(`Sending bulk phone validation result to erxes-api`); + + await sendRequest({ + url: `${hostname}/verifier/webhook`, + method: 'POST', + body: { + phones, + }, + }); + } catch (e) { + // request may fail + throw e; + } + } catch (e) { + // request may fail + throw e; + } +}; diff --git a/email-verifier/src/connection.ts b/email-verifier/src/connection.ts new file mode 100644 index 000000000..f7b854647 --- /dev/null +++ b/email-verifier/src/connection.ts @@ -0,0 +1,53 @@ +import * as dotenv from 'dotenv'; +import mongoose = require('mongoose'); +import { debugBase } from './utils'; + +dotenv.config(); + +mongoose.Promise = global.Promise; + +export const connectionOptions = { + useNewUrlParser: true, + useCreateIndex: true, + autoReconnect: true, + useFindAndModify: false, +}; + +const { MONGO_URL } = process.env; + +mongoose.connection + .on('connected', () => { + debugBase(`Connected to the database: ${MONGO_URL}`); + }) + .on('disconnected', () => { + debugBase(`Disconnected from the database: ${MONGO_URL}`); + }) + .on('error', error => { + debugBase(`Database connection error: ${MONGO_URL}`, error); + }); + +export const connect = async (URL?: string, options?) => { + return mongoose.connect(URL || MONGO_URL, { + ...connectionOptions, + ...(options || { poolSize: 100 }), + }); +}; + +/** + * Health check status + */ +export const mongoStatus = () => { + return new Promise((resolve, reject) => { + mongoose.connection.db.admin().ping((err, result) => { + if (err) { + return reject(err); + } + + return resolve(result); + }); + }); +}; + +export function disconnect() { + return mongoose.connection.close(); +} diff --git a/email-verifier/src/cronJobs/verifier.ts b/email-verifier/src/cronJobs/verifier.ts new file mode 100644 index 000000000..4f7f4bb87 --- /dev/null +++ b/email-verifier/src/cronJobs/verifier.ts @@ -0,0 +1,55 @@ +import * as schedule from 'node-schedule'; +import { checkTask, getTrueMailBulk } from '../api'; +import { getBulkResult, getStatus } from '../apiPhoneVerifier'; +import { getArray, setArray } from '../redisClient'; +import { debugCrons } from '../utils'; + +schedule.scheduleJob('1 * * * * *', async () => { + let listIds = await getArray('erxes_phone_verifier_list_ids'); + + if (listIds.length === 0) { + return; + } + + for (const { listId, hostname } of listIds) { + debugCrons(`Getting validation progress status with list_id: ${listId}`); + + const { status, data } = await getStatus(listId); + + if (status === 'success' && data.progress_status === 'completed') { + await getBulkResult(listId, hostname).catch(e => { + debugCrons(`Failed to get phone list. Error: ${e.message}`); + }); + debugCrons(`Process is finished with list_id: ${listId}`); + listIds = listIds.filter(item => { + return item.listId !== listId; + }); + + setArray('erxes_phone_verifier_list_ids', listIds); + } + } +}); + +schedule.scheduleJob('2 * * * * *', async () => { + let taskIds = await getArray('erxes_email_verifier_task_ids'); + + if (taskIds.length === 0) { + return; + } + + for (const { taskId, hostname } of taskIds) { + const result = await checkTask(taskId); + + if (result.status === 'finished') { + await getTrueMailBulk(taskId, hostname).catch(e => { + debugCrons(`Failed to get email list. Error: ${e.message}`); + }); + + taskIds = taskIds.filter(item => { + return item.taskId !== taskId; + }); + + setArray('erxes_email_verifier_task_ids', taskIds); + } + } +}); diff --git a/email-verifier/src/index.ts b/email-verifier/src/index.ts new file mode 100644 index 000000000..8b94fb364 --- /dev/null +++ b/email-verifier/src/index.ts @@ -0,0 +1,87 @@ +import * as bodyParser from 'body-parser'; +import * as dotenv from 'dotenv'; +import * as express from 'express'; +import { filterXSS } from 'xss'; +import { bulk, single } from './api'; +import { validateBulkPhones, validateSinglePhone } from './apiPhoneVerifier'; +import { connect } from './connection'; +import './cronJobs/verifier'; +import { initRedis } from './redisClient'; +import { debugBase, debugCrons, debugRequest } from './utils'; + +// load environment variables +dotenv.config(); + +const app = express(); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +app.post('/verify-single', async (req, res, next) => { + debugRequest(debugBase, req); + + const { email, phone, hostname } = req.body; + + if (email) { + try { + const result = await single(email, hostname); + + return res.json(result); + } catch (e) { + return next(new Error(e)); + } + } + + try { + const result = await validateSinglePhone(phone, hostname); + + return res.json(result); + } catch (e) { + return next(new Error(e)); + } +}); + +app.post('/verify-bulk', async (req, res, next) => { + debugRequest(debugBase, req); + + const { phones, emails, hostname } = req.body; + + if (phones) { + try { + const result = await validateBulkPhones(phones, hostname); + + return res.json({ phones: result }); + } catch (e) { + return next(e); + } + } + + try { + const result = await bulk(emails, hostname); + return res.json({ emails: result }); + } catch (e) { + return next(e); + } +}); + +// Error handling middleware +app.use((error, _req, res, _next) => { + const msg = filterXSS(error.message); + + debugBase(`Error: `, msg); + res.status(500).send(msg); +}); + +const { PORT } = process.env; + +app.listen(PORT, async () => { + await connect(); + initRedis(); + debugBase(`Email verifier server is running on port ${PORT}`); +}); + +const { PORT_CRONS = 4700 } = process.env; + +app.listen(PORT_CRONS, () => { + debugCrons(`Cron Server is now running on ${PORT_CRONS}`); +}); diff --git a/email-verifier/src/models.ts b/email-verifier/src/models.ts new file mode 100644 index 000000000..7bc434873 --- /dev/null +++ b/email-verifier/src/models.ts @@ -0,0 +1,111 @@ +import { Document, Model, model, Schema } from 'mongoose'; + +export const EMAIL_VALIDATION_STATUSES = { + VALID: 'valid', + INVALID: 'invalid', + ACCEPT_ALL_UNVERIFIABLE: 'accept_all_unverifiable', + UNKNOWN: 'unknown', + DISPOSABLE: 'disposable', + CATCHALL: 'catchall', + BAD_SYNTAX: 'badsyntax', + UNVERIFIABLE: 'unverifiable', + NOT_CHECKED: 'Not checked', + ALL: [ + 'valid', + 'invalid', + 'accept_all_unverifiable', + 'unknown', + 'disposable', + 'catchall', + 'badsyntax', + 'unverifiable', + 'Not checked', + ], +}; + +export const PHONE_VALIDATION_STATUSES = { + VALID: 'valid', + INVALID: 'invalid', + UNKNOWN: 'unknown', + RECEIVES_SMS: 'receives_sms', + UNVERIFIABLE: 'unverifiable', + ALL: ['valid', 'invalid', 'unknown', 'receives_sms', 'unverifiable'], +}; + +export const EMAIL_VALIDATION_SOURCES = { + ERXES: 'erxes', + TRUEMAIL: 'truemail', + ALL: ['erxes', 'truemail'], +}; + +interface IEmail { + email: string; + status: string; +} + +interface IEmailDocument extends IEmail, Document { + _id: string; +} + +const emailSchema = new Schema({ + email: { type: String, unique: true }, + status: { type: String, enum: EMAIL_VALIDATION_STATUSES.ALL }, + created: { type: Date, default: Date.now() }, +}); + +interface IEmailModel extends Model { + createEmail(doc: IEmail): Promise; +} + +interface IPhone { + phone: string; + status: string; + lineType?: string; + carrier?: string; + localFormat?: string; + internationalFormat?: string; +} + +interface IPhoneDocument extends IPhone, Document { + _id: string; +} + +const phoneSchema = new Schema({ + phone: { type: String, unique: true }, + status: { type: String, enum: PHONE_VALIDATION_STATUSES.ALL }, + lineType: { type: String, optional: true }, + carrier: { type: String, optional: true }, + localFormat: { type: String, optional: true }, + internationalFormat: { type: String, optional: true }, + created: { type: Date, default: Date.now() }, +}); + +interface IPhoneModel extends Model { + createPhone(doc: IPhone): Promise; +} + +export const loadClass = () => { + class Email { + public static createEmail(doc: IEmail) { + return Emails.create(doc); + } + } + + emailSchema.loadClass(Email); + + class Phone { + public static createPhone(doc: IPhone) { + return Phones.create(doc); + } + } + + phoneSchema.loadClass(Phone); +}; + +loadClass(); + +// tslint:disable-next-line +export const Emails = model('emails', emailSchema); + +// tslint:disable-next-line:variable-name +export const Phones = model('phones', phoneSchema); diff --git a/src/redisClient.ts b/email-verifier/src/redisClient.ts similarity index 59% rename from src/redisClient.ts rename to email-verifier/src/redisClient.ts index dded90b1d..381555b4c 100644 --- a/src/redisClient.ts +++ b/email-verifier/src/redisClient.ts @@ -16,30 +16,25 @@ const { NODE_ENV?: string; } = process.env; -/** - * Docs on the different redis options - * @see {@link https://github.com/NodeRedis/node_redis#options-object-properties} - */ -export const redisOptions = { - host: REDIS_HOST, - port: REDIS_PORT, - password: REDIS_PASSWORD, - connect_timeout: 15000, - enable_offline_queue: true, - retry_unfulfilled_commands: true, - retry_strategy: options => { - // reconnect after - return Math.max(options.attempt * 100, 3000); - }, -}; +let client; -let client = { - get: (_key, _callback) => true, - set: (_key, _value) => true, -}; +export const initRedis = (callback?: (client) => void) => { + client = redis.createClient({ + host: REDIS_HOST, + port: REDIS_PORT, + password: REDIS_PASSWORD, + connect_timeout: 15000, + enable_offline_queue: true, + retry_unfulfilled_commands: true, + retry_strategy: options => { + // reconnect after + return Math.max(options.attempt * 100, 3000); + }, + }); -export const initRedis = () => { - client = redis.createClient(redisOptions); + if (callback) { + callback(client); + } }; /* @@ -48,7 +43,7 @@ export const initRedis = () => { export const get = (key: string, defaultValue?: any): Promise => { return new Promise((resolve, reject) => { if (NODE_ENV === 'test') { - return resolve(''); + return resolve(defaultValue || ''); } client.get(key, (error, reply) => { @@ -87,3 +82,19 @@ export const getArray = async (key: string): Promise => { export const setArray = (key: string, value: any[]) => { client.set(key, JSON.stringify(value)); }; + +/** + * Health check status + * retryStrategy - get response immediately + */ +export const redisStatus = () => { + return new Promise((resolve, reject) => { + client.ping((error, result) => { + if (error) { + return reject(error); + } + + return resolve(result); + }); + }); +}; diff --git a/email-verifier/src/utils.ts b/email-verifier/src/utils.ts new file mode 100644 index 000000000..8b624d461 --- /dev/null +++ b/email-verifier/src/utils.ts @@ -0,0 +1,76 @@ +import * as debug from 'debug'; +import * as requestify from 'requestify'; + +export const debugBase = debug('erxes-email-verifier:base'); +export const debugCrons = debug('erxes-email-verifier:crons'); + +export const debugRequest = (debugInstance, req) => + debugInstance(` + Receiving ${req.path} request from ${req.headers.origin} + body: ${JSON.stringify(req.body || {})} + queryParams: ${JSON.stringify(req.query)} + `); + +interface IRequestParams { + url?: string; + path?: string; + method?: string; + headers?: { [key: string]: string }; + params?: { [key: string]: string }; + body?: { [key: string]: any }; + form?: { [key: string]: any }; +} + +/** + * Sends post request to specific url + */ +export const sendRequest = async ( + { url, method, headers, form, body, params }: IRequestParams, + errorMessage?: string, +) => { + debugBase(` + Sending request to + url: ${url} + method: ${method} + body: ${JSON.stringify(body)} + params: ${JSON.stringify(params)} + headers: ${JSON.stringify(headers)} + form: ${JSON.stringify(form)} + `); + + try { + const response = await requestify.request(url, { + method, + headers: { 'Content-Type': 'application/json', ...(headers || {}) }, + form, + body, + params, + }); + + const responseBody = response.getBody(); + + debugBase(` + Success from : ${url} + responseBody: ${JSON.stringify(responseBody)} + `); + + return responseBody; + } catch (e) { + if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND') { + throw new Error(errorMessage); + } else { + const message = e.body || e.message; + throw new Error(message); + } + } +}; + +export const getEnv = ({ name, defaultValue }: { name: string; defaultValue?: string }): string => { + const value = process.env[name]; + + if (!value && typeof defaultValue !== 'undefined') { + return defaultValue; + } + + return value || ''; +}; diff --git a/email-verifier/tsconfig.json b/email-verifier/tsconfig.json new file mode 100644 index 000000000..5d28d8b76 --- /dev/null +++ b/email-verifier/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "allowJs": true, + "target": "es6", + "moduleResolution": "node", + "module": "commonjs", + "lib": ["es2015", "es6", "es7", "esnext.asynciterable"], + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["./src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/email-verifier/tsconfig.prod.json b/email-verifier/tsconfig.prod.json new file mode 100644 index 000000000..fd8086ccf --- /dev/null +++ b/email-verifier/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/__tests__"] +} diff --git a/email-verifier/yarn.lock b/email-verifier/yarn.lock new file mode 100644 index 000000000..e6674b112 --- /dev/null +++ b/email-verifier/yarn.lock @@ -0,0 +1,1644 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bson@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.1.tgz#2bfc80819e7055b76d5496d5344ed23e5d12bbb2" + integrity sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw== + dependencies: + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/dotenv@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67" + integrity sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf" + integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@^4.16.0": + version "4.17.3" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.3.tgz#38e4458ce2067873b09a73908df488870c303bd9" + integrity sha512-I8cGRJj3pyOLs/HndoP+25vOqhqWkAZsWMEmq1qXy/b/M3ppufecUwaK2/TVDVxcV61/iSdhykUjQQ2DLSrTdg== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/mongodb@*", "@types/mongodb@^3.1.2": + version "3.5.2" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.2.tgz#2d815ebb9de9019dd12eb72e8ed1a5121619bf0a" + integrity sha512-/p4+HjfQqmNtq88rlLJ9XgUROhmbdUEU0yeVPCPIt8/vA1fSO1dSjwsRcGNaGuPMUSDRuRm8tDlXeGpWUVF71w== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/mongoose@^5.2.1": + version "5.7.6" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.7.6.tgz#c08d41b33a4a5e1984fc4d3c7bc2a76cc6e17ffc" + integrity sha512-+GMPd3MRem1A7fw97PkE2aQwWoyYMMZ55lyr7slaCnAvz24+c2WQ2zGHafT9toW/gOciN2yDTY1e0xGLeKHgRA== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + +"@types/node@*": + version "13.9.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.1.tgz#96f606f8cd67fb018847d9b61e93997dabdefc72" + integrity sha512-E6M6N0blf/jiZx8Q3nb0vNaswQeEyn0XlupO+xN6DtJ6r6IT4nXrTry7zhIfYvFCl3/8Cu6WIysmUBKiqV0bqQ== + +"@types/node@^10.12.18": + version "10.17.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" + integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== + +"@types/q@^1.5.0": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +ajv@^6.5.5: + version "6.12.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" + integrity sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" + integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.0, body-parser@^1.19.0: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bson@^1.1.1, bson@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cron-parser@^2.7.3: + version "2.15.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.15.0.tgz#04803cd51d8efcfcc6f83ac08e60f3f8c40c7ec5" + integrity sha512-rMFkrQw8+oG5OuwjiXesup4KeIlEG/IU82YtG4xyAHbO5jhKmYaHPp/ZNhq9+7TjSJ65E3zV3kQPUbmXSff2/g== + dependencies: + is-nan "^1.3.0" + moment-timezone "^0.5.31" + +cssfilter@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= + +csv-writer@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/csv-writer/-/csv-writer-1.6.0.tgz#d0cea44b6b4d7d3baa2ecc6f3f7209233514bcf9" + integrity sha512-NOx7YDFWEsM/fTRAJjRpPp8t+MKRVvniAg9wQlUKx20MFrPs73WLJhFf5iteqrxNYnsy924K3Iroh3yNHeYd2g== + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debounce@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= + +dynamic-dedupe@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= + dependencies: + xtend "^4.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +email-deep-validator@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/email-deep-validator/-/email-deep-validator-3.3.0.tgz#095e2a725ca730c4a75b34c90f665b87df80768b" + integrity sha512-iTthLvoKphPK56T2ge5uTxDFixcXfVMfjPQyJewSv9ZbbocdfmYyoB6rZQYEK5uCOsId2tYsY2/+5J9vbr2spw== + dependencies: + loglevel "^1.6.1" + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +express@^4.16.4: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +filewatcher@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= + dependencies: + debounce "^1.0.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-nan@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" + integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== + dependencies: + define-properties "^1.1.3" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +jquery@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kareem@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" + integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +loglevel@^1.6.1: + version "1.6.7" + resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" + integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== + +long-timeout@0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-db@1.44.0: + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== + dependencies: + mime-db "1.44.0" + +mime-types@~2.1.24: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +moment-timezone@^0.5.31: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== + dependencies: + moment ">= 2.9.0" + +"moment@>= 2.9.0": + version "2.27.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + +mongodb@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.2.tgz#ff086b5f552cf07e24ce098694210f3d42d668b2" + integrity sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.7.5.tgz#b787b47216edf62036aa358c3ef0f1869c46cdc2" + integrity sha512-BZ4FxtnbTurc/wcm/hLltLdI4IDxo4nsE0D9q58YymTdZwreNzwO62CcjVtaHhmr8HmJtOInp2W/T12FZaMf8g== + dependencies: + bson "~1.1.1" + kareem "2.3.1" + mongodb "3.3.2" + mongoose-legacy-pluralize "1.0.2" + mpath "0.6.0" + mquery "3.2.2" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.1.2" + sift "7.0.1" + sliced "1.0.1" + +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== + +mquery@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" + integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +node-notifier@^5.4.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +node-schedule@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" + integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw== + dependencies: + cron-parser "^2.7.3" + long-timeout "0.1.1" + sorted-array-functions "^1.0.0" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-keys@^1.0.12: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^0.9.7: + version "0.9.7" + resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" + integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redis-commands@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +redis@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a" + integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ== + dependencies: + denque "^1.4.1" + redis-commands "^1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + +request-promise@^4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c" + integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg== + dependencies: + bluebird "^3.5.0" + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.2: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requestify@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/requestify/-/requestify-0.2.5.tgz#80249f1ca7dfdf79fa2a6048aeac37d43e23c905" + integrity sha1-gCSfHKff33n6KmBIrqw31D4jyQU= + dependencies: + jquery "^3.1.0" + q "^0.9.7" + underscore "^1.8.3" + +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve@^1.0.0, resolve@^1.10.0: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== + dependencies: + path-parse "^1.0.6" + +rimraf@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.0.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +sorted-array-functions@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz#43265b21d6e985b7df31621b1c11cc68d8efc7c3" + integrity sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg== + +source-map-support@^0.5.12, source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tough-cookie@^2.3.3, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tree-kill@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +ts-node-dev@^1.0.0-pre.32: + version "1.0.0-pre.44" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.44.tgz#2f4d666088481fb9c4e4f5bc8f15995bd8b06ecb" + integrity sha512-M5ZwvB6FU3jtc70i5lFth86/6Qj5XR5nMMBwVxZF4cZhpO7XcbWw6tbNiJo22Zx0KfjEj9py5DANhwLOkPPufw== + dependencies: + dateformat "~1.0.4-1.2.3" + dynamic-dedupe "^0.3.0" + filewatcher "~3.0.0" + minimist "^1.1.3" + mkdirp "^0.5.1" + node-notifier "^5.4.0" + resolve "^1.0.0" + rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" + ts-node "*" + tsconfig "^7.0.0" + +ts-node@*: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + +ts-node@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641" + integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA== + dependencies: + arg "^4.1.0" + diff "^3.1.0" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^3.7.2: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +underscore@^1.8.3: + version "1.9.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f" + integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +xss@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51" + integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A== + dependencies: + commander "^2.9.0" + cssfilter "0.0.10" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yn@3.1.1, yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/engages-email-sender/.dockerignore b/engages-email-sender/.dockerignore new file mode 100644 index 000000000..340de91ab --- /dev/null +++ b/engages-email-sender/.dockerignore @@ -0,0 +1,8 @@ +*.md +*.yml +.dockerignore +.git* +.prettierrc +Dockerfile* +src +*.tar.gz diff --git a/engages-email-sender/.env.sample b/engages-email-sender/.env.sample new file mode 100644 index 000000000..ca6c46d1a --- /dev/null +++ b/engages-email-sender/.env.sample @@ -0,0 +1,10 @@ +# general +NODE_ENV=development +PORT=3900 + +# public urls +MAIN_API_DOMAIN=http://localhost:3300 + +# MongoDB +MONGO_URL=mongodb://localhost/erxes-engages +TEST_MONGO_URL=mongodb://localhost/erxes-engages-test \ No newline at end of file diff --git a/engages-email-sender/.snyk b/engages-email-sender/.snyk new file mode 100644 index 000000000..29509a00b --- /dev/null +++ b/engages-email-sender/.snyk @@ -0,0 +1,4 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.13.5 +ignore: {} +patch: {} diff --git a/engages-email-sender/Dockerfile b/engages-email-sender/Dockerfile new file mode 100644 index 000000000..8c1a58cf1 --- /dev/null +++ b/engages-email-sender/Dockerfile @@ -0,0 +1,7 @@ +FROM node:12.18-slim +WORKDIR /erxes-engages +RUN chown -R node:node /erxes-engages +COPY --chown=node:node . /erxes-engages +USER node +EXPOSE 3900 +ENTRYPOINT ["node", "--max_old_space_size=8192", "--experimental-worker", "dist"] diff --git a/engages-email-sender/Dockerfile.dev b/engages-email-sender/Dockerfile.dev new file mode 100644 index 000000000..0931d85b2 --- /dev/null +++ b/engages-email-sender/Dockerfile.dev @@ -0,0 +1,5 @@ +FROM erxes/runner +WORKDIR /erxes-engages-email-sender +COPY yarn.lock package.json ./ +RUN yarn install +CMD ["yarn", "dev"] diff --git a/engages-email-sender/jest.config.js b/engages-email-sender/jest.config.js new file mode 100644 index 000000000..96da818a3 --- /dev/null +++ b/engages-email-sender/jest.config.js @@ -0,0 +1,26 @@ +module.exports = { + roots: ['/src/__tests__'], + preset: 'ts-jest', + transform: { + '^.+\\.ts?$': 'ts-jest', + }, + testRegex: '/__tests__/.*\\.(ts|js)$', + globals: { + 'ts-jest': { + tsConfig: 'tsconfig.json', + }, + }, + testEnvironment: 'node', + testPathIgnorePatterns: ['setup.ts', 'factories.ts', 'coverage/'], + moduleFileExtensions: ['ts', 'js', 'json', 'node'], + collectCoverage: true, + coverageDirectory: 'src/__tests__/coverage/', + collectCoverageFrom: ['src/**', '!**/node_modules/**', '!src/__tests__/**'], + coverageThreshold: { + global: { + functions: 100, + lines: 100, + statements: 100, + }, + } +}; diff --git a/engages-email-sender/package.json b/engages-email-sender/package.json new file mode 100644 index 000000000..a3f34aec9 --- /dev/null +++ b/engages-email-sender/package.json @@ -0,0 +1,47 @@ +{ + "name": "erxes-engages", + "private": true, + "scripts": { + "start": "node dist", + "dev": "NODE_ENV=development DEBUG=erxes* node_modules/.bin/ts-node-dev --respawn src", + "build": "tsc -p tsconfig.prod.json", + "test": "jest --coverage --forceExit --runInBand" + }, + "dependencies": { + "@types/body-parser": "^1.17.0", + "@types/cors": "^2.8.4", + "@types/dotenv": "^4.0.3", + "@types/express": "^4.16.0", + "@types/jest": "^24.0.23", + "@types/mongodb": "^3.1.2", + "@types/mongoose": "^5.2.1", + "@types/node": "^10.12.18", + "amqplib": "^0.5.5", + "aws-sdk": "^2.493.0", + "body-parser": "^1.17.1", + "debug": "^4.1.1", + "dotenv": "^4.0.0", + "erxes-message-broker": "^1.0.17", + "express": "^4.16.4", + "faker": "^4.1.0", + "meteor-random": "^0.0.3", + "mongoose": "5.7.5", + "nodemailer": "^6.2.1", + "redis": "^2.8.0", + "requestify": "^0.2.5", + "telnyx": "^1.7.2", + "ts-node": "8.0.3", + "xss": "^1.0.6" + }, + "peerOptionalDependencies": { + "kerberos": "^1.0.0" + }, + "devDependencies": { + "@types/q": "^1.5.0", + "jest": "^24.9.0", + "supertest": "^5.0.0", + "ts-jest": "^24.2.0", + "ts-node-dev": "^1.0.0-pre.32", + "typescript": "^3.7.2" + } +} diff --git a/engages-email-sender/src/__tests__/configsDb.test.ts b/engages-email-sender/src/__tests__/configsDb.test.ts new file mode 100644 index 000000000..794b6a099 --- /dev/null +++ b/engages-email-sender/src/__tests__/configsDb.test.ts @@ -0,0 +1,53 @@ +import { Configs } from '../models'; +import { configFactory } from './factories'; +import './setup'; + +test('Test updateConfigs()', async done => { + const doc = { + accessKeyId: 'accessKeyId', + secretAccessKey: 'secretAccessKey', + region: 'region', + }; + + await Configs.updateConfigs(doc); + + const accessKeyId = await Configs.findOne({ code: doc.accessKeyId }); + const secretAccessKey = await Configs.findOne({ code: doc.secretAccessKey }); + const region = await Configs.findOne({ code: doc.region }); + + expect(accessKeyId.value).toBe(doc.accessKeyId); + expect(secretAccessKey.value).toBe(doc.secretAccessKey); + expect(region.value).toBe(doc.region); + + done(); +}); + +test('Test getConfigs()', async done => { + const accessKeyId = await configFactory({ code: 'accessKeyId' }); + const secretAccessKey = await configFactory({ code: 'secretAccessKey' }); + const region = await configFactory({ code: 'region' }); + + let configs = await Configs.getSESConfigs(); + + expect(configs.secretAccessKey).toBe(secretAccessKey.value); + expect(configs.accessKeyId).toBe(accessKeyId.value); + expect(configs.region).toBe(region.value); + + await Configs.deleteMany({}); + + configs = await Configs.getSESConfigs(); + + expect(configs.secretAccessKey).toBe(''); + expect(configs.accessKeyId).toBe(''); + expect(configs.region).toBe(''); + + done(); +}); + +test('Test createOrUpdateConfig()', async () => { + const config = await configFactory({ code: 'code', value: 'value' }); + + const updated = await Configs.createOrUpdateConfig({ code: config.code, value: 'updatedValue' }); + + expect(updated.value).toBe('updatedValue'); +}); diff --git a/engages-email-sender/src/__tests__/deliveryReportsDb.test.ts b/engages-email-sender/src/__tests__/deliveryReportsDb.test.ts new file mode 100644 index 000000000..43742985c --- /dev/null +++ b/engages-email-sender/src/__tests__/deliveryReportsDb.test.ts @@ -0,0 +1,37 @@ +import { DeliveryReports, Stats } from '../models'; +import { statsFactory } from './factories'; +import './setup'; + +test('Stats: updateStats', async done => { + const statsObj = await statsFactory({ engageMessageId: 'engageMessageId' }); + await Stats.updateStats('engageMessageId', 'bounce'); + + const stats = await Stats.findOne({ engageMessageId: 'engageMessageId' }); + + expect(stats.bounce).toBe(statsObj.bounce + 1); + + done(); +}); + +test('DeliveryReports: updateOrCreateReport', async done => { + const headers = { + engageMessageId: '123', + mailId: 'mailid123', + customerId: 'customer', + }; + + const deliveryReport = await DeliveryReports.updateOrCreateReport(headers, 'open'); + + expect(deliveryReport).toBeDefined(); + expect(deliveryReport).toBeTruthy(); + + const result = await DeliveryReports.updateOrCreateReport(headers, 'complaint'); + + expect(result).toBe('reject'); + + const deliveryReportObj = await DeliveryReports.findOne({ customerId: 'customer' }); + + expect(deliveryReportObj.status).toBe('complaint'); + + done(); +}); diff --git a/engages-email-sender/src/__tests__/factories.ts b/engages-email-sender/src/__tests__/factories.ts new file mode 100644 index 000000000..8f6d3c3f4 --- /dev/null +++ b/engages-email-sender/src/__tests__/factories.ts @@ -0,0 +1,35 @@ +import * as faker from 'faker'; +import { Configs, Stats } from '../models'; + +/** + * Returns random element of an array + */ +export const randomElementOfArray = array => { + return array[Math.floor(Math.random() * array.length)]; +}; + +export const configFactory = params => { + const configObj = new Configs({ + code: params.code || faker.random.word(), + value: params.value || faker.random.word(), + }); + + return configObj.save(); +}; + +export const statsFactory = params => { + const statsObj = new Stats({ + engageMessageId: params.engageMessageId || faker.random.id(), + open: params.open || faker.random.number(), + click: params.click || faker.random.number(), + complaint: params.complaint || faker.random.number(), + delivery: params.delivery || faker.random.number(), + bounce: params.bounce || faker.random.number(), + reject: params.reject || faker.random.number(), + send: params.send || faker.random.number(), + renderingfailure: params.renderingfailure || faker.random.number(), + total: params.total || faker.random.number(), + }); + + return statsObj.save(); +}; diff --git a/engages-email-sender/src/__tests__/httpEndpoints.test.ts b/engages-email-sender/src/__tests__/httpEndpoints.test.ts new file mode 100644 index 000000000..76c05b85e --- /dev/null +++ b/engages-email-sender/src/__tests__/httpEndpoints.test.ts @@ -0,0 +1,30 @@ +import * as request from 'supertest'; + +import { app } from '../index'; +import { smsRequestFactory, telnyxWebhookDataFactory } from '../models/factories'; +import './setup'; + +describe('HTTP endpoint tests', () => { + test('Test /telnyx/webhook', async () => { + const smsRequest = await smsRequestFactory({}); + const webhookParams = { + from: '+13322200406', + to: '+97688276317', + telnyxId: smsRequest.telnyxId, + }; + const webhookData = telnyxWebhookDataFactory(webhookParams); + + const response = await request(app) + .post('/telnyx/webhook') + .send(webhookData); + + const { status, data } = response.body; + + expect(status).toBeDefined(); + expect(status).toBe('ok'); + expect(data).toBeDefined(); + expect(data.payload).toBeDefined(); + expect(data.payload.from).toBe(webhookParams.from); + expect(data.payload.id).toBe(webhookParams.telnyxId); + }); +}); diff --git a/engages-email-sender/src/__tests__/logsDb.test.ts b/engages-email-sender/src/__tests__/logsDb.test.ts new file mode 100644 index 000000000..8bb5c7347 --- /dev/null +++ b/engages-email-sender/src/__tests__/logsDb.test.ts @@ -0,0 +1,13 @@ +import { Logs } from '../models'; +import { LOG_MESSAGE_TYPES } from '../models/Logs'; +import { randomElementOfArray } from './factories'; +import './setup'; + +test('Test createLog()', async () => { + const type = randomElementOfArray(LOG_MESSAGE_TYPES); + const log = await Logs.createLog('messageId', type, 'Message'); + + expect(log.engageMessageId).toBe('messageId'); + expect(log.type).toBe(type); + expect(log.message).toBe('Message'); +}); diff --git a/engages-email-sender/src/__tests__/setup.ts b/engages-email-sender/src/__tests__/setup.ts new file mode 100644 index 000000000..f3fcdbbdf --- /dev/null +++ b/engages-email-sender/src/__tests__/setup.ts @@ -0,0 +1,43 @@ +import * as dotenv from 'dotenv'; +import * as mongoose from 'mongoose'; + +dotenv.config(); + +let db; + +const getCollections = () => { + return Object.keys(db.connection.collections); +}; + +const getCollectionByName = collectionName => { + return db.connection.collections[collectionName]; +}; + +beforeAll(async done => { + db = await mongoose.connect(process.env.TEST_MONGO_URL); + done(); +}); + +afterEach(async () => { + for (const collectionName of getCollections()) { + await getCollectionByName(collectionName).deleteMany({}); + } +}); + +afterAll(async () => { + for (const collectionName of getCollections()) { + try { + await getCollectionByName(collectionName).drop(); + } catch (error) { + if (error.message === 'ns not found') { + return; + } + } + } + + db.connection.removeAllListeners('open'); + + db.connection.db.dropDatabase(); + + db.connection.close(); +}); diff --git a/engages-email-sender/src/api/configs.ts b/engages-email-sender/src/api/configs.ts new file mode 100644 index 000000000..762e65064 --- /dev/null +++ b/engages-email-sender/src/api/configs.ts @@ -0,0 +1,85 @@ +import { Router } from 'express'; +import { debugEngages, debugRequest } from '../debuggers'; +import { Configs } from '../models'; +import { awsRequests } from '../trackers/engageTracker'; +import { createTransporter, updateConfigs } from '../utils'; + +const router = Router(); + +router.post('/save', async (req, res, next) => { + debugRequest(debugEngages, req); + + const { configsMap } = req.body; + + try { + await updateConfigs(configsMap); + } catch (e) { + return next(new Error(e)); + } + + return res.json({ status: 'ok' }); +}); + +router.get('/detail', async (req, res) => { + debugRequest(debugEngages, req); + + const configs = await Configs.find({}); + + return res.json(configs); +}); + +router.get('/get-verified-emails', async (req, res, next) => { + debugRequest(debugEngages, req); + + try { + const emails = await awsRequests.getVerifiedEmails(); + return res.json(emails); + } catch (e) { + return next(new Error(e)); + } +}); + +router.post('/verify-email', async (req, res, next) => { + debugRequest(debugEngages, req); + + try { + const response = await awsRequests.verifyEmail(req.body.email); + return res.json(JSON.stringify(response)); + } catch (e) { + return next(new Error(e)); + } +}); + +router.post('/remove-verified-email', async (req, res, next) => { + debugRequest(debugEngages, req); + + try { + const response = await awsRequests.removeVerifiedEmail(req.body.email); + return res.json(JSON.stringify(response)); + } catch (e) { + return next(new Error(e)); + } +}); + +router.post('/send-test-email', async (req, res, next) => { + debugRequest(debugEngages, req); + + const { from, to, content } = req.body; + + const transporter = await createTransporter(); + + try { + const response = await transporter.sendMail({ + from, + to, + subject: content, + html: content, + }); + + return res.json(JSON.stringify(response)); + } catch (e) { + return next(new Error(e)); + } +}); + +export default router; diff --git a/engages-email-sender/src/api/deliveryReports.ts b/engages-email-sender/src/api/deliveryReports.ts new file mode 100644 index 000000000..56c3bd75b --- /dev/null +++ b/engages-email-sender/src/api/deliveryReports.ts @@ -0,0 +1,78 @@ +import { Router } from 'express'; +import { prepareSmsStats } from '../telnyxUtils'; + +const router = Router(); + +import { debugEngages, debugRequest } from '../debuggers'; +import { DeliveryReports, Logs, Stats } from '../models'; + +router.get('/statsList/:engageMessageId', async (req, res) => { + debugRequest(debugEngages, req); + + const { engageMessageId } = req.params; + + const stats = await Stats.findOne({ engageMessageId }); + + if (!stats) { + return res.json({}); + } + + return res.json(stats); +}); + +router.get('/smsStats/:engageMessageId', async (req, res) => { + debugRequest(debugEngages, req); + + const { engageMessageId } = req.params; + + const smsStats = await prepareSmsStats(engageMessageId); + + return res.json(smsStats); +}); + +router.get('/reportsList', async (req, res) => { + debugRequest(debugEngages, req); + + const { page, perPage } = req.query; + + const _page = Number(page || '1'); + const _limit = Number(perPage || '20'); + + const deliveryReports = await DeliveryReports.find() + .limit(_limit) + .skip((_page - 1) * _limit) + .sort({ createdAt: -1 }); + + if (!deliveryReports) { + return res.json({ list: [], totalCount: 0 }); + } + + const totalCount = await DeliveryReports.countDocuments(); + + return res.json({ + list: deliveryReports, + totalCount, + }); +}); + +router.get(`/reportsList/:engageMessageId`, async (req, res) => { + debugRequest(debugEngages, req); + + const deliveryReports = await DeliveryReports.findOne({ engageMessageId: req.params.engageMessageId }); + + if (!deliveryReports) { + return res.json({}); + } + + return res.json(deliveryReports); +}); + +router.get(`/logs/:engageMessageId`, async (req, res) => { + debugRequest(debugEngages, req); + + const logs = await Logs.find({ engageMessageId: req.params.engageMessageId }); + + return res.json(logs); +}); + +export default router; diff --git a/engages-email-sender/src/api/telnyx.ts b/engages-email-sender/src/api/telnyx.ts new file mode 100644 index 000000000..ba68aff9c --- /dev/null +++ b/engages-email-sender/src/api/telnyx.ts @@ -0,0 +1,26 @@ +import { Router } from 'express'; +import { debugEngages, debugRequest } from '../debuggers'; +import { saveTelnyxHookData } from '../telnyxUtils'; + +const handleWebhookData = async (req, res) => { + debugRequest(debugEngages, req); + + const { data } = req.body; + + await saveTelnyxHookData(data); + + return res.json({ status: 'ok', data }); +}; + +const router = Router(); + +router.post('/webhook', async (req, res) => { + return handleWebhookData(req, res); +}); + +// telnyx sends the same data here if url above fails +router.get('/webhook-failover', async (req, res) => { + return handleWebhookData(req, res); +}); + +export default router; diff --git a/engages-email-sender/src/connection.ts b/engages-email-sender/src/connection.ts new file mode 100644 index 000000000..f5d6d1fb3 --- /dev/null +++ b/engages-email-sender/src/connection.ts @@ -0,0 +1,36 @@ +import * as dotenv from 'dotenv'; +import mongoose = require('mongoose'); +import { debugDb } from './debuggers'; +import { getEnv } from './utils'; + +dotenv.config(); + +mongoose.Promise = global.Promise; + +const MONGO_URL = getEnv({ name: 'MONGO_URL' }); + +export const connectionOptions = { + useNewUrlParser: true, + useCreateIndex: true, + autoReconnect: true, + useFindAndModify: false, +}; + +mongoose.connection + .on('connected', () => { + debugDb(`Connected to the database: ${MONGO_URL}`); + }) + .on('disconnected', () => { + debugDb(`Disconnected from the database: ${MONGO_URL}`); + }) + .on('error', error => { + debugDb(`Database connection error: ${MONGO_URL}`, error); + }); + +export const connect = (URL?: string) => { + return mongoose.connect(URL || MONGO_URL, connectionOptions); +}; + +export function disconnect() { + return mongoose.connection.close(); +} diff --git a/engages-email-sender/src/constants.ts b/engages-email-sender/src/constants.ts new file mode 100644 index 000000000..57049960c --- /dev/null +++ b/engages-email-sender/src/constants.ts @@ -0,0 +1,40 @@ +export const SMS_DELIVERY_STATUSES = { + QUEUED: 'queued', + SENDING: 'sending', + SENT: 'sent', + DELIVERED: 'delivered', + SENDING_FAILED: 'sending_failed', + DELIVERY_FAILED: 'delivery_failed', + DELIVERY_UNCONFIRMED: 'delivery_unconfirmed', + ALL: ['queued', 'sending', 'sent', 'delivered', 'sending_failed', 'delivery_failed', 'delivery_unconfirmed'], + OPTIONS: [ + { + value: 'queued', + label: `The message is queued up on Telnyx's side`, + }, + { + value: 'sending', + label: 'The message is currently being sent to an upstream provider', + }, + { + value: 'sent', + label: 'The message has been sent to the upstream provider', + }, + { + value: 'delivered', + label: 'The upstream provider has confirmed delivery of the message', + }, + { + value: 'sending_failed', + label: 'Telnyx has failed to send the message to the upstream provider', + }, + { + value: 'delivery_failed', + label: 'The upstream provider has failed to send the message to the receiver', + }, + { + value: 'delivery_unconfirmed', + label: 'There is no indication whether or not the message has reached the receiver', + }, + ], +}; diff --git a/engages-email-sender/src/debuggers.ts b/engages-email-sender/src/debuggers.ts new file mode 100644 index 000000000..b364fb1fd --- /dev/null +++ b/engages-email-sender/src/debuggers.ts @@ -0,0 +1,13 @@ +import * as debug from 'debug'; + +export const debugInit = debug('erxes-engages:init'); +export const debugDb = debug('erxes-engages:db'); +export const debugBase = debug('erxes-engages:base'); +export const debugEngages = debug('erxes-engages:engages'); + +export const debugRequest = (debugInstance, req) => + debugInstance(` + Receiving ${req.path} request from ${req.headers.origin} + body: ${JSON.stringify(req.body || {})} + queryParams: ${JSON.stringify(req.query)} + `); diff --git a/engages-email-sender/src/index.ts b/engages-email-sender/src/index.ts new file mode 100644 index 000000000..a3179c56d --- /dev/null +++ b/engages-email-sender/src/index.ts @@ -0,0 +1,71 @@ +import * as bodyParser from 'body-parser'; +import * as dotenv from 'dotenv'; +import * as express from 'express'; +import { filterXSS } from 'xss'; +import configs from './api/configs'; +import deliveryReports from './api/deliveryReports'; +import telnyx from './api/telnyx'; + +// load environment variables +dotenv.config(); + +import { connect } from './connection'; +import { debugBase, debugInit } from './debuggers'; +import { initBroker } from './messageBroker'; +import { trackEngages } from './trackers/engageTracker'; + +export const app = express(); + +app.disable('x-powered-by'); + +trackEngages(app); + +// for health checking +app.get('/health', async (_req, res) => { + res.end('ok'); +}); + +app.use((req: any, _res, next) => { + req.rawBody = ''; + + req.on('data', chunk => { + req.rawBody += chunk.toString().replace(/\//g, '/'); + }); + + next(); +}); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// Insert routes below +app.use('/configs', configs); +app.use('/deliveryReports', deliveryReports); +app.use('/telnyx', telnyx); + +// Error handling middleware +app.use((error, _req, res, _next) => { + const msg = filterXSS(error.message); + + debugBase(`Error: `, msg); + res.status(500).send(msg); +}); + +const { MONGO_URL, NODE_ENV, PORT, TEST_MONGO_URL } = process.env; + +app.listen(PORT, () => { + let mongoUrl = MONGO_URL; + + if (NODE_ENV === 'test') { + mongoUrl = TEST_MONGO_URL; + } + + // connect to mongo database + connect(mongoUrl).then(async () => { + initBroker(app).catch(e => { + debugBase(`Error ocurred during message broker init ${e.message}`); + }); + }); + + debugInit(`Engages server is running on port ${PORT}`); +}); diff --git a/engages-email-sender/src/messageBroker.ts b/engages-email-sender/src/messageBroker.ts new file mode 100644 index 000000000..fb909c046 --- /dev/null +++ b/engages-email-sender/src/messageBroker.ts @@ -0,0 +1,44 @@ +import * as dotenv from 'dotenv'; +import messageBroker from 'erxes-message-broker'; +import { debugBase } from './debuggers'; +import { Logs } from './models'; +import { sendBulkSms, start } from './sender'; + +dotenv.config(); + +let client; + +export const initBroker = async server => { + client = await messageBroker({ + name: 'logger', + server, + envs: process.env, + }); + + const { consumeQueue } = client; + + // listen for rpc queue ========= + consumeQueue('erxes-api:engages-notification', async ({ action, data }) => { + debugBase(`Receiving queue data from erxes-api`, JSON.stringify(data)); + + if (action === 'sendEngage') { + await start(data); + } + + if (action === 'writeLog') { + await Logs.createLog(data.engageMessageId, 'regular', data.msg); + } + + if (action === 'sendEngageSms') { + await sendBulkSms(data); + } + }); +}; + +export const sendRPCMessage = async (message): Promise => { + return client.sendRPCMessage('rpc_queue:api_to_integrations', message); +}; + +export default function() { + return client; +} diff --git a/engages-email-sender/src/models/Configs.ts b/engages-email-sender/src/models/Configs.ts new file mode 100644 index 000000000..5d180939c --- /dev/null +++ b/engages-email-sender/src/models/Configs.ts @@ -0,0 +1,106 @@ +import { Document, Model, model, Schema } from 'mongoose'; +import { getValueAsString } from '../utils'; + +export interface IConfig { + code: string; + value: string; +} + +export interface ISESConfig { + accessKeyId: string; + region: string; + secretAccessKey: string; +} +export interface IConfigDocument extends IConfig, Document { + _id: string; +} + +export const configsSchema = new Schema({ + code: { type: String, label: 'Code', unique: true }, + value: { type: String, label: 'Value' }, +}); + +export interface IConfigModel extends Model { + getConfig(code: string): Promise; + updateConfigs(configsMap): Promise; + createOrUpdateConfig({ code, value }: IConfig): IConfigDocument; + getSESConfigs(): Promise; +} + +export const loadClass = () => { + class Config { + /** + * Get a Config + */ + public static async getConfig(code: string) { + const config = await Configs.findOne({ code }); + + if (!config) { + return { value: '' }; + } + + return config; + } + + /** + * Create or update config + */ + public static async createOrUpdateConfig({ code, value }: { code: string; value: string[] }) { + const obj = await Configs.findOne({ code }); + + if (obj) { + await Configs.updateOne({ _id: obj._id }, { $set: { value } }); + + return Configs.findOne({ _id: obj._id }); + } + + return Configs.create({ code, value }); + } + + /** + * Update configs + */ + public static async updateConfigs(configsMap) { + const codes = Object.keys(configsMap); + + for (const code of codes) { + if (!code) { + continue; + } + + const value = configsMap[code]; + const doc = { code, value }; + + await Configs.createOrUpdateConfig(doc); + } + } + + /** + * Get a Config + */ + public static async getSESConfigs() { + const accessKeyId = await getValueAsString('accessKeyId'); + const secretAccessKey = await getValueAsString('secretAccessKey'); + const region = await getValueAsString('region'); + const unverifiedEmailsLimit = await getValueAsString('unverifiedEmailsLimit'); + + return { + accessKeyId, + secretAccessKey, + region, + unverifiedEmailsLimit, + }; + } + } + + configsSchema.loadClass(Config); + + return configsSchema; +}; + +loadClass(); + +// tslint:disable-next-line +const Configs = model('configs', configsSchema); + +export default Configs; diff --git a/engages-email-sender/src/models/DeliveryReports.ts b/engages-email-sender/src/models/DeliveryReports.ts new file mode 100644 index 000000000..3353efa70 --- /dev/null +++ b/engages-email-sender/src/models/DeliveryReports.ts @@ -0,0 +1,114 @@ +import { Document, Model, model, Schema } from 'mongoose'; + +export interface IStats { + open: number; + click: number; + complaint: number; + delivery: number; + bounce: number; + reject: number; + send: number; + renderingfailure: number; + total: number; + engageMessageId: string; +} + +export interface IStatsDocument extends IStats, Document {} + +export const statsSchema = new Schema({ + engageMessageId: { type: String }, + createdAt: { type: Date, default: new Date() }, + open: { type: Number, default: 0 }, + click: { type: Number, default: 0 }, + complaint: { type: Number, default: 0 }, + delivery: { type: Number, default: 0 }, + bounce: { type: Number, default: 0 }, + reject: { type: Number, default: 0 }, + send: { type: Number, default: 0 }, + renderingfailure: { type: Number, default: 0 }, + total: { type: Number, default: 0 }, +}); + +export interface IDeliveryReports { + engageMessageId: string; + mailId: string; + status: string; + customerId: string; +} + +export interface IDeliveryReportsDocument extends IDeliveryReports, Document {} + +export const deliveryReportsSchema = new Schema({ + customerId: { type: String }, + mailId: { type: String, optional: true }, + status: { type: String, optional: true }, + engageMessageId: { type: String, optional: true }, + createdAt: { type: Date }, +}); + +export interface IStatsModel extends Model { + updateStats(engageMessageId: string, stat: string): Promise; +} + +export const loadStatsClass = () => { + class Stat { + /** + * Increase stat by 1 + */ + public static async updateStats(engageMessageId: string, stat: string) { + return Stats.updateOne({ engageMessageId }, { $inc: { [stat]: 1 } }); + } + } + + statsSchema.loadClass(Stat); + + return statsSchema; +}; + +loadStatsClass(); + +// tslint:disable-next-line +const Stats = model('engage_stats', statsSchema); + +export interface IDeliveryReportModel extends Model { + updateOrCreateReport(headers: any, status: string): Promise; +} + +export const loadDeliveryReportsClass = () => { + class DeliveryReport { + /** + * Change delivery report status + */ + public static async updateOrCreateReport(headers: any, status: string) { + const { engageMessageId, mailId, customerId } = headers; + + const deliveryReports = await DeliveryReports.findOne({ engageMessageId }); + + if (deliveryReports) { + await DeliveryReports.updateOne({ engageMessageId }, { $set: { mailId, status } }); + } else { + await DeliveryReports.create({ customerId, mailId, engageMessageId, status }); + } + + if (status === 'complaint' || status === 'bounce' || status === 'reject') { + return 'reject'; + } + + return true; + } + } + + deliveryReportsSchema.loadClass(DeliveryReport); + + return deliveryReportsSchema; +}; + +loadDeliveryReportsClass(); + +// tslint:disable-next-line +const DeliveryReports = model( + 'delivery_reports', + deliveryReportsSchema, +); + +export { Stats, DeliveryReports }; diff --git a/engages-email-sender/src/models/Logs.ts b/engages-email-sender/src/models/Logs.ts new file mode 100644 index 000000000..7e4cfff1a --- /dev/null +++ b/engages-email-sender/src/models/Logs.ts @@ -0,0 +1,42 @@ +import { Document, Model, model, Schema } from 'mongoose'; + +export type MessageType = 'regular' | 'success' | 'failure'; +export const LOG_MESSAGE_TYPES = ['regular', 'success', 'failure']; + +export interface ILog { + engageMessageId: string; + message: string; + type: MessageType; +} + +export interface ILogDocument extends ILog, Document {} + +export interface ILogModel extends Model { + createLog(engageMessageId: string, type: MessageType, message: string): Promise; +} + +export const logSchema = new Schema({ + createdAt: { type: Date, default: new Date(), label: 'Created at' }, + engageMessageId: { type: String, label: 'Engage message id' }, + message: { type: String, label: 'Message' }, + type: { type: String, label: 'Message type', enum: LOG_MESSAGE_TYPES }, +}); + +export const loadLogClass = () => { + class Log { + public static async createLog(engageMessageId: string, type: MessageType, message: string) { + return Logs.create({ engageMessageId, message, type }); + } + } + + logSchema.loadClass(Log); + + return logSchema; +}; + +loadLogClass(); + +// tslint:disable-next-line +const Logs = model('engage_logs', logSchema); + +export default Logs; diff --git a/engages-email-sender/src/models/SmsRequests.ts b/engages-email-sender/src/models/SmsRequests.ts new file mode 100644 index 000000000..0f2be763f --- /dev/null +++ b/engages-email-sender/src/models/SmsRequests.ts @@ -0,0 +1,78 @@ +import { Document, Model, model, Schema } from 'mongoose'; + +interface ISmsStatus { + date: Date; + status: string; +} + +export interface ISmsRequest { + engageMessageId?: string; + to?: string; + status?: string; + requestData?: string; + responseData?: string; + telnyxId?: string; + statusUpdates?: ISmsStatus[]; + errorMessages?: string[]; +} + +export interface ISmsRequestDocument extends ISmsRequest, Document {} + +export interface ISmsRequestModel extends Model { + createRequest(doc: ISmsRequest): Promise; + updateRequest(_id: string, doc: ISmsRequest): Promise; +} + +const statusSchema = new Schema( + { + date: { type: Date, label: 'Status update date' }, + status: { type: String, label: 'Sms delivery status' }, + }, + { _id: false }, +); + +const schema = new Schema({ + createdAt: { type: Date, default: new Date(), label: 'Created at' }, + engageMessageId: { type: String, label: 'Engage message id' }, + to: { type: String, label: 'Receiver phone number' }, + requestData: { type: String, label: 'Stringified request JSON' }, + // telnyx data + status: { type: String, label: 'Sms delivery status' }, + responseData: { type: String, label: 'Stringified response JSON' }, + telnyxId: { type: String, label: 'Telnyx message record id' }, + statusUpdates: { type: [statusSchema], label: 'Sms status updates' }, + errorMessages: { type: [String], label: 'Error messages' }, +}); + +export const loadLogClass = () => { + class SmsRequest { + public static async createRequest(doc: ISmsRequest) { + const { engageMessageId, to } = doc; + + const exists = await SmsRequests.findOne({ engageMessageId, to }); + + if (exists) { + throw new Error(`Sms request to "${to}" from engage id "${engageMessageId}" already exists.`); + } + + return SmsRequests.create(doc); + } + + public static async updateRequest(_id: string, doc: ISmsRequest) { + await SmsRequests.updateOne({ _id }, { $set: doc }); + + return SmsRequests.findOne({ _id }); + } + } + + schema.loadClass(SmsRequest); + + return schema; +}; + +loadLogClass(); + +// tslint:disable-next-line +const SmsRequests = model('engage_sms_requests', schema); + +export default SmsRequests; diff --git a/engages-email-sender/src/models/factories.ts b/engages-email-sender/src/models/factories.ts new file mode 100644 index 000000000..0462a0765 --- /dev/null +++ b/engages-email-sender/src/models/factories.ts @@ -0,0 +1,54 @@ +import * as faker from 'faker'; + +import { SmsRequests } from './index'; +import { ISmsRequest } from './SmsRequests'; + +interface ITelnyxWebhookData { + from?: string; + text?: string; + to?: string; + telnyxId?: string; +} + +export const telnyxWebhookDataFactory = (params: ITelnyxWebhookData) => ({ + data: { + event_type: faker.random.word(), + id: faker.random.uuid(), + occured_at: new Date().toISOString(), + record_type: faker.random.word(), + payload: { + completed_at: new Date().toISOString(), + direction: faker.random.word(), + encoding: faker.random.word(), + from: params.from || faker.phone.phoneNumber(), + id: params.telnyxId || faker.random.uuid(), + messaging_profile_id: faker.random.uuid(), + organization_id: faker.random.uuid(), + parts: faker.random.number(), + received_at: new Date().toISOString(), + record_type: faker.random.word(), + sent_at: new Date().toISOString(), + text: params.text || faker.random.word(), + to: [{ phone_number: params.to || faker.phone.phoneNumber(), status: faker.random.word() }], + type: faker.random.word(), + valid_until: new Date().toISOString(), + webhook_url: faker.internet.url(), + webhook_failover_url: faker.internet.url(), + }, + }, + meta: { + attempt: faker.random.number(), + delivered_to: faker.internet.url(), + }, +}); + +export const smsRequestFactory = async (params: ISmsRequest) => { + const smsRequest = new SmsRequests({ + engageMessageId: params.engageMessageId || faker.random.uuid(), + to: params.to || faker.phone.phoneNumber(), + requestData: params.requestData || '{}', + telnyxId: params.telnyxId || faker.random.uuid(), + }); + + return smsRequest.save(); +}; diff --git a/engages-email-sender/src/models/index.ts b/engages-email-sender/src/models/index.ts new file mode 100644 index 000000000..76a7f25b9 --- /dev/null +++ b/engages-email-sender/src/models/index.ts @@ -0,0 +1,6 @@ +import Configs from './Configs'; +import { DeliveryReports, Stats } from './DeliveryReports'; +import Logs from './Logs'; +import SmsRequests from './SmsRequests'; + +export { Configs, Stats, DeliveryReports, Logs, SmsRequests }; diff --git a/engages-email-sender/src/sender.ts b/engages-email-sender/src/sender.ts new file mode 100644 index 000000000..4058d2d27 --- /dev/null +++ b/engages-email-sender/src/sender.ts @@ -0,0 +1,261 @@ +import * as dotenv from 'dotenv'; +import * as Random from 'meteor-random'; +import { debugEngages } from './debuggers'; +import { Logs, SmsRequests, Stats } from './models'; +import { getTelnyxInfo } from './telnyxUtils'; +import { createTransporter, getConfigs, getEnv, ICustomer } from './utils'; + +dotenv.config(); + +interface IShortMessage { + content: string; + from?: string; + fromIntegrationId: string; +} + +interface IIntegration { + _id: string; + kind: string; + erxesApiId: string; + telnyxProfileId?: string; + telnyxPhoneNumber: string; +} + +interface IMessageParams { + shortMessage: IShortMessage; + to: string; + integrations: IIntegration[]; +} + +interface ITelnyxMessageParams { + from: string; + to: string; + text: string; + messaging_profile_id?: string; + webhook_url?: string; + webhook_failover_url?: string; +} + +interface ICallbackParams { + engageMessageId?: string; + msg: ITelnyxMessageParams; +} + +// alphanumeric sender id only works for countries outside north america +const isNumberNorthAmerican = (phoneNumber: string) => { + return phoneNumber.substring(0, 2) === '+1'; +}; + +// prepares sms object matching telnyx requirements +const prepareMessage = async ({ shortMessage, to, integrations }: IMessageParams): Promise => { + const MAIN_API_DOMAIN = getEnv({ name: 'MAIN_API_DOMAIN' }); + const { content, from, fromIntegrationId } = shortMessage; + + const integration = integrations.find(i => i.erxesApiId === fromIntegrationId); + + if (!integration.telnyxPhoneNumber) { + throw new Error('Telnyx phone is not configured'); + } + + const msg = { + from: integration.telnyxPhoneNumber, + to, + text: content, + messaging_profile_id: integration.telnyxProfileId || '', + webhook_url: `${MAIN_API_DOMAIN}/telnyx/webhook`, + webhook_failover_url: `${MAIN_API_DOMAIN}/telnyx/webhook-failover`, + }; + + // to use alphanumeric sender id, messaging profile id must be set + if (msg.messaging_profile_id && from) { + msg.from = from; + } + + if (isNumberNorthAmerican(msg.to)) { + msg.from = integration.telnyxPhoneNumber; + } + + return msg; +}; + +const handleMessageCallback = async (err: any, res: any, data: ICallbackParams) => { + const { engageMessageId, msg } = data; + + const request = await SmsRequests.createRequest({ + engageMessageId, + to: msg.to, + requestData: JSON.stringify(msg), + }); + + if (err) { + if (engageMessageId) { + await Logs.createLog(engageMessageId, 'failure', `${err.message} "${msg.to}"`); + } + + await SmsRequests.updateRequest(request._id, { + errorMessages: [err.message], + }); + } + + if (res && res.data && res.data.to) { + const receiver = res.data.to.find(item => item.phone_number === msg.to); + + if (engageMessageId) { + await Logs.createLog(engageMessageId, 'success', `Message successfully sent to "${msg.to}"`); + } + + await SmsRequests.updateRequest(request._id, { + status: receiver && receiver.status, + responseData: JSON.stringify(res.data), + telnyxId: res.data.id, + }); + } +}; + +export const start = async (data: { + fromEmail: string; + email: any; + engageMessageId: string; + customers: ICustomer[]; +}) => { + const { fromEmail, email, engageMessageId, customers } = data; + const { content, subject, attachments, sender, replyTo } = email; + + await Stats.findOneAndUpdate({ engageMessageId }, { engageMessageId }, { upsert: true }); + + const transporter = await createTransporter(); + + const sendEmail = async (customer: ICustomer) => { + const mailMessageId = Random.id(); + + let mailAttachment = []; + + if (attachments.length > 0) { + mailAttachment = attachments.map(file => { + return { + filename: file.name || '', + path: file.url || '', + }; + }); + } + + const MAIN_API_DOMAIN = getEnv({ name: 'MAIN_API_DOMAIN' }); + + const unSubscribeUrl = `${MAIN_API_DOMAIN}/unsubscribe/?cid=${customer._id}`; + + // replace customer attributes ===== + let replacedContent = content; + + if (customer.replacers) { + for (const replacer of customer.replacers) { + const regex = new RegExp(replacer.key, 'gi'); + replacedContent = replacedContent.replace(regex, replacer.value); + } + } + + replacedContent += `
If you want to use service like this click here to read more. Also you can opt out from our email subscription here.
© 2020 erxes inc Growth Marketing Platform
`; + + try { + await transporter.sendMail({ + from: `${sender || ''} <${fromEmail}>`, + to: customer.primaryEmail, + replyTo, + subject, + attachments: mailAttachment, + html: replacedContent, + headers: { + 'X-SES-CONFIGURATION-SET': 'erxes', + EngageMessageId: engageMessageId, + CustomerId: customer._id, + MailMessageId: mailMessageId, + }, + }); + const msg = `Sent email to: ${customer.primaryEmail}`; + debugEngages(msg); + await Logs.createLog(engageMessageId, 'success', msg); + } catch (e) { + debugEngages(e.message); + await Logs.createLog( + engageMessageId, + 'failure', + `Error occurred while sending email to ${customer.primaryEmail}: ${e.message}`, + ); + } + + await Stats.updateOne({ engageMessageId }, { $inc: { total: 1 } }); + }; + + const configs = await getConfigs(); + const unverifiedEmailsLimit = parseInt(configs.unverifiedEmailsLimit || '100', 10); + + let filteredCustomers = []; + let emails = []; + + if (customers.length > unverifiedEmailsLimit) { + await Logs.createLog( + engageMessageId, + 'regular', + `Unverified emails limit exceeced ${unverifiedEmailsLimit}. Customers who have unverified emails will be eliminated.`, + ); + + for (const customer of customers) { + if (customer.emailValidationStatus === 'valid') { + filteredCustomers.push(customer); + + emails.push(customer.primaryEmail); + } + } + } else { + filteredCustomers = customers; + emails = customers.map(customer => customer.primaryEmail); + } + + if (emails.length > 0) { + await Logs.createLog(engageMessageId, 'regular', `Preparing to send emails to ${emails.length}: ${emails}`); + } + + for (const customer of filteredCustomers) { + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + await sendEmail(customer); + } + + return true; +}; + +// sends bulk sms via engage message +export const sendBulkSms = async (data: { + engageMessageId: string; + shortMessage: IShortMessage; + customers: ICustomer[]; +}) => { + const { customers, engageMessageId, shortMessage } = data; + + const telnyxInfo = await getTelnyxInfo(); + + const filteredCustomers = customers.filter(c => c.primaryPhone && c.phoneValidationStatus === 'valid'); + + await Logs.createLog(engageMessageId, 'regular', `Preparing to send SMS to "${filteredCustomers.length}" customers`); + + for (const customer of filteredCustomers) { + await new Promise(resolve => { + setTimeout(resolve, 1000); + }); + + const msg = await prepareMessage({ + shortMessage, + to: customer.primaryPhone, + integrations: telnyxInfo.integrations, + }); + + try { + await telnyxInfo.instance.messages.create(msg, async (err: any, res: any) => { + await handleMessageCallback(err, res, { engageMessageId, msg }); + }); // end sms creation + } catch (e) { + await Logs.createLog(engageMessageId, 'failure', `${e.message} while sending to "${msg.to}"`); + } + } // end customers loop +}; // end sendBuklSms() diff --git a/engages-email-sender/src/telnyxUtils.ts b/engages-email-sender/src/telnyxUtils.ts new file mode 100644 index 000000000..a88f0b27d --- /dev/null +++ b/engages-email-sender/src/telnyxUtils.ts @@ -0,0 +1,66 @@ +import * as Telnyx from 'telnyx'; +import { SMS_DELIVERY_STATUSES } from './constants'; +import { sendRPCMessage } from './messageBroker'; +import SmsRequests from './models/SmsRequests'; + +// fetches telnyx config & integrations from erxes-integrations +export const getTelnyxInfo = async () => { + const response = await sendRPCMessage({ action: 'getTelnyxInfo' }); + + const { telnyxApiKey, integrations = [] } = response; + + if (!telnyxApiKey) { + throw new Error('Telnyx API key is not configured'); + } + + if (integrations.length < 1) { + throw new Error('No telnyx integrations configured'); + } + + return { + telnyxApiKey, + instance: new Telnyx(telnyxApiKey), + integrations, + }; +}; + +export const saveTelnyxHookData = async (data: any) => { + if (data && data.payload) { + const { to = [], id } = data.payload; + + const initialRequest = await SmsRequests.findOne({ telnyxId: id }); + + if (initialRequest) { + const receiver = to.find(item => item.phone_number === initialRequest.to); + + // prevent updates since sms is delivered + if (receiver && receiver.status !== SMS_DELIVERY_STATUSES.DELIVERED) { + const statuses = initialRequest.statusUpdates || []; + + statuses.push({ date: new Date(), status: receiver.status }); + + await SmsRequests.updateRequest(initialRequest._id, { + status: receiver.status, + responseData: JSON.stringify(data.payload), + statusUpdates: statuses, + }); + } + } + } +}; + +export const prepareSmsStats = async (engageMessageId: string) => { + const stats = await SmsRequests.aggregate([ + { $match: { engageMessageId } }, + { $group: { _id: '$status', count: { $sum: 1 } } }, + ]); + + const result: any = { total: 0 }; + + for (const s of stats) { + result[s._id] = s.count || 0; + result.total += s.count || 0; + } + + return result; +}; diff --git a/engages-email-sender/src/trackers/engageTracker.ts b/engages-email-sender/src/trackers/engageTracker.ts new file mode 100644 index 000000000..1852b7f9a --- /dev/null +++ b/engages-email-sender/src/trackers/engageTracker.ts @@ -0,0 +1,152 @@ +import * as AWS from 'aws-sdk'; +import { debugBase } from '../debuggers'; +import messageBroker from '../messageBroker'; +import { Configs, DeliveryReports, Stats } from '../models'; +import { ISESConfig } from '../models/Configs'; + +export const getApi = async (type: string): Promise => { + const config: ISESConfig = await Configs.getSESConfigs(); + + if (!config) { + return; + } + + AWS.config.update(config); + + if (type === 'ses') { + return new AWS.SES(); + } + + return new AWS.SNS(); +}; + +/* + * Receives notification from amazon simple notification service + * And updates engage message status and stats + */ +const handleMessage = async message => { + let parsedMessage; + + try { + parsedMessage = JSON.parse(message); + } catch (e) { + parsedMessage = message; + } + + const { eventType, mail } = parsedMessage; + const { headers } = mail; + + const engageMessageId = headers.find(header => header.name === 'Engagemessageid'); + + const mailId = headers.find(header => header.name === 'Mailmessageid'); + + const customerId = headers.find(header => header.name === 'Customerid'); + + const emailDeliveryId = headers.find(header => header.name === 'Emaildeliveryid'); + + const type = eventType.toLowerCase(); + + if (emailDeliveryId) { + return messageBroker().sendMessage('engagesNotification', { + action: 'transactionEmail', + data: { emailDeliveryId: emailDeliveryId.value, status: type }, + }); + } + + const mailHeaders = { + engageMessageId: engageMessageId.value, + mailId: mailId.value, + customerId: customerId.value, + }; + + await Stats.updateStats(mailHeaders.engageMessageId, type); + + const rejected = await DeliveryReports.updateOrCreateReport(mailHeaders, type); + + if (rejected === 'reject') { + await messageBroker().sendMessage('engagesNotification', { + action: 'setDoNotDisturb', + data: { customerId: mail.customerId }, + }); + } + + return true; +}; + +export const trackEngages = expressApp => { + expressApp.post(`/service/engage/tracker`, async (req, res) => { + const chunks: any = []; + + req.setEncoding('utf8'); + + req.on('data', chunk => { + chunks.push(chunk); + }); + + req.on('end', async () => { + const message = JSON.parse(chunks.join('')); + + debugBase('receiving on tracker:', message); + + const { Type = '', Message = {}, Token = '', TopicArn = '' } = message; + + if (Type === 'SubscriptionConfirmation') { + await getApi('sns').then(api => api.confirmSubscription({ Token, TopicArn }).promise()); + + return res.end('success'); + } + + if (Message === 'Successfully validated SNS topic for Amazon SES event publishing.') { + res.end('success'); + } + + await handleMessage(Message); + + return res.end('success'); + }); + }); +}; + +export const awsRequests = { + async getVerifiedEmails() { + const api = await getApi('ses'); + + return new Promise((resolve, reject) => { + api.listVerifiedEmailAddresses((error, data) => { + if (error) { + return reject(error); + } + + return resolve(data.VerifiedEmailAddresses); + }); + }); + }, + + async verifyEmail(email: string) { + const api = await getApi('ses'); + + return new Promise((resolve, reject) => { + api.verifyEmailAddress({ EmailAddress: email }, (error, data) => { + if (error) { + return reject(error); + } + + return resolve(data); + }); + }); + }, + + async removeVerifiedEmail(email: string) { + const api = await getApi('ses'); + + return new Promise((resolve, reject) => { + api.deleteVerifiedEmailAddress({ EmailAddress: email }, (error, data) => { + if (error) { + return reject(error); + } + + return resolve(data); + }); + }); + }, +}; diff --git a/engages-email-sender/src/utils.ts b/engages-email-sender/src/utils.ts new file mode 100644 index 000000000..3c2e110ce --- /dev/null +++ b/engages-email-sender/src/utils.ts @@ -0,0 +1,169 @@ +import * as AWS from 'aws-sdk'; +import * as nodemailer from 'nodemailer'; +import { debugBase } from './debuggers'; +import Configs, { ISESConfig } from './models/Configs'; +import { getApi } from './trackers/engageTracker'; + +export const createTransporter = async () => { + const config: ISESConfig = await Configs.getSESConfigs(); + + AWS.config.update(config); + + return nodemailer.createTransport({ + SES: new AWS.SES({ apiVersion: '2010-12-01' }), + }); +}; + +export interface ICustomer { + _id: string; + primaryEmail: string; + emailValidationStatus: string; + primaryPhone: string; + phoneValidationStatus: string; + replacers: Array<{ key: string; value: string }>; +} + +export interface IUser { + name: string; + position: string; + email: string; +} + +export const getEnv = ({ name, defaultValue }: { name: string; defaultValue?: string }): string => { + const value = process.env[name]; + + if (!value && typeof defaultValue !== 'undefined') { + return defaultValue; + } + + if (!value) { + debugBase(`Missing environment variable configuration for ${name}`); + } + + return value || ''; +}; + +export const subscribeEngage = () => { + return new Promise(async (resolve, reject) => { + const snsApi = await getApi('sns'); + const sesApi = await getApi('ses'); + const configSet = await getConfig('configSet', 'erxes'); + + const MAIN_API_DOMAIN = getEnv({ name: 'MAIN_API_DOMAIN' }); + + const topicArn = await snsApi + .createTopic({ Name: configSet }) + .promise() + .catch(e => { + return reject(e.message); + }); + + if (!topicArn) { + return reject('Error occured'); + } + + await snsApi + .subscribe({ + TopicArn: topicArn.TopicArn, + Protocol: 'https', + Endpoint: `${MAIN_API_DOMAIN}/service/engage/tracker`, + }) + .promise() + .then(response => { + debugBase(response); + }) + .catch(e => { + return reject(e.message); + }); + + await sesApi + .createConfigurationSet({ + ConfigurationSet: { + Name: configSet, + }, + }) + .promise() + .catch(e => { + if (e.message.includes('already exists')) { + return; + } + + return reject(e.message); + }); + + await sesApi + .createConfigurationSetEventDestination({ + ConfigurationSetName: configSet, + EventDestination: { + MatchingEventTypes: [ + 'send', + 'reject', + 'bounce', + 'complaint', + 'delivery', + 'open', + 'click', + 'renderingFailure', + ], + Name: configSet, + Enabled: true, + SNSDestination: { + TopicARN: topicArn.TopicArn, + }, + }, + }) + .promise() + .catch(e => { + if (e.message.includes('already exists')) { + return; + } + + return reject(e.message); + }); + + return resolve(true); + }); +}; + +export const getValueAsString = async name => { + const entry = await Configs.getConfig(name); + + if (entry.value) { + return entry.value.toString(); + } + + return entry.value; +}; + +export const updateConfigs = async (configsMap): Promise => { + const prevSESConfigs = await Configs.getSESConfigs(); + + await Configs.updateConfigs(configsMap); + + const updatedSESConfigs = await Configs.getSESConfigs(); + + if (JSON.stringify(prevSESConfigs) !== JSON.stringify(updatedSESConfigs)) { + await subscribeEngage(); + } +}; + +export const getConfigs = async (): Promise => { + const configsMap = {}; + const configs = await Configs.find({}); + + for (const config of configs) { + configsMap[config.code] = config.value; + } + + return configsMap; +}; + +export const getConfig = async (code, defaultValue?) => { + const configs = await getConfigs(); + + if (!configs[code]) { + return defaultValue; + } + + return configs[code]; +}; diff --git a/engages-email-sender/tsconfig.json b/engages-email-sender/tsconfig.json new file mode 100644 index 000000000..5d28d8b76 --- /dev/null +++ b/engages-email-sender/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "allowJs": true, + "target": "es6", + "moduleResolution": "node", + "module": "commonjs", + "lib": ["es2015", "es6", "es7", "esnext.asynciterable"], + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["./src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/engages-email-sender/tsconfig.prod.json b/engages-email-sender/tsconfig.prod.json new file mode 100644 index 000000000..fd8086ccf --- /dev/null +++ b/engages-email-sender/tsconfig.prod.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/__tests__"] +} diff --git a/engages-email-sender/yarn.lock b/engages-email-sender/yarn.lock new file mode 100644 index 000000000..c064153de --- /dev/null +++ b/engages-email-sender/yarn.lock @@ -0,0 +1,4870 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/core@^7.1.0": + version "7.8.7" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.7.tgz#b69017d221ccdeb203145ae9da269d72cf102f3b" + integrity sha512-rBlqF3Yko9cynC5CCFy6+K/w2N+Sq/ff2BPy+Krp7rHlABIr5epbA7OxVeKoMHB39LZOp1UY5SuLjy6uWi35yA== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.7" + "@babel/helpers" "^7.8.4" + "@babel/parser" "^7.8.7" + "@babel/template" "^7.8.6" + "@babel/traverse" "^7.8.6" + "@babel/types" "^7.8.7" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.8.6", "@babel/generator@^7.8.7": + version "7.8.8" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.8.tgz#cdcd58caab730834cee9eeadb729e833b625da3e" + integrity sha512-HKyUVu69cZoclptr8t8U5b6sx6zoWjh8jiUhnuj3MpZuKT2dJ8zPTuiy31luq32swhI0SpwItCIlU8XW7BZeJg== + dependencies: + "@babel/types" "^7.8.7" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" + integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" + +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" + integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== + +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helpers@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" + integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== + dependencies: + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.4" + "@babel/types" "^7.8.3" + +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.8.6", "@babel/parser@^7.8.7": + version "7.8.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.8.tgz#4c3b7ce36db37e0629be1f0d50a571d2f86f6cd4" + integrity sha512-mO5GWzBPsPf6865iIbzNE0AvkKF3NE+2S3eRUpE+FE07BOAkXh6G+GW/Pj01hhXjve1WScbaIO4UlY1JKeqCcA== + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/runtime@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.4", "@babel/traverse@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" + integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.6" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.8.7": + version "7.8.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.7.tgz#1fc9729e1acbb2337d5b6977a63979b4819f5d1d" + integrity sha512-k2TreEHxFA4CjGkL+GYjRyx35W0Mr7DP5+9q6WMkyKXB+904bYmG40syjMFV0oLlhhFCwWl0vA0DyzTDkwAiJw== + dependencies: + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== + dependencies: + exec-sh "^0.3.2" + minimist "^1.2.0" + +"@dashersw/axon@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@dashersw/axon/-/axon-2.0.5.tgz#708b8cd21a5c803de8dd517a9252828b007d77bb" + integrity sha512-e7az6UOh/1JqLvzg2GPhP3n47QMQal3Qg2a2497JwY7dlbSKUg4dQmnRyKWNjFz0FHjranUjKvX6J6NAV3Sm/Q== + dependencies: + amp "~0.3.1" + amp-message "~0.1.1" + configurable "0.0.1" + debug "*" + escape-regexp "0.0.1" + +"@dashersw/node-discover@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@dashersw/node-discover/-/node-discover-1.0.4.tgz#3fd2aad22228e0ecf72bb069e9f0e06ef4bd5b82" + integrity sha512-OblARM345ECaTSSFQcuWUl+7/uhOjhKBIA0G0CbOPbUzwF3cqBbl2R0E9tulnsLk3XB6Zpmja0TZIU5ClKF6LA== + dependencies: + redis "^2.7.1" + uuid "^3.3.2" + +"@jest/console@^24.7.1", "@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== + dependencies: + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" + +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" + +"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== + dependencies: + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@types/babel__core@^7.1.0": + version "7.1.6" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" + integrity sha512-tTnhWszAqvXnhW7m5jQU9PomXSiKXk2sFxpahXvI20SZKu9ylPi8WtIxueZ6ehDWikPT0jeFujMj3X4ZHuf3Tg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" + integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.9.tgz#be82fab304b141c3eee81a4ce3b034d0eba1590a" + integrity sha512-jEFQ8L1tuvPjOI8lnpaf73oCJe+aoxL6ygqSy6c8LcW98zaC+4mzWuQIRCEvKeCOu+lbqdXcg4Uqmm1S8AP1tw== + dependencies: + "@babel/types" "^7.3.0" + +"@types/body-parser@*", "@types/body-parser@^1.17.0": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bson@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.1.tgz#2bfc80819e7055b76d5496d5344ed23e5d12bbb2" + integrity sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw== + dependencies: + "@types/node" "*" + +"@types/connect@*": + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.4": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" + integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== + dependencies: + "@types/express" "*" + +"@types/dotenv@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67" + integrity sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf" + integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.16.0": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest@^24.0.23": + version "24.9.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" + integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q== + dependencies: + jest-diff "^24.3.0" + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/mongodb@*", "@types/mongodb@^3.1.2": + version "3.5.0" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.5.0.tgz#3876f0884dda1f73b31b76ea5978817b1e18adff" + integrity sha512-wHk5o4zM9Md1us5GcNw6dogf9Oyfu4SmIt/TCVWX4W+YetRu913LBF/Grf+pa2Uvb9UIXftyY8YWeDwyIvR7GQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/mongoose@^5.2.1": + version "5.7.3" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.7.3.tgz#2fc7f26cb0fda43d8ff46b5ab6ad13068370f268" + integrity sha512-kZR/hBOft/Nm6aFP/1k0aBrfaYZQBM8I7eynpiOdgON2GqzSTd0S1kSGLUkeDLrm5NLcJ6wbXyrbYRm/nWZvlA== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + +"@types/node@*": + version "13.7.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.7.tgz#1628e6461ba8cc9b53196dfeaeec7b07fa6eea99" + integrity sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg== + +"@types/node@^10.12.18": + version "10.17.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" + integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== + +"@types/q@^1.5.0": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + +"@types/yargs@^13.0.0": + version "13.0.8" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.8.tgz#a38c22def2f1c2068f8971acb3ea734eb3c64a99" + integrity sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA== + dependencies: + "@types/yargs-parser" "*" + +abab@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== + +accepts@~1.3.4, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +acorn-globals@^4.1.0: + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== + dependencies: + acorn "^6.0.1" + acorn-walk "^6.0.1" + +acorn-walk@^6.0.1: + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== + +acorn@^5.5.3: + version "5.7.4" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.4.tgz#3e8d8a9947d0599a1796d10225d7432f4a4acf5e" + integrity sha512-1D++VG7BhrtvQpNbBzovKNc1FLGGEE/oGe7b9xJm/RFHMBeUaUGpluV9RLjZa47YFdPcDAenEYuq9pQPcMdLJg== + +acorn@^6.0.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.1.tgz#531e58ba3f51b9dacb9a6646ca4debf5b14ca474" + integrity sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA== + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +ajv@^6.5.5: + version "6.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" + integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +amp-message@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/amp-message/-/amp-message-0.1.2.tgz#a78f1c98995087ad36192a41298e4db49e3dfc45" + integrity sha1-p48cmJlQh602GSpBKY5NtJ49/EU= + dependencies: + amp "0.3.1" + +amp@0.3.1, amp@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/amp/-/amp-0.3.1.tgz#6adf8d58a74f361e82c1fa8d389c079e139fc47d" + integrity sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0= + +amqplib@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.5.5.tgz#698f0cb577e0591954a90572fcb3b8998a76fd40" + integrity sha512-sWx1hbfHbyKMw6bXOK2k6+lHL8TESWxjAx5hG8fBtT7wcxoXNIsFxZMnFyBjxt3yL14vn7WqBDe5U6BGOadtLg== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +amqplib@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.6.0.tgz#87857c7c95d56d22438ced4cf1f7e5f0dc43b309" + integrity sha512-zXCh4jQ77TBZe1YtvZ1n7sUxnTjnNagpy8MVi2yc1ive239pS3iLwm4e4d5o4XZGx1BdTKQ/U0ZmaDU3c8MxYQ== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.0.0, ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== + dependencies: + micromatch "^3.1.4" + normalize-path "^2.1.1" + +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +atob@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== + +aws-sdk@^2.493.0: + version "2.631.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.631.0.tgz#31f5327ea9afb55f1e60ba594727fc16a04e8cf0" + integrity sha512-oGYe21pc8b1rxJFZMETZIhY4e/QBJv8Keq4RCef3D/GNXR+oxDQsEDk9KchbkW0WJDxiglj3LnZzPYUhISnBbw== + dependencies: + buffer "4.9.1" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== + dependencies: + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" + +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" + +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== + dependencies: + "@types/babel__traverse" "^7.0.6" + +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64-js@^1.0.2: + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64id@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +base@^0.11.1: + version "0.11.2" + resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== + dependencies: + cache-base "^1.0.1" + class-utils "^0.3.5" + component-emitter "^1.2.1" + define-property "^1.0.0" + isobject "^3.0.1" + mixin-deep "^1.2.0" + pascalcase "^0.1.1" + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bitsyntax@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" + integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== + dependencies: + buffer-more-ints "~1.0.0" + debug "~2.6.9" + safe-buffer "~5.1.2" + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bluebird@^3.5.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.0, body-parser@^1.17.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^2.3.1: + version "2.3.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== + dependencies: + arr-flatten "^1.1.0" + array-unique "^0.3.2" + extend-shallow "^2.0.1" + fill-range "^4.0.0" + isobject "^3.0.1" + repeat-element "^1.1.2" + snapdragon "^0.8.1" + snapdragon-node "^2.0.1" + split-string "^3.0.2" + to-regex "^3.0.1" + +browser-process-hrtime@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz#3c9b4b7d782c8121e56f10106d84c0d0ffc94626" + integrity sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow== + +browser-resolve@^1.11.3: + version "1.11.3" + resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== + dependencies: + resolve "1.1.7" + +bs-logger@0.x: + version "0.2.6" + resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" + integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== + dependencies: + fast-json-stable-stringify "2.x" + +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== + dependencies: + node-int64 "^0.4.0" + +bson@^1.1.1, bson@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== + +buffer-from@1.x, buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== + +buffer@4.9.1: + version "4.9.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + isarray "^1.0.0" + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +cache-base@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== + dependencies: + collection-visit "^1.0.0" + component-emitter "^1.2.1" + get-value "^2.0.6" + has-value "^1.0.0" + isobject "^3.0.1" + set-value "^2.0.0" + to-object-path "^0.3.0" + union-value "^1.0.0" + unset-value "^1.0.0" + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +camelcase@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= + +camelcase@^5.0.0, camelcase@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== + dependencies: + rsvp "^4.8.4" + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +charm@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + dependencies: + inherits "^2.0.1" + +ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + +class-utils@^0.3.5: + version "0.3.6" + resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== + dependencies: + arr-union "^3.1.0" + define-property "^0.2.5" + isobject "^3.0.0" + static-extend "^0.1.1" + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +co@^4.6.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= + +collection-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= + dependencies: + map-visit "^1.0.0" + object-visit "^1.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@^1.2.1, component-emitter@^1.3.0, component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +configurable@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/configurable/-/configurable-0.0.1.tgz#47d75b727b51b4eb84c1dadafe3f8240313833b1" + integrity sha1-R9dbcntRtOuEwdra/j+CQDE4M7E= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +convert-source-map@^1.4.0, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== + dependencies: + safe-buffer "~5.1.1" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== + +copy-descriptor@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= + +core-util-is@1.0.2, core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cote@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cote/-/cote-1.0.0.tgz#ca1cf6ffc3064504e8605e1a933e779e12e1328e" + integrity sha512-O9L5bCnA556UHbdGS0O+D7hhf6yxRon6igbl18ADgMnsCd8P9pVfvuvgO8X/Uch5Mj+xXRoVtkWYgddwNc74eg== + dependencies: + "@dashersw/axon" "2.0.5" + "@dashersw/node-discover" "^1.0.4" + charm "1.0.2" + colors "1.4.0" + eventemitter2 "6.0.0" + lodash "^4.17.15" + portfinder "1.0.25" + socket.io "^2.3.0" + uuid "^3.3.3" + +cross-env@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" + integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^6.0.0: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0" + integrity sha1-RwqBuGvkxe4XrMggeh9TFa4g27A= + +cssfilter@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= + +cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== + +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== + dependencies: + cssom "0.3.x" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +data-urls@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== + dependencies: + abab "^2.0.0" + whatwg-mimetype "^2.2.0" + whatwg-url "^7.0.0" + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debounce@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@*, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.1: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.2, decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decode-uri-component@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +define-property@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= + dependencies: + is-descriptor "^0.1.0" + +define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= + dependencies: + is-descriptor "^1.0.0" + +define-property@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== + dependencies: + is-descriptor "^1.0.2" + isobject "^3.0.1" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= + +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + +domexception@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== + dependencies: + webidl-conversions "^4.0.2" + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= + +double-ended-queue@^2.1.0-0: + version "2.1.0-0" + resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" + integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= + +dynamic-dedupe@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= + dependencies: + xtend "^4.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +end-of-stream@^1.1.0: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== + dependencies: + once "^1.4.0" + +engine.io-client@~3.4.0: + version "3.4.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" + integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== + dependencies: + component-emitter "~1.3.0" + component-inherit "0.0.3" + debug "~4.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" + integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c" + integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg== + dependencies: + accepts "~1.3.4" + base64id "2.0.0" + cookie "0.3.1" + debug "~4.1.0" + engine.io-parser "~2.2.0" + ws "^7.1.2" + +error-ex@^1.2.0, error-ex@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +erxes-message-broker@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/erxes-message-broker/-/erxes-message-broker-1.0.17.tgz#2407312163d010f292e153cf1b45e33e0fb16f63" + integrity sha512-UdA5nRLn1tn5pqnDM+hR6LTrkiiOduVBanvAjxlvZujv6bUP+SdKzf17VWQhk4/3jZMmWzHIBZ0vpzHbHaVQTg== + dependencies: + "@babel/runtime" "^7.11.2" + amqplib "^0.6.0" + cote "^1.0.0" + cross-env "^7.0.2" + debug "^4.1.1" + requestify "^0.2.5" + uuid "^8.3.0" + +es-abstract@^1.17.0-next.1, es-abstract@^1.17.2: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-regexp@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254" + integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ= + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escodegen@^1.9.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== + dependencies: + esprima "^4.0.1" + estraverse "^4.2.0" + esutils "^2.0.2" + optionator "^0.8.1" + optionalDependencies: + source-map "~0.6.1" + +esprima@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter2@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.0.0.tgz#218eb512c3603c5341724b6af7b686a1aa5ab8f5" + integrity sha512-ZuNWHD7S7IoikyEmx35vPU8H1W0L+oi644+4mSTg7nwXvBQpIwQL7DPjYUF0VMB0jPkNMo3MqD07E7MYrkFmjQ== + +events@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= + +exec-sh@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== + +execa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== + dependencies: + cross-spawn "^6.0.0" + get-stream "^4.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= + +expand-brackets@^2.1.4: + version "2.1.4" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= + dependencies: + debug "^2.3.3" + define-property "^0.2.5" + extend-shallow "^2.0.1" + posix-character-classes "^0.1.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + +express@^4.16.4: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= + dependencies: + is-extendable "^0.1.0" + +extend-shallow@^3.0.0, extend-shallow@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= + dependencies: + assign-symbols "^1.0.0" + is-extendable "^1.0.1" + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extglob@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== + dependencies: + array-unique "^0.3.2" + define-property "^1.0.0" + expand-brackets "^2.1.4" + extend-shallow "^2.0.1" + fragment-cache "^0.2.1" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +faker@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" + integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== + +fb-watchman@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== + dependencies: + bser "2.1.1" + +file-uri-to-path@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" + integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== + +filewatcher@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= + dependencies: + debounce "^1.0.0" + +fill-range@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= + dependencies: + extend-shallow "^2.0.1" + is-number "^3.0.0" + repeat-string "^1.6.1" + to-regex-range "^2.1.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +for-in@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fragment-cache@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= + dependencies: + map-cache "^0.2.2" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@^1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== + dependencies: + bindings "^1.5.0" + nan "^2.12.1" + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stream@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" + integrity sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w== + dependencies: + pump "^3.0.0" + +get-value@^2.0.3, get-value@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has-value@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= + dependencies: + get-value "^2.0.3" + has-values "^0.1.4" + isobject "^2.0.0" + +has-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= + dependencies: + get-value "^2.0.6" + has-values "^1.0.0" + isobject "^3.0.0" + +has-values@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= + +has-values@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= + dependencies: + is-number "^3.0.0" + kind-of "^4.0.0" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +html-encoding-sniffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== + dependencies: + whatwg-encoding "^1.0.1" + +html-escaper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" + integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@1.1.13, ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== + +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +is-accessor-descriptor@^0.1.6: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= + dependencies: + kind-of "^3.0.2" + +is-accessor-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== + dependencies: + kind-of "^6.0.0" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + +is-ci@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" + integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== + dependencies: + ci-info "^2.0.0" + +is-data-descriptor@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= + dependencies: + kind-of "^3.0.2" + +is-data-descriptor@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== + dependencies: + kind-of "^6.0.0" + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-descriptor@^0.1.0: + version "0.1.6" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== + dependencies: + is-accessor-descriptor "^0.1.6" + is-data-descriptor "^0.1.4" + kind-of "^5.0.0" + +is-descriptor@^1.0.0, is-descriptor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== + dependencies: + is-accessor-descriptor "^1.0.0" + is-data-descriptor "^1.0.0" + kind-of "^6.0.2" + +is-extendable@^0.1.0, is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= + +is-extendable@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== + dependencies: + is-plain-object "^2.0.4" + +is-finite@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== + +is-number@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= + dependencies: + kind-of "^3.0.2" + +is-plain-object@^2.0.3, is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + +is-stream@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-windows@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@1.0.0, isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= + dependencies: + isarray "1.0.0" + +isobject@^3.0.0, isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== + +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== + dependencies: + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" + +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== + dependencies: + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" + +istanbul-reports@^2.2.6: + version "2.2.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" + integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== + dependencies: + html-escaper "^2.0.0" + +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== + dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" + throat "^4.0.0" + +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== + dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" + +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" + chalk "^2.0.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" + +jest-diff@^24.3.0, jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" + integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== + dependencies: + detect-newline "^2.1.0" + +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== + dependencies: + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" + +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + +jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== + +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== + dependencies: + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" + +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== + dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== + dependencies: + chalk "^2.0.1" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" + +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== + +jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== + +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== + dependencies: + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" + +jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== + dependencies: + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" + chalk "^2.0.1" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" + +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== + +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== + dependencies: + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" + chalk "^2.0.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" + +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0, jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== + dependencies: + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" + +jmespath@0.15.0: + version "0.15.0" + resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= + +jquery@^3.1.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +jsdom@^11.5.1: + version "11.12.0" + resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== + dependencies: + abab "^2.0.0" + acorn "^5.5.3" + acorn-globals "^4.1.0" + array-equal "^1.0.0" + cssom ">= 0.3.2 < 0.4.0" + cssstyle "^1.0.0" + data-urls "^1.0.0" + domexception "^1.0.1" + escodegen "^1.9.1" + html-encoding-sniffer "^1.0.2" + left-pad "^1.3.0" + nwsapi "^2.0.7" + parse5 "4.0.0" + pn "^1.1.0" + request "^2.87.0" + request-promise-native "^1.0.5" + sax "^1.2.4" + symbol-tree "^3.2.2" + tough-cookie "^2.3.4" + w3c-hr-time "^1.0.1" + webidl-conversions "^4.0.2" + whatwg-encoding "^1.0.3" + whatwg-mimetype "^2.1.0" + whatwg-url "^6.4.1" + ws "^5.2.0" + xml-name-validator "^3.0.0" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-parse-better-errors@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" + integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json5@2.x, json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +kareem@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" + integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== + +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= + dependencies: + is-buffer "^1.1.5" + +kind-of@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= + dependencies: + is-buffer "^1.1.5" + +kind-of@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== + +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== + +left-pad@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +load-json-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" + integrity sha1-L19Fq5HjMhYjT9U62rZo607AmTs= + dependencies: + graceful-fs "^4.1.2" + parse-json "^4.0.0" + pify "^3.0.0" + strip-bom "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash.isplainobject@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= + +lodash.memoize@4.x: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= + +lodash@^4.17.13, lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lodash@^4.17.14: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-error@1.x, make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + +makeerror@1.0.x: + version "1.0.11" + resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= + dependencies: + tmpl "1.0.x" + +map-cache@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +map-visit@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= + dependencies: + object-visit "^1.0.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +meteor-random@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/meteor-random/-/meteor-random-0.0.3.tgz#0d1489ecdb9bcb58bb52decebfbceddf54473a68" + integrity sha1-DRSJ7Nuby1i7Ut7Ov7zt31RHOmg= + dependencies: + crypto "0.0.3" + +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +micromatch@^3.1.10, micromatch@^3.1.4: + version "3.1.10" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + braces "^2.3.1" + define-property "^2.0.2" + extend-shallow "^3.0.2" + extglob "^2.0.4" + fragment-cache "^0.2.1" + kind-of "^6.0.2" + nanomatch "^1.2.9" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.2" + +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.1: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +minimist@^1.1.3, minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +mixin-deep@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== + dependencies: + for-in "^1.0.2" + is-extendable "^1.0.1" + +mkdirp@0.x, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mongodb@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.2.tgz#ff086b5f552cf07e24ce098694210f3d42d668b2" + integrity sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.7.5.tgz#b787b47216edf62036aa358c3ef0f1869c46cdc2" + integrity sha512-BZ4FxtnbTurc/wcm/hLltLdI4IDxo4nsE0D9q58YymTdZwreNzwO62CcjVtaHhmr8HmJtOInp2W/T12FZaMf8g== + dependencies: + bson "~1.1.1" + kareem "2.3.1" + mongodb "3.3.2" + mongoose-legacy-pluralize "1.0.2" + mpath "0.6.0" + mquery "3.2.2" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.1.2" + sift "7.0.1" + sliced "1.0.1" + +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== + +mquery@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" + integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== + +nanomatch@^1.2.9: + version "1.2.13" + resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== + dependencies: + arr-diff "^4.0.0" + array-unique "^0.3.2" + define-property "^2.0.2" + extend-shallow "^3.0.2" + fragment-cache "^0.2.1" + is-windows "^1.0.2" + kind-of "^6.0.2" + object.pick "^1.3.0" + regex-not "^1.0.0" + snapdragon "^0.8.1" + to-regex "^3.0.1" + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-int64@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= + +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= + +node-notifier@^5.4.0, node-notifier@^5.4.2: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +nodemailer@^6.2.1: + version "6.4.4" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.4.4.tgz#f4bb26a833786e8908b3ac8afbf2d0382ac24feb" + integrity sha512-2GqGu5o3FBmDibczU3+LZh9lCEiKmNx7LvHl512p8Kj+Kn5FQVOICZv85MDFz/erK0BDd5EJp3nqQLpWCZD1Gg== + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +normalize-path@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= + dependencies: + remove-trailing-separator "^1.0.1" + +npm-run-path@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= + dependencies: + path-key "^2.0.0" + +nwsapi@^2.0.7: + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + +object-copy@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= + dependencies: + copy-descriptor "^0.1.0" + define-property "^0.2.5" + kind-of "^3.0.3" + +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object-visit@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= + dependencies: + isobject "^3.0.0" + +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +object.pick@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= + dependencies: + isobject "^3.0.1" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +once@^1.3.0, once@^1.3.1, once@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +optionator@^0.8.1: + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.6" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + word-wrap "~1.2.3" + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" + +p-finally@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-limit@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parse-json@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-4.0.0.tgz#be35f5425be1f7f6c747184f98a788cb99477ee0" + integrity sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA= + dependencies: + error-ex "^1.3.1" + json-parse-better-errors "^1.0.1" + +parse5@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +pascalcase@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.0, path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +path-type@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== + dependencies: + pify "^3.0.0" + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pify@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= + +pify@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" + +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== + dependencies: + find-up "^3.0.0" + +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== + +portfinder@1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== + dependencies: + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" + ansi-styles "^3.2.0" + react-is "^16.8.4" + +prompts@^2.0.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.1.tgz#b63a9ce2809f106fa9ae1277c275b167af46ea05" + integrity sha512-qIP2lQyCwYbdzcqHIUi2HAxiWixhoM9OdLCWf8txXsapC/X9YdsCoeyRIXE/GP+Q0J37Q7+XN/MFqbUa7IzXNA== + dependencies: + kleur "^3.0.3" + sisteransi "^1.0.4" + +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.1" + +psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== + +pump@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" + integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== + dependencies: + end-of-stream "^1.1.0" + once "^1.3.1" + +punycode@1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@^0.9.7: + version "0.9.7" + resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" + integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@^6.6.0, qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +react-is@^16.8.4: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" + integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +read-pkg@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" + integrity sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k= + dependencies: + load-json-file "^4.0.0" + normalize-package-data "^2.3.2" + path-type "^3.0.0" + +"readable-stream@1.x >=1.1.9": + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redis-commands@^1.2.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-parser@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" + integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= + +redis@^2.7.1, redis@^2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" + integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.6.0" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regex-not@^1.0.0, regex-not@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== + dependencies: + extend-shallow "^3.0.2" + safe-regex "^1.1.0" + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= + +repeat-element@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== + +repeat-string@^1.6.1: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + +request-promise-native@^1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== + dependencies: + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.87.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + aws-sign2 "~0.7.0" + aws4 "^1.8.0" + caseless "~0.12.0" + combined-stream "~1.0.6" + extend "~3.0.2" + forever-agent "~0.6.1" + form-data "~2.3.2" + har-validator "~5.1.3" + http-signature "~1.2.0" + is-typedarray "~1.0.0" + isstream "~0.1.2" + json-stringify-safe "~5.0.1" + mime-types "~2.1.19" + oauth-sign "~0.9.0" + performance-now "^2.1.0" + qs "~6.5.2" + safe-buffer "^5.1.2" + tough-cookie "~2.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +requestify@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/requestify/-/requestify-0.2.5.tgz#80249f1ca7dfdf79fa2a6048aeac37d43e23c905" + integrity sha1-gCSfHKff33n6KmBIrqw31D4jyQU= + dependencies: + jquery "^3.1.0" + q "^0.9.7" + underscore "^1.8.3" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" + integrity sha1-six699nWiBvItuZTM17rywoYh0g= + +resolve-url@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= + +resolve@1.1.7: + version "1.1.7" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= + +resolve@1.x, resolve@^1.0.0, resolve@^1.10.0, resolve@^1.3.2: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== + dependencies: + path-parse "^1.0.6" + +ret@~0.1.10: + version "0.1.15" + resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== + +rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== + +safe-buffer@5.1.2, safe-buffer@~5.1.1, safe-buffer@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safe-buffer@^5.1.1, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-regex@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= + dependencies: + ret "~0.1.10" + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== + dependencies: + "@cnakazawa/watch" "^1.0.3" + anymatch "^2.0.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" + fb-watchman "^2.0.0" + micromatch "^3.1.4" + minimist "^1.1.1" + walker "~1.0.5" + +sax@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= + +sax@>=0.6.0, sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.4.1, semver@^5.5, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.0.0, semver@^6.2.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== + dependencies: + extend-shallow "^2.0.1" + is-extendable "^0.1.1" + is-plain-object "^2.0.3" + split-string "^3.0.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0, signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +sisteransi@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.4.tgz#386713f1ef688c7c0304dc4c0632898941cad2e3" + integrity sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig== + +slash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" + integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +snapdragon-node@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== + dependencies: + define-property "^1.0.0" + isobject "^3.0.0" + snapdragon-util "^3.0.1" + +snapdragon-util@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== + dependencies: + kind-of "^3.2.0" + +snapdragon@^0.8.1: + version "0.8.2" + resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== + dependencies: + base "^0.11.1" + debug "^2.2.0" + define-property "^0.2.5" + extend-shallow "^2.0.1" + map-cache "^0.2.2" + source-map "^0.5.6" + source-map-resolve "^0.5.0" + use "^3.1.0" + +socket.io-adapter@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" + integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== + +socket.io-client@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + +socket.io-parser@~3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a" + integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A== + dependencies: + component-emitter "1.2.1" + debug "~4.1.0" + isarray "2.0.1" + +socket.io@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" + integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== + dependencies: + debug "~4.1.0" + engine.io "~3.4.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.3.0" + socket.io-parser "~3.4.0" + +source-map-resolve@^0.5.0: + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== + dependencies: + atob "^2.1.2" + decode-uri-component "^0.2.0" + resolve-url "^0.2.1" + source-map-url "^0.4.0" + urix "^0.1.0" + +source-map-support@^0.5.12, source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map-url@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= + +source-map@^0.5.0, source-map@^0.5.6: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= + +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +split-string@^3.0.1, split-string@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== + dependencies: + extend-shallow "^3.0.0" + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + asn1 "~0.2.3" + assert-plus "^1.0.0" + bcrypt-pbkdf "^1.0.0" + dashdash "^1.12.0" + ecc-jsbn "~0.1.1" + getpass "^0.1.1" + jsbn "~0.1.0" + safer-buffer "^2.0.2" + tweetnacl "~0.14.0" + +stack-utils@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== + +static-extend@^0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= + dependencies: + define-property "^0.2.5" + object-copy "^0.1.0" + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +string-length@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= + dependencies: + astral-regex "^1.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-eof@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +superagent@6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== + dependencies: + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-5.0.0.tgz#771aedfeb0a95466cc5d100d5d11288736fd25da" + integrity sha512-2JAWpPrUOZF4hHH5ZTCN2xjKXvJS3AEwPNXl0HUseHsfcXFvMy9kcsufIHCNAmQ5hlGCvgeAqaR5PBEouN3hlQ== + dependencies: + methods "1.1.2" + superagent "6.1.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + dependencies: + has-flag "^3.0.0" + +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== + +telnyx@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/telnyx/-/telnyx-1.7.2.tgz#8e004dbba68dfb5847fa842a0a1f73f88f1a9cc0" + integrity sha512-tmA3Pl+8tIsFCTOnRpqYYA90P3TiNSB3+L9cTYMwV6SEkA04PzcZkJfMPxbZ6n5bdbngEm6E8IE04a9TW2Va0A== + dependencies: + lodash.isplainobject "^4.0.6" + qs "^6.6.0" + safe-buffer "^5.1.1" + tweetnacl "^1.0.1" + uuid "^3.3.2" + +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== + dependencies: + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +throat@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= + +tmpl@1.0.x: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= + +to-object-path@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= + dependencies: + kind-of "^3.0.2" + +to-regex-range@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= + dependencies: + is-number "^3.0.0" + repeat-string "^1.6.1" + +to-regex@^3.0.1, to-regex@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== + dependencies: + define-property "^2.0.2" + extend-shallow "^3.0.2" + regex-not "^1.0.2" + safe-regex "^1.1.0" + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= + dependencies: + punycode "^2.1.0" + +tree-kill@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +ts-jest@^24.2.0: + version "24.3.0" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-24.3.0.tgz#b97814e3eab359ea840a1ac112deae68aa440869" + integrity sha512-Hb94C/+QRIgjVZlJyiWwouYUF+siNJHJHknyspaOcZ+OQAIdFG/UrdQVXw/0B8Z3No34xkUXZJpOTy9alOWdVQ== + dependencies: + bs-logger "0.x" + buffer-from "1.x" + fast-json-stable-stringify "2.x" + json5 "2.x" + lodash.memoize "4.x" + make-error "1.x" + mkdirp "0.x" + resolve "1.x" + semver "^5.5" + yargs-parser "10.x" + +ts-node-dev@^1.0.0-pre.32: + version "1.0.0-pre.44" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.44.tgz#2f4d666088481fb9c4e4f5bc8f15995bd8b06ecb" + integrity sha512-M5ZwvB6FU3jtc70i5lFth86/6Qj5XR5nMMBwVxZF4cZhpO7XcbWw6tbNiJo22Zx0KfjEj9py5DANhwLOkPPufw== + dependencies: + dateformat "~1.0.4-1.2.3" + dynamic-dedupe "^0.3.0" + filewatcher "~3.0.0" + minimist "^1.1.3" + mkdirp "^0.5.1" + node-notifier "^5.4.0" + resolve "^1.0.0" + rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" + ts-node "*" + tsconfig "^7.0.0" + +ts-node@*: + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "3.1.1" + +ts-node@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641" + integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA== + dependencies: + arg "^4.1.0" + diff "^3.1.0" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +tweetnacl@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^3.7.2: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +underscore@^1.8.3: + version "1.9.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f" + integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ== + +union-value@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== + dependencies: + arr-union "^3.1.0" + get-value "^2.0.6" + is-extendable "^0.1.1" + set-value "^2.0.1" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +unset-value@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= + dependencies: + has-value "^0.3.1" + isobject "^3.0.0" + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +urix@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= + +url-parse@~1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +url@0.10.3: + version "0.10.3" + resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= + dependencies: + punycode "1.3.2" + querystring "0.2.0" + +use@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +util.promisify@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +w3c-hr-time@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" + integrity sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ== + dependencies: + browser-process-hrtime "^1.0.0" + +walker@^1.0.7, walker@~1.0.5: + version "1.0.7" + resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= + dependencies: + makeerror "1.0.x" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: + version "1.0.5" + resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== + dependencies: + iconv-lite "0.4.24" + +whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== + +whatwg-url@^6.4.1: + version "6.5.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9, which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== + dependencies: + graceful-fs "^4.1.11" + imurmurhash "^0.1.4" + signal-exit "^3.0.2" + +ws@^5.2.0: + version "5.2.2" + resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== + dependencies: + async-limiter "~1.0.0" + +ws@^7.1.2: + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + +xml-name-validator@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== + +xml2js@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== + dependencies: + sax ">=0.6.0" + xmlbuilder "~9.0.1" + +xmlbuilder@~9.0.1: + version "9.0.7" + resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= + +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + +xss@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51" + integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A== + dependencies: + commander "^2.9.0" + cssfilter "0.0.10" + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yargs-parser@10.x: + version "10.1.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" + integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== + dependencies: + camelcase "^4.1.0" + +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^13.3.0: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@3.1.1, yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/initialData/activity_logs.bson b/initialData/activity_logs.bson deleted file mode 100644 index 8a0ce8d08..000000000 Binary files a/initialData/activity_logs.bson and /dev/null differ diff --git a/initialData/activity_logs.metadata.json b/initialData/activity_logs.metadata.json deleted file mode 100644 index 1b1c5283f..000000000 --- a/initialData/activity_logs.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.activity_logs"}],"uuid":"475cbddb84ce4a8b90d3cfad816f01db"} \ No newline at end of file diff --git a/initialData/brands.bson b/initialData/brands.bson deleted file mode 100644 index d5b25f6b4..000000000 Binary files a/initialData/brands.bson and /dev/null differ diff --git a/initialData/brands.metadata.json b/initialData/brands.metadata.json deleted file mode 100644 index 256dd48ba..000000000 --- a/initialData/brands.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.brands"}],"uuid":"f2489825947d47918b5c80dafc344e48"} \ No newline at end of file diff --git a/initialData/channels.metadata.json b/initialData/channels.metadata.json deleted file mode 100644 index 947d963b2..000000000 --- a/initialData/channels.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.channels"}],"uuid":"c791f01cfe5f40e3991a47f391861e6a"} \ No newline at end of file diff --git a/initialData/companies.bson b/initialData/companies.bson deleted file mode 100644 index 9b01fe2bd..000000000 Binary files a/initialData/companies.bson and /dev/null differ diff --git a/initialData/companies.metadata.json b/initialData/companies.metadata.json deleted file mode 100644 index 4870a43fe..000000000 --- a/initialData/companies.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.companies"}],"uuid":"21e6f9d86b414f539bbde07758f6add2"} \ No newline at end of file diff --git a/initialData/configs.bson b/initialData/configs.bson deleted file mode 100644 index f048f0f21..000000000 Binary files a/initialData/configs.bson and /dev/null differ diff --git a/initialData/configs.metadata.json b/initialData/configs.metadata.json deleted file mode 100644 index 165763076..000000000 --- a/initialData/configs.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.configs"}],"uuid":"5f2010fddb0b43c3a9cd5065b4e1dfe4"} \ No newline at end of file diff --git a/initialData/conversation_messages.bson b/initialData/conversation_messages.bson deleted file mode 100644 index e01b1eaf4..000000000 Binary files a/initialData/conversation_messages.bson and /dev/null differ diff --git a/initialData/conversation_messages.metadata.json b/initialData/conversation_messages.metadata.json deleted file mode 100644 index 352c1d2af..000000000 --- a/initialData/conversation_messages.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.conversation_messages"},{"v":2,"key":{"conversationId":1},"name":"conversationId_1","ns":"erxes.conversation_messages","background":true},{"v":2,"key":{"createdAt":1},"name":"createdAt_1","ns":"erxes.conversation_messages","background":true}],"uuid":"7810722d091640058f889b497ecbc306"} \ No newline at end of file diff --git a/initialData/conversations.bson b/initialData/conversations.bson deleted file mode 100644 index 7446f64c7..000000000 Binary files a/initialData/conversations.bson and /dev/null differ diff --git a/initialData/conversations.metadata.json b/initialData/conversations.metadata.json deleted file mode 100644 index 158427e33..000000000 --- a/initialData/conversations.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.conversations"},{"v":2,"key":{"createdAt":1},"name":"createdAt_1","ns":"erxes.conversations","background":true}],"uuid":"873a980e2ce040d8b357dfb880c90a96"} \ No newline at end of file diff --git a/initialData/customers.bson b/initialData/customers.bson deleted file mode 100644 index 51b09877c..000000000 Binary files a/initialData/customers.bson and /dev/null differ diff --git a/initialData/deal_boards.bson b/initialData/deal_boards.bson deleted file mode 100644 index a50681e90..000000000 Binary files a/initialData/deal_boards.bson and /dev/null differ diff --git a/initialData/deal_boards.metadata.json b/initialData/deal_boards.metadata.json deleted file mode 100644 index ac6de24a8..000000000 --- a/initialData/deal_boards.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.deal_boards"}],"uuid":"3c54e2fe8ebe43229f138ab0ec4d99fa"} \ No newline at end of file diff --git a/initialData/deal_pipelines.bson b/initialData/deal_pipelines.bson deleted file mode 100644 index 3d3b0cb44..000000000 Binary files a/initialData/deal_pipelines.bson and /dev/null differ diff --git a/initialData/deal_pipelines.metadata.json b/initialData/deal_pipelines.metadata.json deleted file mode 100644 index ad93f0718..000000000 --- a/initialData/deal_pipelines.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.deal_pipelines"}],"uuid":"69cf853096134ea9ae6dc334fb5fe962"} \ No newline at end of file diff --git a/initialData/deal_stages.bson b/initialData/deal_stages.bson deleted file mode 100644 index 81740533e..000000000 Binary files a/initialData/deal_stages.bson and /dev/null differ diff --git a/initialData/deal_stages.metadata.json b/initialData/deal_stages.metadata.json deleted file mode 100644 index fb7ad50fc..000000000 --- a/initialData/deal_stages.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.deal_stages"}],"uuid":"2bf45d537f5c4470aa301624fa9d1eae"} \ No newline at end of file diff --git a/initialData/deals.bson b/initialData/deals.bson deleted file mode 100644 index 235ca9089..000000000 Binary files a/initialData/deals.bson and /dev/null differ diff --git a/initialData/deals.metadata.json b/initialData/deals.metadata.json deleted file mode 100644 index 5a3c52537..000000000 --- a/initialData/deals.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.deals"}],"uuid":"21ee5a02d35a4dab9bc9d710a68adb38"} \ No newline at end of file diff --git a/initialData/email_templates.bson b/initialData/email_templates.bson deleted file mode 100644 index d150756e7..000000000 Binary files a/initialData/email_templates.bson and /dev/null differ diff --git a/initialData/email_templates.metadata.json b/initialData/email_templates.metadata.json deleted file mode 100644 index 7750e679f..000000000 --- a/initialData/email_templates.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.email_templates"}],"uuid":"8c3a4f0bddad459986221fd8fede82dd"} \ No newline at end of file diff --git a/initialData/engage_messages.metadata.json b/initialData/engage_messages.metadata.json deleted file mode 100644 index ce3cd3b1d..000000000 --- a/initialData/engage_messages.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.engage_messages"}],"uuid":"6d9f9dc037294135a32f282fedcfffce"} \ No newline at end of file diff --git a/initialData/fields.bson b/initialData/fields.bson deleted file mode 100644 index 9fe10bd6e..000000000 Binary files a/initialData/fields.bson and /dev/null differ diff --git a/initialData/forms.bson b/initialData/forms.bson deleted file mode 100644 index f03952bac..000000000 Binary files a/initialData/forms.bson and /dev/null differ diff --git a/initialData/integrations.bson b/initialData/integrations.bson deleted file mode 100644 index 1ef447c9b..000000000 Binary files a/initialData/integrations.bson and /dev/null differ diff --git a/initialData/integrations.metadata.json b/initialData/integrations.metadata.json deleted file mode 100644 index b899f360c..000000000 --- a/initialData/integrations.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.integrations"}],"uuid":"ccec122f943f4d4489c2a114014f8231"} \ No newline at end of file diff --git a/initialData/internal_notes.metadata.json b/initialData/internal_notes.metadata.json deleted file mode 100644 index d079d1317..000000000 --- a/initialData/internal_notes.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.internal_notes"}],"uuid":"76a399333a644d6b88d791b7f8b960e4"} \ No newline at end of file diff --git a/initialData/knowledgebase_articles.bson b/initialData/knowledgebase_articles.bson deleted file mode 100644 index ba1c41a75..000000000 Binary files a/initialData/knowledgebase_articles.bson and /dev/null differ diff --git a/initialData/knowledgebase_articles.metadata.json b/initialData/knowledgebase_articles.metadata.json deleted file mode 100644 index a05b9b038..000000000 --- a/initialData/knowledgebase_articles.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.knowledgebase_articles"}],"uuid":"0665ecf554904625a12158f7fdab02d5"} \ No newline at end of file diff --git a/initialData/knowledgebase_categories.bson b/initialData/knowledgebase_categories.bson deleted file mode 100644 index 54785ea48..000000000 Binary files a/initialData/knowledgebase_categories.bson and /dev/null differ diff --git a/initialData/knowledgebase_categories.metadata.json b/initialData/knowledgebase_categories.metadata.json deleted file mode 100644 index d5838a714..000000000 --- a/initialData/knowledgebase_categories.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.knowledgebase_categories"}],"uuid":"2577052f5c794711943a68baa5500818"} \ No newline at end of file diff --git a/initialData/knowledgebase_topics.metadata.json b/initialData/knowledgebase_topics.metadata.json deleted file mode 100644 index 38e3a737a..000000000 --- a/initialData/knowledgebase_topics.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.knowledgebase_topics"}],"uuid":"426a2e6a6560474db3f9588d587788a7"} \ No newline at end of file diff --git a/initialData/permissions.bson b/initialData/permissions.bson deleted file mode 100644 index 6f0625e85..000000000 Binary files a/initialData/permissions.bson and /dev/null differ diff --git a/initialData/permissions.metadata.json b/initialData/permissions.metadata.json deleted file mode 100644 index baa3aa5d0..000000000 --- a/initialData/permissions.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.permissions"}],"uuid":"eac83e42ddfe49e995638ae9bd3e68e0"} \ No newline at end of file diff --git a/initialData/products.bson b/initialData/products.bson deleted file mode 100644 index ff7298926..000000000 Binary files a/initialData/products.bson and /dev/null differ diff --git a/initialData/products.metadata.json b/initialData/products.metadata.json deleted file mode 100644 index c2c18c9a0..000000000 --- a/initialData/products.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.products"}],"uuid":"7f80bf5ae8684c2bb2d66fd70d465137"} \ No newline at end of file diff --git a/initialData/response_templates.metadata.json b/initialData/response_templates.metadata.json deleted file mode 100644 index 3c1cba508..000000000 --- a/initialData/response_templates.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.response_templates"}],"uuid":"8f3c68dbd321479da147a5882af9f186"} \ No newline at end of file diff --git a/initialData/segments.bson b/initialData/segments.bson deleted file mode 100644 index 32df0b86f..000000000 Binary files a/initialData/segments.bson and /dev/null differ diff --git a/initialData/segments.metadata.json b/initialData/segments.metadata.json deleted file mode 100644 index 31fdfafde..000000000 --- a/initialData/segments.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.segments"}],"uuid":"df6562c4c46b40d4848251ab9ae6f693"} \ No newline at end of file diff --git a/initialData/tags.bson b/initialData/tags.bson deleted file mode 100644 index 74e3d0467..000000000 Binary files a/initialData/tags.bson and /dev/null differ diff --git a/initialData/user_groups.bson b/initialData/user_groups.bson deleted file mode 100644 index b429fb9d2..000000000 Binary files a/initialData/user_groups.bson and /dev/null differ diff --git a/initialData/users.bson b/initialData/users.bson deleted file mode 100644 index 4c0b721d9..000000000 Binary files a/initialData/users.bson and /dev/null differ diff --git a/initialData/users.metadata.json b/initialData/users.metadata.json deleted file mode 100644 index 34b51adab..000000000 --- a/initialData/users.metadata.json +++ /dev/null @@ -1 +0,0 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.users"},{"v":2,"unique":true,"key":{"email":1},"name":"email_1","background":true,"ns":"erxes.users"}],"uuid":"e560ab63d85b40babba073d6257a26c0"} \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index 96c9c42e5..d084c3608 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,15 +1,35 @@ module.exports = { roots: ['/src/__tests__'], transform: { - '^.+\\.ts$': 'ts-jest' + '^.+\\.ts$': 'ts-jest', }, testRegex: '/__tests__/.*\\.(ts|js)$', testEnvironment: 'node', moduleFileExtensions: ['ts', 'js', 'json', 'node'], - modulePathIgnorePatterns: ['utils.ts', 'setup.ts'], + modulePathIgnorePatterns: ['utils.ts', 'setup.ts', 'conversationCronJob.test.ts', 'coverage/'], + coverageDirectory: 'src/__tests__/coverage/', + collectCoverage: true, + collectCoverageFrom: [ + 'src/db/models/**', + '!src/db/models/Robot.ts', + '!src/db/models/definitions/**', + 'src/data/resolvers/**', + '!src/data/resolvers/customScalars.ts', + '!src/data/resolvers/mutations/robot.ts', + '!src/data/resolvers/queries/insights.ts', + '!src/data/resolvers/queries/robot.ts', + '!src/data/resolvers/subscriptions/**', + ], + coverageThreshold: { + global: { + functions: 100, + lines: 100, + statements: 100, + }, + }, globals: { 'ts-jest': { - tsConfigFile: 'tsconfig.json' - } - } -}; + tsConfigFile: 'tsconfig.json', + }, + }, +}; \ No newline at end of file diff --git a/logger/.dockerignore b/logger/.dockerignore new file mode 100644 index 000000000..8e76163eb --- /dev/null +++ b/logger/.dockerignore @@ -0,0 +1,10 @@ +*.md +*.yml +.env* +.git* +.prettierrc +.snyk +Dockerfile* +google_cred.json.sample +scripts +*.tar.gz diff --git a/logger/.env.sample b/logger/.env.sample new file mode 100644 index 000000000..6c80f972b --- /dev/null +++ b/logger/.env.sample @@ -0,0 +1,2 @@ +PORT=3800 +MONGO_URL=mongodb://localhost/erxes_logs \ No newline at end of file diff --git a/logger/.snyk b/logger/.snyk new file mode 100644 index 000000000..29509a00b --- /dev/null +++ b/logger/.snyk @@ -0,0 +1,4 @@ +# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. +version: v1.13.5 +ignore: {} +patch: {} diff --git a/logger/Dockerfile b/logger/Dockerfile new file mode 100644 index 000000000..f4dc20a62 --- /dev/null +++ b/logger/Dockerfile @@ -0,0 +1,7 @@ +FROM node:12.18-slim +WORKDIR /erxes-logger/ +RUN chown -R node:node /erxes-logger +COPY --chown=node:node . /erxes-logger +USER node +EXPOSE 3800 +ENTRYPOINT [ "node", "--max_old_space_size=8192", "dist" ] diff --git a/logger/db-migrate-store.js b/logger/db-migrate-store.js new file mode 100644 index 000000000..23ffabeb5 --- /dev/null +++ b/logger/db-migrate-store.js @@ -0,0 +1,66 @@ +const mongoose = require('mongoose'); +const dotenv = require('dotenv'); + +dotenv.config(); + +class dbStore { + constructor() { + + this.url = process.env.MONGO_URL; + this.db = null; + } + + connect() { + return mongoose.createConnection(this.url, { useNewUrlParser: true }).then(client => { + return client.db; + }) + } + + load(fn) { + return this.connect() + .then(db => db.collection('migrations').find().toArray()) + .then(data => { + if (!data.length) return fn(null, {}); + + const store = data[0]; + + // Check if old format and convert if needed + if (!Object.prototype.hasOwnProperty.call(store, 'lastRun') && + Object.prototype.hasOwnProperty.call(store, 'pos')) { + + if (store.pos === 0) { + store.lastRun = null; + } else { + if (store.pos > store.migrations.length) + return fn(new Error('Store file contains invalid pos property')); + + store.lastRun = store.migrations[store.pos - 1].title; + } + + // In-place mutate the migrations in the array + store.migrations.forEach((migration, index) => { + if (index < store.pos) + migration.timestamp = Date.now(); + }) + } + + // Check if does not have required properties + if (!Object.prototype.hasOwnProperty.call(store, 'lastRun') || !Object.prototype.hasOwnProperty.call(store, 'migrations')) + return fn(new Error('Invalid store file')); + + return fn(null, store); + }) + .catch(fn) + } + + save(set, fn) { + return this.connect() + .then(db => db.collection('migrations') + .replaceOne({}, { migrations: set.migrations, lastRun: set.lastRun }, { upsert: true }) + .then(result => fn(null, result)) + ) + .catch(fn) + } +} + +module.exports = dbStore diff --git a/logger/package.json b/logger/package.json new file mode 100644 index 000000000..a124feb21 --- /dev/null +++ b/logger/package.json @@ -0,0 +1,48 @@ +{ + "name": "erxes-logger", + "description": "Logger module for erxes", + "homepage": "https://erxes.io", + "repository": { + "type": "git", + "url": "https://github.com/erxes/erxes-api" + }, + "bugs": "https://github.com/erxes/erxes-api/issues", + "keywords": [ + "node", + "express", + "graphql", + "apollo" + ], + "scripts": { + "start": "node dist", + "dev": "DEBUG=erxes-logger:*,erxes-message-broker:* node_modules/.bin/ts-node-dev --respawn src", + "test": "NODE_ENV=test jest --runInBand --forceExit", + "build": "tsc -p tsconfig.prod.json", + "migrate": "NODE_ENV=command migrate --migrations-dir='./dist/migrations' --store='./db-migrate-store.js' up" + }, + "dependencies": { + "@types/body-parser": "^1.17.0", + "@types/cors": "^2.8.4", + "@types/dotenv": "^4.0.3", + "@types/express": "^4.16.0", + "@types/mongodb": "^3.1.2", + "@types/mongoose": "^5.2.1", + "@types/node": "^10.12.18", + "@types/q": "^1.5.0", + "amqplib": "^0.5.5", + "body-parser": "^1.17.1", + "cote": "^1.0.0", + "debug": "^4.1.1", + "dotenv": "^4.0.0", + "erxes-message-broker": "^1.0.17", + "express": "^4.16.4", + "migrate": "^1.6.2", + "mongoose": "5.7.10", + "ts-node": "8.0.3", + "uuid": "^8.3.0" + }, + "devDependencies": { + "ts-node-dev": "^1.0.0-pre.32", + "typescript": "^2.9.2" + } +} diff --git a/logger/src/connection.ts b/logger/src/connection.ts new file mode 100644 index 000000000..f2ccd0583 --- /dev/null +++ b/logger/src/connection.ts @@ -0,0 +1,30 @@ +import * as dotenv from 'dotenv'; +import mongoose = require('mongoose'); +import { debugDb } from './debuggers'; + +dotenv.config(); + +mongoose.Promise = global.Promise; + +export const connectionOptions = { + useNewUrlParser: true, + useCreateIndex: true, + autoReconnect: true, + useFindAndModify: false, +}; + +export const connect = () => { + const URI = process.env.MONGO_URL; + mongoose.connect(URI, connectionOptions); + + mongoose.connection + .on('connected', () => { + debugDb(`Connected to the database: ${URI}`); + }) + .on('disconnected', () => { + debugDb(`Disconnected from the database: ${URI}`); + }) + .on('error', error => { + debugDb(`Database connection error: ${URI}`, error); + }); +}; diff --git a/logger/src/debuggers.ts b/logger/src/debuggers.ts new file mode 100644 index 000000000..d982f445d --- /dev/null +++ b/logger/src/debuggers.ts @@ -0,0 +1,16 @@ +import * as debug from 'debug'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +export const debugInit = debug('erxes-logger:init'); +export const debugDb = debug('erxes-logger:db'); +export const debugBase = debug('erxes-logger:base'); +export const debugExternalRequests = debug('erxes-logger:external-requests'); + +export const debugRequest = (debugInstance, req) => + debugInstance(` + Receiving ${req.path} request from ${req.headers.origin} + body: ${JSON.stringify(req.body || {})} + queryParams: ${JSON.stringify(req.query)} + `); diff --git a/logger/src/index.ts b/logger/src/index.ts new file mode 100644 index 000000000..d6a9279ed --- /dev/null +++ b/logger/src/index.ts @@ -0,0 +1,109 @@ +import * as bodyParser from 'body-parser'; +import * as dotenv from 'dotenv'; +import * as express from 'express'; + +// load environment variables +dotenv.config(); + +import { connect } from './connection'; +import { debugBase, debugInit } from './debuggers'; +import { initBroker } from './messageBroker'; +import Logs from './models/Logs'; + +connect(); + +const app = express(); + +app.use((req: any, _res, next) => { + req.rawBody = ''; + + req.on('data', chunk => { + req.rawBody += chunk.toString().replace(/\//g, '/'); + }); + + next(); +}); + +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: true })); + +// sends logs according to specified filter +app.get('/logs', async (req, res) => { + interface IFilter { + createdAt?: any; + createdBy?: string; + action?: string; + type?: string; + description?: object; + } + + const params = JSON.parse(req.body.params); + const { start, end, userId, action, page, perPage, type, desc } = params; + const filter: IFilter = {}; + + // filter by date + if (start && end) { + filter.createdAt = { $gte: new Date(start), $lte: new Date(end) }; + } else if (start) { + filter.createdAt = { $gte: new Date(start) }; + } else if (end) { + filter.createdAt = { $lte: new Date(end) }; + } + + // filter by user + if (userId) { + filter.createdBy = userId; + } + + // filter by actions + if (action) { + filter.action = action; + } + + // filter by module + if (type) { + filter.type = type; + } + + // filter by description text + if (desc) { + filter.description = { $regex: desc, $options: '$i' }; + } + + const _page = Number(page || '1'); + const _limit = Number(perPage || '20'); + + try { + const logs = await Logs.find(filter) + .sort({ createdAt: -1 }) + .limit(_limit) + .skip((_page - 1) * _limit); + + const logsCount = await Logs.countDocuments(filter); + + return res.json({ logs, totalCount: logsCount }); + } catch (e) { + return res.status(500).send(e.message); + } +}); + +// for health checking +app.get('/health', async (_req, res) => { + res.end('ok'); +}); + +// Error handling middleware +app.use((error, _req, res, _next) => { + console.error(error.stack); + res.status(500).send(error.message); +}); + +const { PORT } = process.env; + +app.listen(PORT, () => { + initBroker(app).catch(e => { + debugBase(`Error ocurred during message broker init ${e.message}`); + }); + + debugInit(`Logger server is running on port ${PORT}`); +}); diff --git a/logger/src/messageBroker.ts b/logger/src/messageBroker.ts new file mode 100644 index 000000000..5ea9141d5 --- /dev/null +++ b/logger/src/messageBroker.ts @@ -0,0 +1,25 @@ +import * as dotenv from 'dotenv'; +import messageBroker from 'erxes-message-broker'; +import { receivePutLogCommand } from './utils'; + +dotenv.config(); + +let client; + +export const initBroker = async server => { + client = await messageBroker({ + name: 'logger', + server, + envs: process.env, + }); + + const { consumeQueue } = client; + + consumeQueue('putLog', async data => { + await receivePutLogCommand(data); + }); +}; + +export default function() { + return client; +} diff --git a/logger/src/migrations/1574928447728-apply-new-fields.ts b/logger/src/migrations/1574928447728-apply-new-fields.ts new file mode 100644 index 000000000..b356a6276 --- /dev/null +++ b/logger/src/migrations/1574928447728-apply-new-fields.ts @@ -0,0 +1,57 @@ +import { connect } from '../connection'; +import Logs from '../models/Logs'; +import { compareObjects } from '../utils'; + +module.exports.up = async () => { + await connect(); + + const logs = await Logs.find({}); + + for (const log of logs) { + let parsedOldData; + let parsedNewData; + + try { + parsedOldData = JSON.parse(log.oldData || '{}'); + + if (log.newData) { + parsedNewData = JSON.parse(log.newData || '{}'); + } + } catch (e) { + console.log(e, 'JSON parsing error'); + parsedOldData = JSON.parse(log.oldData.replace('\n', '')); + } + + switch (log.action) { + case 'create': + await Logs.updateOne({ _id: log._id }, { $set: { addedData: log.newData } }); + break; + case 'update': + if (log.oldData && log.newData) { + try { + const comparison = compareObjects(parsedOldData, parsedNewData); + + await Logs.updateOne( + { _id: log._id }, + { + $set: { + addedData: JSON.stringify(comparison.added), + changedData: JSON.stringify(comparison.changed), + unchangedData: JSON.stringify(comparison.unchanged), + removedData: JSON.stringify(comparison.removed), + }, + }, + ); + } catch (e) { + console.log(e, 'object comparison error'); + } + } + break; + case 'delete': + await Logs.updateOne({ _id: log._id }, { $set: { removedData: log.oldData } }); + break; + default: + break; + } + } // end for loop +}; diff --git a/logger/src/models/Logs.ts b/logger/src/models/Logs.ts new file mode 100644 index 000000000..646ee41b4 --- /dev/null +++ b/logger/src/models/Logs.ts @@ -0,0 +1,143 @@ +import { Document, model, Model, Schema } from 'mongoose'; +import { compareObjects } from '../utils'; + +/** + * Mongoose field options wrapper + * @param {Object} options Mongoose schema options + */ +export const field = options => { + const { type, optional } = options; + + if (type === String && !optional) { + options.validate = /\S+/; + } + + return options; +}; + +export interface ILogDoc { + createdAt: Date; + createdBy: string; + type: string; + ipAddress?: string; + action: string; + object?: string; + unicode?: string; + description?: string; + oldData?: string; + newData?: string; + objectId?: string; + addedData?: string; + changedData?: string; + unchangedData?: string; + removedData?: string; + extraDesc?: string; +} + +export interface ILogDocument extends ILogDoc, Document {} + +export interface ILogModel extends Model { + createLog(doc: ILogDoc): Promise; +} + +export const schema = new Schema({ + createdAt: field({ + type: Date, + label: 'Created date', + index: true, + default: new Date(), + }), + createdBy: field({ type: String, label: 'Performer of the action' }), + type: field({ + type: String, + label: 'Module name which has been changed', + }), + action: field({ + type: String, + label: 'Action, one of (create|update|delete)', + }), + ipAddress: field({ type: String, optional: true, label: 'IP address' }), + objectId: field({ type: String, label: 'Collection row id' }), + unicode: field({ type: String, label: 'Performer username' }), + description: field({ type: String, label: 'Description' }), + // restore db from these if disaster happens + oldData: field({ type: String, label: 'Data before changes', optional: true }), + newData: field({ type: String, label: 'Data to be changed', optional: true }), + // processed data to show in front side + addedData: field({ type: String, label: 'Newly added fields', optional: true }), + unchangedData: field({ type: String, label: 'Unchanged fields', optional: true }), + changedData: field({ type: String, label: 'Changed fields', optional: true }), + removedData: field({ type: String, label: 'Removed fields', optional: true }), + extraDesc: field({ type: String, label: 'Extra description', optional: true }), +}); + +export const loadLogClass = () => { + class Log { + public static createLog(doc: ILogDoc) { + const { object, newData } = doc; + const logDoc = { ...doc }; + let oldData; + let parsedNewData; + + try { + oldData = JSON.parse(object); + + if (newData) { + parsedNewData = JSON.parse(newData); + } + } catch (e) { + console.log(e, 'JSON parsing error'); + oldData = JSON.parse(object.replace('\n', '')); + } + + if (oldData._id) { + logDoc.objectId = oldData._id; + } + + switch (doc.action) { + case 'create': + logDoc.addedData = JSON.stringify(parsedNewData); + break; + case 'update': + if (oldData && newData) { + try { + const comparison = compareObjects(oldData, parsedNewData); + + // store exactly how it was for safety purpose + logDoc.oldData = JSON.stringify(oldData); + // it comes as string already + logDoc.newData = newData; + + logDoc.addedData = JSON.stringify(comparison.added); + logDoc.changedData = JSON.stringify(comparison.changed); + logDoc.unchangedData = JSON.stringify(comparison.unchanged); + logDoc.removedData = JSON.stringify(comparison.removed); + } catch (e) { + console.log(e, 'object comparison error'); + } + } + + break; + case 'delete': + logDoc.oldData = JSON.stringify(oldData); + logDoc.removedData = JSON.stringify(oldData); + break; + default: + break; + } + + return Logs.create(logDoc); + } + } + + schema.loadClass(Log); + + return schema; +}; + +loadLogClass(); + +// tslint:disable-next-line +const Logs = model('logs', schema); + +export default Logs; diff --git a/logger/src/utils.ts b/logger/src/utils.ts new file mode 100644 index 000000000..99ac1189e --- /dev/null +++ b/logger/src/utils.ts @@ -0,0 +1,255 @@ +import { debugBase } from './debuggers'; +import Logs from './models/Logs'; + +/** + * Takes 2 arrays and detect changes between them. + * @param oldArray Old array + * @param newArray New array + * @returns Object specifying changed & unchanged fields + * @todo Improve object array comparison part + */ +const compareArrays = (oldArray: any[] = [], newArray: any[] = []) => { + const changedItems = []; + const unchangedItems = []; + const addedItems = []; + let removedItems = []; + + if (oldArray.length > 0 && newArray.length === 0) { + removedItems = oldArray; + } + + for (const elem of oldArray) { + if (typeof elem !== 'object') { + const found = newArray.find(el => el === elem); + + /** + * If removedItems contains the pushing value, then it caused the following error + * FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory + */ + if (!found && elem && !removedItems.includes(elem)) { + removedItems.push(elem); + } + } + } + + newArray.forEach((elem, index) => { + // primary data types + if (typeof elem !== 'object') { + const found = oldArray.find(el => el === elem); + + if (found) { + unchangedItems.push(elem); + } + + // means an element has been added + if (!found && elem) { + addedItems.push(elem); + } + } + + if (typeof elem === 'object') { + const comparison = compareObjects(oldArray[index], newArray[index]); + const { unchanged, changed, added, removed } = comparison; + + if (changed && !isObjectEmpty(changed)) { + changedItems.push(changed); + } + + if (added && !isObjectEmpty(added)) { + addedItems.push(added); + } + + if (removed && !isObjectEmpty(removed)) { + removedItems.push(removed); + } + + if (unchanged && !isObjectEmpty(unchanged)) { + unchangedItems.push(unchanged); + } + } + }); + + return { + unchanged: unchangedItems, + changed: changedItems, + added: addedItems, + removed: removedItems, + }; +}; + +/** + * Shorthand empty value checker + * @param val Value to check + */ +const isNull = val => val === null || val === undefined || val === ''; + +/** + * Shorthand empty object checker + * @param obj Object to check + */ +const isObjectEmpty = obj => { + return typeof obj === 'object' && obj && Object.keys(obj).length === 0 && obj.constructor === Object; +}; + +/** + * Removes null, undefined attributes from given object one level down. + * @param {Object} obj Object to check + * @returns {Object} Flattened object + */ +const flattenObject = (obj = {}) => { + const flatObject = { ...obj }; + const names = obj ? Object.getOwnPropertyNames(obj) : []; + + for (const name of names) { + const attr = obj[name]; + let empty = false; + + if (typeof attr !== 'object') { + if (isNull(attr)) { + empty = true; + } + } + + if (Array.isArray(attr) && attr.length === 0) { + empty = true; + } + + if (typeof attr === 'object' && !Array.isArray(attr)) { + if (isObjectEmpty(attr)) { + empty = true; + } + } + + if (empty) { + delete flatObject[name]; + } + } // end for loop + + return flatObject; +}; + +/** + * Detects changes between two objects. + * Input objects MUST have same hierarchical level of attributes. + * @param oldData Actual data in db + * @param newData Doc to be changed + * @returns Object specifying changed & unchanged fields + */ +export const compareObjects = (oldData: object = {}, newData: object = {}) => { + const changedFields = {}; + const unchangedFields = {}; + const addedFields = {}; + const removedFields = {}; + // exclude field names not stored in db + const nonSchemaNames = ['uid', 'length']; + let fieldNames: string[] = []; + + if (newData && !isObjectEmpty(newData)) { + fieldNames = Object.getOwnPropertyNames(newData); + // split + fieldNames = fieldNames.map(n => { + if (!nonSchemaNames.includes(n)) { + return n; + } + }); + } + + for (const name of fieldNames) { + const oldField = oldData[name]; + const newField = newData[name]; + + if (typeof newField !== 'object') { + // changed fields + if (oldField !== newField) { + changedFields[name] = newField; + + // means removed a field + if (!isNull(oldField) && isNull(newField)) { + removedFields[name] = oldField; + } + + // means added a new field + if (isNull(oldField) && !isNull(newField)) { + addedFields[name] = newField; + } + } + + // unchanged fields + if (oldField === newField) { + unchangedFields[name] = newField; + } + } // end primary type comparison + + if (Array.isArray(newField)) { + const comparison = compareArrays(oldField, newField); + const { changed, unchanged, added, removed } = comparison; + + if (changed.length > 0) { + changedFields[name] = changed; + } + if (added.length > 0) { + addedFields[name] = added; + } + if (removed.length > 0) { + removedFields[name] = removed; + } + if (unchanged.length > 0) { + unchangedFields[name] = unchanged; + } + } // end array comparison + + if (typeof newField === 'object' && !Array.isArray(newField)) { + // compare deeply when only both fields have values + if (newField && oldField) { + const comparison = compareObjects(oldField, newField); + const { changed, added, removed, unchanged } = comparison; + + if (!isObjectEmpty(changed)) { + changedFields[name] = flattenObject(changed); + } + if (!isObjectEmpty(added)) { + addedFields[name] = flattenObject(added); + } + if (!isObjectEmpty(removed)) { + removedFields[name] = flattenObject(removed); + } + if (!isObjectEmpty(unchanged)) { + unchangedFields[name] = flattenObject(unchanged); + } + } // end both field checking + + if (!oldField && newField) { + addedFields[name] = newField; + } + + if (oldField && !newField) { + removedFields[name] = oldField; + } + } // end regular object comparison + } // end old data for loop + + return { + changed: changedFields, + unchanged: unchangedFields, + added: addedFields, + removed: removedFields, + }; +}; + +export const receivePutLogCommand = async params => { + debugBase(params); + + const { createdBy, type, action, unicode, description, object, newData, extraDesc } = params; + + return Logs.createLog({ + createdBy, + type, + action, + object, + newData, + unicode, + createdAt: new Date(), + description, + extraDesc, + }); +}; diff --git a/logger/tsconfig.json b/logger/tsconfig.json new file mode 100644 index 000000000..63c2b8153 --- /dev/null +++ b/logger/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "allowJs": true, + "target": "es5", + "moduleResolution": "node", + "module": "commonjs", + "lib": ["es2015", "es6", "es7"], + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true + }, + "include": ["./src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/logger/tsconfig.prod.json b/logger/tsconfig.prod.json new file mode 100644 index 000000000..b0d0290d1 --- /dev/null +++ b/logger/tsconfig.prod.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "src/__tests__"], + "compilerOptions": { + "sourceMap": false, + "inlineSourceMap": false, + "inlineSources": false + } +} diff --git a/logger/yarn.lock b/logger/yarn.lock new file mode 100644 index 000000000..d5681ecd8 --- /dev/null +++ b/logger/yarn.lock @@ -0,0 +1,1811 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@dashersw/axon@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@dashersw/axon/-/axon-2.0.5.tgz#708b8cd21a5c803de8dd517a9252828b007d77bb" + integrity sha512-e7az6UOh/1JqLvzg2GPhP3n47QMQal3Qg2a2497JwY7dlbSKUg4dQmnRyKWNjFz0FHjranUjKvX6J6NAV3Sm/Q== + dependencies: + amp "~0.3.1" + amp-message "~0.1.1" + configurable "0.0.1" + debug "*" + escape-regexp "0.0.1" + +"@dashersw/node-discover@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@dashersw/node-discover/-/node-discover-1.0.4.tgz#3fd2aad22228e0ecf72bb069e9f0e06ef4bd5b82" + integrity sha512-OblARM345ECaTSSFQcuWUl+7/uhOjhKBIA0G0CbOPbUzwF3cqBbl2R0E9tulnsLk3XB6Zpmja0TZIU5ClKF6LA== + dependencies: + redis "^2.7.1" + uuid "^3.3.2" + +"@types/body-parser@*", "@types/body-parser@^1.17.0": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" + integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bson@*": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.1.tgz#2bfc80819e7055b76d5496d5344ed23e5d12bbb2" + integrity sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw== + dependencies: + "@types/node" "*" + +"@types/connect@*": + version "3.4.32" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.4": + version "2.8.6" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" + integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== + dependencies: + "@types/express" "*" + +"@types/dotenv@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67" + integrity sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw== + dependencies: + "@types/node" "*" + +"@types/express-serve-static-core@*": + version "4.17.0" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz#e80c25903df5800e926402b7e8267a675c54a281" + integrity sha512-Xnub7w57uvcBqFdIGoRg1KhNOeEj0vB6ykUM7uFWyxvbdE89GFyqgmUcanAriMr4YOxNFZBAWkfcWIb4WBPt3g== + dependencies: + "@types/node" "*" + "@types/range-parser" "*" + +"@types/express@*", "@types/express@^4.16.0": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/mime@*": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== + +"@types/mongodb@*", "@types/mongodb@^3.1.2": + version "3.3.12" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.3.12.tgz#c5afa2de5ee5558603ecee8c9366e86e24576703" + integrity sha512-gWIdrA8YKC4OetBk4eT5Zsp4p3oy/BJQKt80tXfgPnfBuLigumcmwNZseVSkLQJ3XkN/1OR0/kIunGWlew3rmQ== + dependencies: + "@types/bson" "*" + "@types/node" "*" + +"@types/mongoose@^5.2.1": + version "5.5.32" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.32.tgz#8a76c5be029086c1225bf88ed3ca83f01181121f" + integrity sha512-2BemWy7SynT87deweqc2eCzg6pRyTVlnnMat2JxsTNoyeSFKC27b19qBTeKRfBVt+SjtaWd/ud4faUaObONwBA== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + +"@types/node@*": + version "12.12.16" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.16.tgz#3ebcbd7bf978fa4c5120fee8be57083271a8b3ac" + integrity sha512-vRuMyoOr5yfNf8QWxXegOjeyjpWJxFePzHzmBOIzDIzo+rSqF94RW0PkS6y4T2+VjAWLXHWrfbIJY3E3aS7lUw== + +"@types/node@^10.12.18": + version "10.17.8" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.8.tgz#07c0819207b4bb46e5a509fe25f3232e76fa6683" + integrity sha512-FeTtEwXbQa187ABpeEQoO7pq3dHgE85FmAUExx2sKO6U1/MYrLTYv+BIMcgVbQ66WjI4w+Ni+5HJtY+gHgWnPg== + +"@types/q@^1.5.0": + version "1.5.2" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" + integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + +"@types/range-parser@*": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== + +"@types/serve-static@*": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +"@types/strip-bom@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= + +"@types/strip-json-comments@0.0.30": + version "0.0.30" + resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +accepts@~1.3.4, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== + dependencies: + mime-types "~2.1.24" + negotiator "0.6.2" + +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +amp-message@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/amp-message/-/amp-message-0.1.2.tgz#a78f1c98995087ad36192a41298e4db49e3dfc45" + integrity sha1-p48cmJlQh602GSpBKY5NtJ49/EU= + dependencies: + amp "0.3.1" + +amp@0.3.1, amp@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/amp/-/amp-0.3.1.tgz#6adf8d58a74f361e82c1fa8d389c079e139fc47d" + integrity sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0= + +amqplib@^0.5.5: + version "0.5.5" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.5.5.tgz#698f0cb577e0591954a90572fcb3b8998a76fd40" + integrity sha512-sWx1hbfHbyKMw6bXOK2k6+lHL8TESWxjAx5hG8fBtT7wcxoXNIsFxZMnFyBjxt3yL14vn7WqBDe5U6BGOadtLg== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +amqplib@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.6.0.tgz#87857c7c95d56d22438ced4cf1f7e5f0dc43b309" + integrity sha512-zXCh4jQ77TBZe1YtvZ1n7sUxnTjnNagpy8MVi2yc1ive239pS3iLwm4e4d5o4XZGx1BdTKQ/U0ZmaDU3c8MxYQ== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= + +arg@^4.1.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.2.tgz#e70c90579e02c63d80e3ad4e31d8bfdb8bd50064" + integrity sha512-+ytCkGcBtHZ3V2r2Z06AncYO8jz46UEamcspGoU8lHcEbpn6J77QK0vdWvChsclg/tM5XIJC5tnjmPp7Eq6Obg== + +array-find-index@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== + +async-limiter@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== + +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== + dependencies: + lodash "^4.17.14" + +backo2@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= + +base64id@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +bitsyntax@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" + integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== + dependencies: + buffer-more-ints "~1.0.0" + debug "~2.6.9" + safe-buffer "~5.1.2" + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== + +bluebird@3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bluebird@^3.5.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +body-parser@1.19.0, body-parser@^1.17.1: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== + dependencies: + bytes "3.1.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.2" + http-errors "1.7.2" + iconv-lite "0.4.24" + on-finished "~2.3.0" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +bson@^1.1.1, bson@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== + +bytes@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" + integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== + +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + +camelcase-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= + dependencies: + camelcase "^2.0.0" + map-obj "^1.0.0" + +camelcase@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +charm@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + dependencies: + inherits "^2.0.1" + +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== + +commander@^2.9.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= + +component-emitter@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +configurable@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/configurable/-/configurable-0.0.1.tgz#47d75b727b51b4eb84c1dadafe3f8240313833b1" + integrity sha1-R9dbcntRtOuEwdra/j+CQDE4M7E= + +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +cote@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cote/-/cote-1.0.0.tgz#ca1cf6ffc3064504e8605e1a933e779e12e1328e" + integrity sha512-O9L5bCnA556UHbdGS0O+D7hhf6yxRon6igbl18ADgMnsCd8P9pVfvuvgO8X/Uch5Mj+xXRoVtkWYgddwNc74eg== + dependencies: + "@dashersw/axon" "2.0.5" + "@dashersw/node-discover" "^1.0.4" + charm "1.0.2" + colors "1.4.0" + eventemitter2 "6.0.0" + lodash "^4.17.15" + portfinder "1.0.25" + socket.io "^2.3.0" + uuid "^3.3.3" + +cross-env@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" + integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +currently-unhandled@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= + dependencies: + array-find-index "^1.0.1" + +dateformat@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= + +dateformat@~1.0.4-1.2.3: + version "1.0.12" + resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= + dependencies: + get-stdin "^4.0.1" + meow "^3.3.0" + +debounce@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@*, debug@^4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +debug@2.6.9, debug@~2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + +debug@^3.1.1: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +decamelize@^1.1.2: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= + +diff@^3.1.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + +dotenv@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= + +double-ended-queue@^2.1.0-0: + version "2.1.0-0" + resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" + integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= + +dynamic-dedupe@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= + dependencies: + xtend "^4.0.0" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= + +engine.io-client@~3.4.0: + version "3.4.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" + integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== + dependencies: + component-emitter "~1.3.0" + component-inherit "0.0.3" + debug "~4.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" + integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c" + integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg== + dependencies: + accepts "~1.3.4" + base64id "2.0.0" + cookie "0.3.1" + debug "~4.1.0" + engine.io-parser "~2.2.0" + ws "^7.1.2" + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +erxes-message-broker@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/erxes-message-broker/-/erxes-message-broker-1.0.17.tgz#2407312163d010f292e153cf1b45e33e0fb16f63" + integrity sha512-UdA5nRLn1tn5pqnDM+hR6LTrkiiOduVBanvAjxlvZujv6bUP+SdKzf17VWQhk4/3jZMmWzHIBZ0vpzHbHaVQTg== + dependencies: + "@babel/runtime" "^7.11.2" + amqplib "^0.6.0" + cote "^1.0.0" + cross-env "^7.0.2" + debug "^4.1.1" + requestify "^0.2.5" + uuid "^8.3.0" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-regexp@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254" + integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ= + +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= + +eventemitter2@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.0.0.tgz#218eb512c3603c5341724b6af7b686a1aa5ab8f5" + integrity sha512-ZuNWHD7S7IoikyEmx35vPU8H1W0L+oi644+4mSTg7nwXvBQpIwQL7DPjYUF0VMB0jPkNMo3MqD07E7MYrkFmjQ== + +express@^4.16.4: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== + dependencies: + accepts "~1.3.7" + array-flatten "1.1.1" + body-parser "1.19.0" + content-disposition "0.5.3" + content-type "~1.0.4" + cookie "0.4.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.2" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "~1.1.2" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" + safe-buffer "5.1.2" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +filewatcher@~3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= + dependencies: + debounce "^1.0.0" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.3" + statuses "~1.5.0" + unpipe "~1.0.0" + +find-up@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= + dependencies: + path-exists "^2.0.0" + pinkie-promise "^2.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +get-stdin@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +graceful-fs@^4.1.2: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + +growly@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= + dependencies: + ansi-regex "^2.0.0" + +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" + +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= + +hosted-git-info@^2.1.4: + version "2.8.5" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.5.tgz#759cfcf2c4d156ade59b0b2dfabddc42a6b9c70c" + integrity sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg== + +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +http-errors@~1.7.2: + version "1.7.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" + integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== + dependencies: + depd "~1.1.2" + inherits "2.0.4" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= + dependencies: + repeating "^2.0.0" + +indexof@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ipaddr.js@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" + integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + integrity sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko= + dependencies: + number-is-nan "^1.0.0" + +is-utf8@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-wsl@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" + integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= + +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +jquery@^3.1.0: + version "3.5.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5" + integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg== + +kareem@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" + integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== + +load-json-file@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + pinkie-promise "^2.0.0" + strip-bom "^2.0.0" + +lodash@^4.17.14, lodash@^4.17.15: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== + +loud-rejection@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= + dependencies: + currently-unhandled "^0.4.1" + signal-exit "^3.0.0" + +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +map-obj@^1.0.0, map-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= + +memory-pager@^1.0.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== + +meow@^3.3.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= + dependencies: + camelcase-keys "^2.0.0" + decamelize "^1.1.2" + loud-rejection "^1.0.0" + map-obj "^1.0.1" + minimist "^1.1.3" + normalize-package-data "^2.3.4" + object-assign "^4.0.1" + read-pkg-up "^1.0.1" + redent "^1.0.0" + trim-newlines "^1.0.0" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= + +migrate@^1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/migrate/-/migrate-1.6.2.tgz#8970d596780553fe9f545bdf83806df8473f025b" + integrity sha512-XAFab+ArPTo9BHzmihKjsZ5THKRryenA+lwob0R+ax0hLDs7YzJFJT5YZE3gtntZgzdgcuFLs82EJFB/Dssr+g== + dependencies: + chalk "^1.1.3" + commander "^2.9.0" + dateformat "^2.0.0" + dotenv "^4.0.0" + inherits "^2.0.3" + minimatch "^3.0.3" + mkdirp "^0.5.1" + slug "^0.9.2" + +mime-db@1.42.0: + version "1.42.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac" + integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ== + +mime-types@~2.1.24: + version "2.1.25" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437" + integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg== + dependencies: + mime-db "1.42.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +minimatch@^3.0.3, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +minimist@^1.1.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +mongodb@3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.3.tgz#509cad2225a1c56c65a331ed73a0d5d4ed5cbe67" + integrity sha512-MdRnoOjstmnrKJsK8PY0PjP6fyF/SBS4R8coxmhsfEU7tQ46/J6j+aSHF2n4c2/H8B+Hc/Klbfp8vggZfI0mmA== + dependencies: + bson "^1.1.1" + require_optional "^1.0.1" + safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" + +mongoose-legacy-pluralize@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== + +mongoose@5.7.10: + version "5.7.10" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.7.10.tgz#92bf817f50cf56211f85a079445257a4cc55271f" + integrity sha512-KpQosHPXmlNJKZbiP19mtmC0icaziRlB+xZ14R8q7jY7+OgbbynLD9VWSFb1CyzJX5ebdkVSGmay9HXn341hTA== + dependencies: + bson "~1.1.1" + kareem "2.3.1" + mongodb "3.3.3" + mongoose-legacy-pluralize "1.0.2" + mpath "0.6.0" + mquery "3.2.2" + ms "2.1.2" + regexp-clone "1.0.0" + safe-buffer "5.1.2" + sift "7.0.1" + sliced "1.0.1" + +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== + +mquery@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" + integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== + dependencies: + bluebird "3.5.1" + debug "3.1.0" + regexp-clone "^1.0.0" + safe-buffer "5.1.2" + sliced "1.0.1" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@2.1.2, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== + +node-notifier@^5.4.0: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== + dependencies: + growly "^1.3.0" + is-wsl "^1.1.0" + semver "^5.5.0" + shellwords "^0.1.1" + which "^1.3.0" + +normalize-package-data@^2.3.2, normalize-package-data@^2.3.4: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= + dependencies: + ee-first "1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" + +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= + dependencies: + pinkie-promise "^2.0.0" + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= + +path-type@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= + dependencies: + graceful-fs "^4.1.2" + pify "^2.0.0" + pinkie-promise "^2.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pinkie-promise@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= + dependencies: + pinkie "^2.0.0" + +pinkie@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +portfinder@1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== + dependencies: + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" + +proxy-addr@~2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.5.tgz#34cbd64a2d81f4b1fd21e76f9f06c8a45299ee34" + integrity sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ== + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.9.0" + +q@^0.9.7: + version "0.9.7" + resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" + integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= + +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== + dependencies: + bytes "3.1.0" + http-errors "1.7.2" + iconv-lite "0.4.24" + unpipe "1.0.0" + +read-pkg-up@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= + dependencies: + find-up "^1.0.0" + read-pkg "^1.0.0" + +read-pkg@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= + dependencies: + load-json-file "^1.0.0" + normalize-package-data "^2.3.2" + path-type "^1.0.0" + +"readable-stream@1.x >=1.1.9": + version "1.1.14" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +redent@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= + dependencies: + indent-string "^2.1.0" + strip-indent "^1.0.1" + +redis-commands@^1.2.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23" + integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ== + +redis-parser@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" + integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= + +redis@^2.7.1: + version "2.8.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" + integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== + dependencies: + double-ended-queue "^2.1.0-0" + redis-commands "^1.2.0" + redis-parser "^2.6.0" + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== + +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= + dependencies: + is-finite "^1.0.0" + +requestify@^0.2.5: + version "0.2.5" + resolved "https://registry.yarnpkg.com/requestify/-/requestify-0.2.5.tgz#80249f1ca7dfdf79fa2a6048aeac37d43e23c905" + integrity sha1-gCSfHKff33n6KmBIrqw31D4jyQU= + dependencies: + jquery "^3.1.0" + q "^0.9.7" + underscore "^1.8.3" + +require_optional@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== + dependencies: + resolve-from "^2.0.0" + semver "^5.1.0" + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-from@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= + +resolve@^1.0.0, resolve@^1.10.0: + version "1.13.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16" + integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w== + dependencies: + path-parse "^1.0.6" + +rimraf@^2.6.1: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +safe-buffer@5.1.2, safe-buffer@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +saslprep@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== + dependencies: + sparse-bitfield "^3.0.3" + +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== + dependencies: + debug "2.6.9" + depd "~1.1.2" + destroy "~1.0.4" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" + on-finished "~2.3.0" + range-parser "~1.2.1" + statuses "~1.5.0" + +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.17.1" + +setprototypeof@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" + integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shellwords@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +sliced@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= + +slug@^0.9.2: + version "0.9.4" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.4.tgz#fad5f1ef33150830c7688cd8500514576eccabd8" + integrity sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g== + dependencies: + unicode ">= 0.3.1" + +socket.io-adapter@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" + integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== + +socket.io-client@2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" + +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== + dependencies: + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" + +socket.io-parser@~3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a" + integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A== + dependencies: + component-emitter "1.2.1" + debug "~4.1.0" + isarray "2.0.1" + +socket.io@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" + integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== + dependencies: + debug "~4.1.0" + engine.io "~3.4.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.3.0" + socket.io-parser "~3.4.0" + +source-map-support@^0.5.12, source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +sparse-bitfield@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= + dependencies: + memory-pager "^1.0.2" + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= + +strip-ansi@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-bom@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= + dependencies: + is-utf8 "^0.2.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-indent@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= + dependencies: + get-stdin "^4.0.1" + +strip-json-comments@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +toidentifier@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== + +tree-kill@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" + integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== + +trim-newlines@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= + +ts-node-dev@^1.0.0-pre.32: + version "1.0.0-pre.44" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.44.tgz#2f4d666088481fb9c4e4f5bc8f15995bd8b06ecb" + integrity sha512-M5ZwvB6FU3jtc70i5lFth86/6Qj5XR5nMMBwVxZF4cZhpO7XcbWw6tbNiJo22Zx0KfjEj9py5DANhwLOkPPufw== + dependencies: + dateformat "~1.0.4-1.2.3" + dynamic-dedupe "^0.3.0" + filewatcher "~3.0.0" + minimist "^1.1.3" + mkdirp "^0.5.1" + node-notifier "^5.4.0" + resolve "^1.0.0" + rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" + ts-node "*" + tsconfig "^7.0.0" + +ts-node@*: + version "8.5.4" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.5.4.tgz#a152add11fa19c221d0b48962c210cf467262ab2" + integrity sha512-izbVCRV68EasEPQ8MSIGBNK9dc/4sYJJKYA+IarMQct1RtEot6Xp0bXuClsbUSnKpg50ho+aOAx8en5c+y4OFw== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +ts-node@8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.3.tgz#aa60b836a24dafd8bf21b54766841a232fdbc641" + integrity sha512-2qayBA4vdtVRuDo11DEFSsD/SFsBXQBRZZhbRGSIkmYmVkWjULn/GGMdG10KVqkaGndljfaTD8dKjWgcejO8YA== + dependencies: + arg "^4.1.0" + diff "^3.1.0" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tsconfig@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== + dependencies: + "@types/strip-bom" "^3.0.0" + "@types/strip-json-comments" "0.0.30" + strip-bom "^3.0.0" + strip-json-comments "^2.0.0" + +type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +typescript@^2.9.2: + version "2.9.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" + integrity sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w== + +underscore@^1.8.3: + version "1.10.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.10.2.tgz#73d6aa3668f3188e4adb0f1943bd12cfd7efaaaf" + integrity sha512-N4P+Q/BuyuEKFJ43B9gYuOj4TQUHXX+j2FqguVOpjkssLUUrnJofCcBccJSCoeturDoZU6GorDTHSvUDlSQbTg== + +"unicode@>= 0.3.1": + version "12.1.0" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-12.1.0.tgz#7ee53a7a0ca5539b353419432823d8da58bbbf33" + integrity sha512-Ty6+Ew21DiYTWLYtd05RF/X4c1ekOvOgANyHbBj0h3MaXpfaGr2Rdmc0hMFuGQLyPLb9cU4ArNxl0bTF5HSzXw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= + +url-parse@~1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= + +uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + +which@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +ws@^7.1.2: + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== + +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== + dependencies: + async-limiter "~1.0.0" + +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= + +xtend@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= + +yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== diff --git a/package.json b/package.json index 14fe0f247..8456a2687 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "erxes-api", - "version": "0.9.17", + "version": "0.19.3", "description": "GraphQL API for erxes main project", "homepage": "https://erxes.io", "repository": "https://github.com/erxes/erxes-api", @@ -11,26 +11,31 @@ "graphql", "apollo" ], - "license": "MIT", + "license": "GNU General Public License v3.0 with Commons Clause", "private": true, "scripts": { "start": "node dist", "start-crons": "node dist/cronJobs", "start-workers": "node dist/workers", - "dev": "DEBUG=erxes-api:* NODE_ENV=development node_modules/.bin/ts-node-dev --respawn src", + "dev": "ERXES_TELEMETRY_DISABLED=1 DEBUG=erxes* NODE_ENV=development node_modules/.bin/ts-node-dev --respawn --ignore-watch node_modules -- src", "dev-crons": "PROCESS_NAME=crons DEBUG=erxes-crons:* NODE_ENV=development node_modules/.bin/ts-node-dev --respawn src/cronJobs", - "dev-workers": "PROCESS_NAME=workers DEBUG=erxes-workers:* NODE_ENV=development node_modules/.bin/ts-node-dev --experimental-worker --respawn src/workers", - "build": "tsc -p tsconfig.prod.json && cp -rf src/private dist/ && yarn generateVersion", - "lint": "tslint 'src/**/*.ts'", - "format": "prettier --write 'src/**/*.ts'", + "dev-workers": "PROCESS_NAME=workers DEBUG=erxes-workers:* NODE_ENV=development NODE_OPTIONS='--experimental-worker' node_modules/.bin/ts-node src/workers/index.ts", + "build": "tsc -p tsconfig.prod.json && cp -rf src/private dist/ && mkdir -p dist/src/ && cp -rf src/initialData dist/src/", + "lint": "tslint '../src/**/*.ts'", + "format": "prettier --write '../src/**/*.ts'", "precommit": "lint-staged", - "test": "jest --forceExit && ts-node ./src/commands/aftertest.ts", - "generateVersion": "ts-node ./src/commands/generateVersion.ts", - "loadInitialData": "mongorestore --db erxes ./initialData", + "test": "node --expose-gc --max_old_space_size=4000 ./node_modules/.bin/jest --forceExit --silent --logHeapUsage", + "loadInitialData": "ts-node ./src/commands/loadInitialData.ts", + "loadPermission": "ts-node ./src/commands/loadPermissionData.ts", + "loadGrowthHackData": "ts-node ./src/commands/loadGrowthHackData.ts", + "loadTestData": "ts-node ./src/commands/loadTestData.ts", "initProject": "ts-node ./src/commands/initProject.ts", - "engageSubscriptions": "ts-node ./src/commands/engageSubscriptions.ts", "createGooglePubsubTopics": "ts-node ./src/commands/createGooglePubsubTopics.ts", + "customCommand": "ts-node ./src/commands/customCommand.ts", "resetMigrations": "ts-node ./src/commands/resetMigrations.ts", + "verifyCustomerEmails": "DEBUG=erxes-api:* ts-node ./src/commands/verifyCustomerEmails.ts", + "runEsCommand": "ts-node ./src/commands/runEsCommand.ts", + "trackTelemetry": "node ./dist/commands/trackTelemetry.js", "migrate": "NODE_ENV=command migrate --migrations-dir='./dist/migrations' --store='./db-migrate-store.js' up", "release": "release-it" }, @@ -40,97 +45,101 @@ "git add" ] }, - "jest": { - "collectCoverageFrom": [ - "src/**/*.{ts}", - "!src/index.ts", - "!src/db/factories.ts", - "!src/db/connection.ts", - "!src/data/schema/**", - "!src/data/resolvers/subscriptions/**", - "!src/data/index.ts", - "!src/data/utils.ts" - ] - }, "dependencies": { - "@axelspringer/graphql-google-pubsub": "^1.2.1", - "@google-cloud/pubsub": "0.18.0", - "@google-cloud/storage": "^2.5.0", - "apollo-server-express": "^2.3.1", + "@google-cloud/pubsub": "^1.1.5", + "@google-cloud/storage": "^4.0.0", + "@types/cors": "^2.8.4", + "@types/dotenv": "^4.0.3", + "@types/express": "^4.17.6", + "@types/formidable": "^1.0.31", + "@types/graphql": "^14.0.3", + "@types/ioredis": "^3.2.15", + "@types/jest": "^24.0.21", + "@types/json2csv": "^5.0.1", + "@types/mongodb": "^3.1.2", + "@types/mongoose": "^5.5.32", + "@types/request": "^2.47.1", + "@types/underscore": "^1.8.9", + "amqplib": "0.5.3", + "apollo-datasource-rest": "^0.5.1", + "apollo-server-express": "^2.9.7", "aws-sdk": "^2.151.0", "bcryptjs": "^2.4.3", - "body-parser": "^1.17.1", + "cookie": "^0.4.1", "cookie-parser": "^1.4.3", "cors": "^2.8.1", + "cote": "^1.0.0", + "csvtojson": "^2.0.10", "debug": "^4.1.1", "dotenv": "^4.0.0", - "email-deep-validator": "^3.1.0", - "express": "^4.15.2", + "elasticsearch": "^16.6.0", + "erxes-inmemory-storage": "^1.0.16", + "erxes-message-broker": "^1.0.17", + "erxes-telemetry": "^1.0.4", + "express": "^4.17.1", + "faker": "^4.1.0", "file-type": "^10.4.0", - "firebase-admin": "^7.2.0", + "firebase-admin": "^8.6.1", "formidable": "^1.1.1", + "generate-password": "^1.5.1", "git-repo-info": "^2.1.0", - "googleapis": "^33.0.0", "graphql": "^14.0.2", "graphql-redis-subscriptions": "^1.4.0", + "graphql-subscriptions": "^1.1.0", "graphql-tools": "^4.0.3", - "handlebars": "^4.0.14", + "handlebars": "^4.7.3", + "helmet": "^3.23.3", "ioredis": "^3.2.2", + "json2csv": "^5.0.1", "jsonwebtoken": "^8.1.0", "meteor-random": "^0.0.3", "moment": "^2.18.1", - "mongoose": "^5.2.16", + "moment-timezone": "^0.5.31", + "mongo-uri": "^0.1.2", + "mongoose": "5.7.5", "mongoose-type-email": "^1.0.5", "node-schedule": "^1.2.5", "nodemailer": "^4.1.3", - "oauth": "^0.9.15", "redis": "^2.8.0", "request": "^2.88.0", "requestify": "^0.2.5", "sha256": "^0.2.0", + "shelljs": "^0.8.3", "strip": "^3.0.0", - "twit": "^2.2.9", + "ts-node": "8.0.3", "underscore": "^1.8.3", + "uuid": "^3.3.3", + "uuid-by-string": "^3.0.2", "validator": "^9.0.0", - "xlsx-populate": "^1.14.0" + "vm2": "^3.9.2", + "xlsx-populate": "^1.20.1", + "xlsx-stream-reader": "^1.1.0", + "xss": "^1.0.6", + "migrate": "^1.6.2" }, "peerOptionalDependencies": { "kerberos": "^1.0.0" }, "devDependencies": { - "@release-it/conventional-changelog": "^1.1.0", - "@types/body-parser": "^1.17.0", - "@types/cors": "^2.8.4", - "@types/dotenv": "^4.0.3", - "@types/express": "^4.16.0", - "@types/formidable": "^1.0.31", - "@types/google-cloud__pubsub": "^0.18.1", - "@types/graphql": "^14.0.3", - "@types/ioredis": "^3.2.15", - "@types/jest": "^23.3.0", - "@types/mongodb": "^3.1.2", - "@types/mongoose": "^5.2.1", - "@types/q": "^1.5.0", - "@types/redis": "^2.8.13", - "@types/request": "^2.47.1", - "@types/underscore": "^1.8.9", - "faker": "^4.1.0", + "@release-it/conventional-changelog": "^2.0.0", "husky": "^0.13.4", - "jest": "^21.2.1", + "jest": "^24.9", "jest-tobetype": "^1.1.0", "lint-staged": "^3.6.0", - "migrate": "^1.6.2", + "mongodb-memory-server": "^6.0.2", "ora": "^3.4.0", "prettier": "^1.14.2", - "release-it": "^12.3.0", + "release-it": "^12.4.3", "sinon": "^7.2.2", - "snyk": "^1.192.4", + "supertest": "^5.0.0", "ts-jest": "^22.0.0", - "ts-node": "8.0.3", "ts-node-dev": "^1.0.0-pre.32", "tslint": "^5.8.0", "tslint-config-prettier": "^1.1.0", "tslint-config-standard": "^7.0.0", - "typescript": "^2.9.2" + "typescript": "^3.6.4" + }, + "engines": { + "node": ">=10.x.x" } } diff --git a/src/__tests__/activityDb.test.ts b/src/__tests__/activityDb.test.ts new file mode 100644 index 000000000..e025cef4e --- /dev/null +++ b/src/__tests__/activityDb.test.ts @@ -0,0 +1,152 @@ +import * as faker from 'faker'; +import { ActivityLogs, Customers, Deals, Segments } from '../db/models'; + +import { activityLogFactory, checklistFactory, customerFactory, dealFactory, segmentFactory } from '../db/factories'; +import './setup.ts'; + +describe('Test activity model', () => { + afterEach(async () => { + // Clearing test data + await ActivityLogs.deleteMany({}); + await Deals.deleteMany({}); + await Segments.deleteMany({}); + await Customers.deleteMany({}); + }); + + test('Activity add activity', async () => { + const contentId = faker.random.uuid(); + const contentType = 'customer'; + const createdBy = faker.random.uuid(); + const action = 'create'; + + const activity = await ActivityLogs.addActivityLog({ + contentId, + contentType, + createdBy, + action, + }); + + expect(activity).toBeDefined(); + expect(activity.contentId).toEqual(contentId); + expect(activity.contentType).toEqual(contentType); + expect(activity.createdBy).toEqual(createdBy); + expect(activity.action).toEqual(action); + }); + + test('Activity remove activity', async () => { + const activity = await activityLogFactory(); + + await ActivityLogs.removeActivityLog(activity.contentId); + + const count = await ActivityLogs.find({ contentId: activity.contentId }).countDocuments(); + + expect(count).toBe(0); + }); + + test('Activity create board item log', async () => { + const deal = await dealFactory({ sourceConversationId: '123' }); + + const activity = await ActivityLogs.createBoardItemLog({ item: deal, contentType: 'deal' }); + + expect(activity.contentId).toEqual(deal._id); + }); + + test('Activity create log from widget', async () => { + const item = await customerFactory({}); + + const activity1 = await ActivityLogs.createLogFromWidget('create-customer', item); + const activity2 = await ActivityLogs.createLogFromWidget('create-company', item); + + expect(activity1.contentId).toEqual(item._id); + expect(activity2.contentId).toEqual(item._id); + }); + + test('Activity create coc log', async () => { + const item = await customerFactory({ mergedIds: ['1', '2'] }); + const item2 = await customerFactory({ integrationId: '123', ownerId: undefined }); + + const activity1 = await ActivityLogs.createCocLog({ coc: item, contentType: 'customer' }); + const activity2 = await ActivityLogs.createCocLog({ coc: item2, contentType: 'customer' }); + + expect(activity1.contentId).toEqual(item._id); + expect(activity2.contentId).toEqual(item2._id); + }); + + test('Activity create board item movement log', async () => { + const item = await dealFactory({}); + + const activity1 = await ActivityLogs.createBoardItemMovementLog(item, 'deal', '123', {}); + + expect(activity1.contentId).toEqual(item._id); + }); + + test('Activity create board item segment log', async () => { + const customer = await customerFactory({}); + const segment1 = await segmentFactory({}); + const segment2 = await segmentFactory({}); + const segment3 = await segmentFactory({}); + + await ActivityLogs.create({ + contentType: 'customer', + action: 'segment', + contentId: customer._id, + content: { + id: segment1._id, + content: segment2, + }, + }); + + const activity1 = await ActivityLogs.createSegmentLog(segment1, [customer._id], 'customer'); + const activity2 = await ActivityLogs.createSegmentLog(segment2, [customer._id], 'customer'); + const activity3 = await ActivityLogs.createSegmentLog( + segment3, + [ + customer._id, + (await customerFactory({}))._id, + (await customerFactory({}))._id, + (await customerFactory({}))._id, + (await customerFactory({}))._id, + ], + 'customer', + 3, + ); + + expect(activity1).toBe(undefined); + expect(activity2.length).toEqual(1); + expect(activity3.length).toEqual(2); + }); + + test('Activity create assignee log', async () => { + const activity = await ActivityLogs.createAssigneLog({ + contentId: '123', + contentType: 'task', + userId: '123', + content: {}, + }); + + expect(activity.contentId).toEqual('123'); + }); + + test('Activity create checklist log', async () => { + const deal = await checklistFactory({ contentTypeId: '123' }); + const activity = await ActivityLogs.createChecklistLog({ + item: deal, + contentType: 'deal', + action: 'delete', + }); + + expect(activity.contentId).toEqual('123'); + }); + + test('Activity create archive log', async () => { + const deal = await dealFactory({}); + const activity = await ActivityLogs.createArchiveLog({ + item: deal, + contentType: 'deal', + action: 'archive', + userId: '123', + }); + + expect(activity.createdBy).toEqual('123'); + }); +}); diff --git a/src/__tests__/activityLogCronJob.test.ts b/src/__tests__/activityLogCronJob.test.ts deleted file mode 100644 index 7a1ec60bd..000000000 --- a/src/__tests__/activityLogCronJob.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { customerFactory, segmentFactory } from '../db/factories'; -import { ActivityLogs } from '../db/models'; -import { - ACTIVITY_ACTIONS, - ACTIVITY_CONTENT_TYPES, - ACTIVITY_PERFORMER_TYPES, - ACTIVITY_TYPES, -} from '../db/models/definitions/constants'; - -import { createActivityLogsFromSegments } from '../cronJobs/activityLogs'; -import './setup.ts'; - -describe('test activityLogsCronJob', () => { - test('test if it is working as intended', async () => { - // check if the activity log is being created ================== - const nameEqualsConditions = [ - { - type: 'string', - dateUnit: 'days', - value: 'John Smith', - operator: 'c', - field: 'firstName', - }, - ]; - - const customer = await customerFactory({ firstName: 'john smith' }); - - const segment = await segmentFactory({ - contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, - conditions: nameEqualsConditions, - }); - - await createActivityLogsFromSegments(); - - expect(await ActivityLogs.find().countDocuments()).toBe(1); - - const aLog = await ActivityLogs.findOne(); - - if (!aLog) { - throw new Error('Activity log is empty'); - } - - expect(aLog.activity.toObject()).toEqual({ - type: ACTIVITY_TYPES.SEGMENT, - action: ACTIVITY_ACTIONS.CREATE, - content: segment.name, - id: segment._id, - }); - expect(aLog.contentType.toObject()).toEqual({ - type: ACTIVITY_CONTENT_TYPES.CUSTOMER, - id: customer._id, - }); - - if (!aLog.performedBy) { - throw new Error('Activity log is empty'); - } - - expect(aLog.performedBy.toObject()).toEqual({ - type: ACTIVITY_PERFORMER_TYPES.SYSTEM, - }); - - // check if the second activity log is being created - // also check if the duplicate activity log is - // not being created for the former customer ================ - const nameEqualsConditions2 = [ - { - type: 'string', - dateUnit: 'days', - value: 'jane smith', - operator: 'c', - field: 'firstName', - }, - ]; - - await customerFactory({ firstName: 'jane smith' }); - await segmentFactory({ - contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, - conditions: nameEqualsConditions2, - }); - - await createActivityLogsFromSegments(); - - expect(await ActivityLogs.find().countDocuments()).toBe(2); - }); -}); diff --git a/src/__tests__/activityLogQueries.test.ts b/src/__tests__/activityLogQueries.test.ts index ea617f659..14a0d8873 100644 --- a/src/__tests__/activityLogQueries.test.ts +++ b/src/__tests__/activityLogQueries.test.ts @@ -1,71 +1,338 @@ -import * as faker from 'faker'; -import { graphqlRequest } from '../db/connection'; -import { activityLogFactory } from '../db/factories'; -import { ActivityLogs } from '../db/models'; -import { ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, ACTIVITY_TYPES } from '../db/models/definitions/constants'; - -import './setup.ts'; - -describe('activityLogQueries', () => { - const commonParamDefs = ` - $contentType: String!, - $contentId: String!, - $activityType: String!, - $limit: Int, - `; - - const commonParams = ` - contentType: $contentType - contentId: $contentId - activityType: $activityType - limit: $limit - `; - - const qryActivityLogs = ` - query activityLogs(${commonParamDefs}) { - activityLogs(${commonParams}) { - _id - action - id - createdAt - content - by { - type - details { - avatar - fullName - position - } - } - } - } - `; - - afterEach(async () => { - // Clearing test data - await ActivityLogs.deleteMany({}); - }); - - test('Activity log list', async () => { - const contentType = ACTIVITY_CONTENT_TYPES.CUSTOMER; - const activityType = ACTIVITY_TYPES.INTERNAL_NOTE; - const contentId = faker.random.uuid(); - - for (let i = 0; i < 3; i++) { - await activityLogFactory({ - activity: { type: activityType, action: ACTIVITY_ACTIONS.CREATE }, - contentType: { type: contentType, id: contentId }, - }); - } - - const args = { contentType, activityType, contentId }; - - const responses = await graphqlRequest(qryActivityLogs, 'activityLogs', args); - - expect(responses.length).toBe(3); - - const responsesWithLimit = await graphqlRequest(qryActivityLogs, 'activityLogs', { ...args, limit: 2 }); - - expect(responsesWithLimit.length).toBe(2); - }); -}); +import * as faker from 'faker'; +import { graphqlRequest } from '../db/connection'; +import { + activityLogFactory, + brandFactory, + companyFactory, + conformityFactory, + conversationFactory, + customerFactory, + dealFactory, + engageMessageFactory, + growthHackFactory, + integrationFactory, + internalNoteFactory, + stageFactory, + taskFactory, + ticketFactory, + userFactory, +} from '../db/factories'; +import { + ActivityLogs, + Brands, + Companies, + Conformities, + Customers, + GrowthHacks, + Tasks, + Tickets, + Users, +} from '../db/models'; + +import { IntegrationsAPI } from '../data/dataSources'; +import './setup.ts'; + +describe('activityLogQueries', () => { + let brand; + let integration; + let deal; + let ticket; + let growtHack; + let task; + + const commonParamDefs = ` + $contentType: String!, + $contentId: String!, + $activityType: String, + $limit: Int, + `; + + const commonParams = ` + contentType: $contentType + contentId: $contentId + activityType: $activityType + limit: $limit + `; + + const qryActivityLogs = ` + query activityLogs(${commonParamDefs}) { + activityLogs(${commonParams}) { + _id + action + contentId + contentType + content + createdAt + createdBy + + createdByDetail + contentDetail + contentTypeDetail + } + } + `; + + beforeEach(async () => { + brand = await brandFactory(); + integration = await integrationFactory({ + brandId: brand._id, + }); + deal = await dealFactory(); + ticket = await ticketFactory(); + growtHack = await growthHackFactory(); + task = await taskFactory(); + }); + + afterEach(async () => { + // Clearing test data + await ActivityLogs.deleteMany({}); + await Users.deleteMany({}); + await GrowthHacks.deleteMany({}); + await Tasks.deleteMany({}); + await Companies.deleteMany({}); + await Customers.deleteMany({}); + await Tickets.deleteMany({}); + await Brands.deleteMany({}); + await Conformities.deleteMany({}); + }); + + test('Activity log', async () => { + const contentId = faker.random.uuid(); + const contentType = 'customer'; + + await activityLogFactory({ contentId, contentType }); + await activityLogFactory({ contentId, contentType }); + await activityLogFactory({ contentId, contentType }); + + const args = { contentId, contentType }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(3); + }); + + test('Activity log content type checklist & checklist', async () => { + const contentId = faker.random.uuid(); + + await activityLogFactory({ contentId, contentType: 'checklist' }); + await activityLogFactory({ contentId, contentType: 'checklistitem' }); + + const args1 = { contentId, contentType: 'checklist' }; + const args2 = { contentId, contentType: 'checklistitem' }; + + const response1 = await graphqlRequest(qryActivityLogs, 'activityLogs', args1); + const response2 = await graphqlRequest(qryActivityLogs, 'activityLogs', args2); + + expect(response1.length).toBe(2); + expect(response2.length).toBe(2); + }); + + test('Activity log with all activity types', async () => { + const customer = await customerFactory({}); + await conformityFactory({ + mainType: 'customer', + mainTypeId: customer._id, + relType: 'task', + relTypeId: task._id, + }); + + await conversationFactory({ customerId: customer._id }); + await internalNoteFactory({ contentTypeId: customer._id }); + await engageMessageFactory({ customerIds: [customer._id], method: 'email' }); + + const dataSources = { IntegrationsAPI: new IntegrationsAPI() }; + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + + spy.mockImplementation(() => Promise.resolve()); + + const activityTypes = [ + { type: 'conversation', content: 'company' }, + { type: 'email', content: 'email' }, + { type: 'internal_note', content: 'internal_note' }, + { type: 'task', content: 'task' }, + ]; + + for (const activityType of activityTypes) { + const args = { contentId: customer._id, contentType: 'customer', activityType: activityType.type }; + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args, { dataSources }); + expect(response.length).toBe(1); + } + }); + + test('Activity log with created by user', async () => { + const user = await userFactory(); + const contentId = faker.random.uuid(); + const contentType = 'customer'; + const createdBy = user._id; + + const doc = { + contentId, + contentType, + createdBy, + }; + + await activityLogFactory(doc); + + const args = { contentId, contentType }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + }); + + test('Activity log with created by integration', async () => { + const contentId = faker.random.uuid(); + const contentType = 'customer'; + const createdBy = integration._id; + + const doc = { + contentId, + contentType, + createdBy, + }; + + await activityLogFactory(doc); + + const args = { contentId, contentType }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + }); + + test('Activity log content type detail', async () => { + const createdBy = integration._id; + + const types = [ + { type: 'deal', content: deal }, + { type: 'ticket', content: ticket }, + { type: 'task', content: task }, + { type: 'growthHack', content: growtHack }, + ]; + + for (const type of types) { + const doc = { + contentId: type.content._id, + contentType: type.type, + createdBy, + }; + + await activityLogFactory(doc); + + const args = { contentId: type.content._id, contentType: type.type }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + } + }); + + test('Activity log action merge', async () => { + const customer = await customerFactory(); + const company = await companyFactory(); + + const types = [ + { type: 'company', content: company }, + { type: 'customer', content: customer }, + ]; + + for (const type of types) { + const doc = { + contentId: type.content._id, + contentType: type.type, + action: 'merge', + content: [], + }; + + await activityLogFactory(doc); + + const args = { contentId: type.content._id, contentType: type.type }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + } + }); + + test('Activity log action moved', async () => { + const stage1 = await stageFactory(); + const stage2 = await stageFactory(); + + const types = [ + { type: 'deal', content: deal }, + { type: 'ticket', content: ticket }, + { type: 'task', content: task }, + { type: 'growthHack', content: growtHack }, + ]; + + for (const type of types) { + const doc1 = { + contentId: type.content._id, + contentType: type.type, + action: 'moved', + content: { + oldStageId: stage1._id, + destinationStageId: stage2._id, + }, + }; + + const doc2 = { + contentId: type.content._id, + contentType: type.type, + action: 'moved', + content: { + oldStageId: '', + destinationStageId: '', + }, + }; + + await activityLogFactory(doc1); + await activityLogFactory(doc2); + + const args = { contentId: type.content._id, contentType: type.type }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(2); + } + }); + + test('Activity log action convert', async () => { + const types = [{ type: 'task', content: task }]; + + for (const type of types) { + const doc = { + contentId: type.content._id, + contentType: type.type, + action: 'convert', + }; + + await activityLogFactory(doc); + + const args = { contentId: type.content._id, contentType: type.type }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + } + }); + + test('Activity log action assignee', async () => { + const deal1 = await dealFactory(); + + const doc = { + contentId: deal1._id, + contentType: 'deal', + action: 'assignee', + content: [], + }; + + await activityLogFactory(doc); + + const args = { contentId: deal1._id, contentType: 'deal' }; + + const response = await graphqlRequest(qryActivityLogs, 'activityLogs', args); + + expect(response.length).toBe(1); + }); +}); diff --git a/src/__tests__/boardDb.test.ts b/src/__tests__/boardDb.test.ts index 06eaae1ab..ae008100e 100644 --- a/src/__tests__/boardDb.test.ts +++ b/src/__tests__/boardDb.test.ts @@ -1,5 +1,14 @@ -import { boardFactory, dealFactory, pipelineFactory, stageFactory, userFactory } from '../db/factories'; -import { Boards, Deals, Pipelines, Stages } from '../db/models'; +import { + boardFactory, + dealFactory, + formFactory, + pipelineFactory, + pipelineTemplateFactory, + stageFactory, + userFactory, +} from '../db/factories'; +import { Boards, Deals, Forms, Pipelines, Stages } from '../db/models'; +import { getNewOrder } from '../db/models/boardUtils'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; import { IUserDocument } from '../db/models/definitions/users'; @@ -25,7 +34,19 @@ describe('Test board model', () => { await Boards.deleteMany({}); await Pipelines.deleteMany({}); await Stages.deleteMany({}); - await Deals.deleteMany({}); + await Pipelines.deleteMany({}); + }); + + test('Get board', async () => { + try { + await Boards.getBoard('fakeId'); + } catch (e) { + expect(e.message).toBe('Board not found'); + } + + const response = await Boards.getBoard(board._id); + + expect(response).toBeDefined(); }); // Test deal board @@ -57,13 +78,14 @@ describe('Test board model', () => { }); test('Remove board', async () => { - const doc = { boardId: 'boardId' }; + const removeBoard = await boardFactory(); - await Pipelines.updateMany({}, { $set: doc }); + const removedPipeline = await pipelineFactory({ boardId: removeBoard._id }); - const isDeleted = await Boards.removeBoard(board.id); + const isDeleted = await Boards.removeBoard(removeBoard.id); expect(isDeleted).toBeTruthy(); + expect(await Stages.findOne({ _id: removedPipeline._id })).toBeNull(); }); test('Remove board not found', async () => { @@ -78,16 +100,6 @@ describe('Test board model', () => { } }); - test("Can't remove a board", async () => { - expect.assertions(1); - - try { - await Boards.removeBoard(board._id); - } catch (e) { - expect(e.message).toEqual("Can't remove a board"); - } - }); - // Test deal pipeline test('Create pipeline', async () => { const createdPipeline = await Pipelines.createPipeline( @@ -117,6 +129,42 @@ describe('Test board model', () => { expect(createdPipeline.userId).toEqual(user._id); }); + test('Create pipeline by templateId', async () => { + const form = await formFactory(); + const formStage = await stageFactory({ formId: form._id }); + + const template = await pipelineTemplateFactory({ + stages: [formStage], + }); + + const createdPipeline = await Pipelines.createPipeline( + { + boardId: board._id, + name: pipeline.name, + type: pipeline.type, + templateId: template._id, + }, + [], + ); + + expect(createdPipeline).toBeDefined(); + expect(createdPipeline.name).toEqual(pipeline.name); + expect(createdPipeline.type).toEqual(pipeline.type); + expect(createdPipeline.templateId).toEqual(template._id); + }); + + test('Create pipeline without stages', async () => { + const createdPipeline = await Pipelines.createPipeline({ + boardId: board._id, + name: pipeline.name, + type: pipeline.type, + }); + + expect(createdPipeline).toBeDefined(); + expect(createdPipeline.name).toEqual(pipeline.name); + expect(createdPipeline.type).toEqual(pipeline.type); + }); + test('Update pipeline', async () => { const args = { name: 'deal pipeline', @@ -146,6 +194,49 @@ describe('Test board model', () => { expect(stages.length).toEqual(0); }); + test('Update pipeline by templateId', async () => { + const form = await formFactory(); + const formStage = await stageFactory({ formId: form._id }); + + const template = await pipelineTemplateFactory({ + stages: [formStage], + }); + + const args = { + boardId: board._id, + templateId: template._id, + type: 'deal', + }; + + const pipelineObj = await pipelineFactory({ templateId: 'fakeId' }); + + let updatedPipeline = await Pipelines.updatePipeline(pipelineObj._id, args, []); + + expect(updatedPipeline).toBeDefined(); + expect(updatedPipeline.templateId).toBe(template._id); + + const pipelineSameObj = await pipelineFactory({ templateId: args.templateId }); + + updatedPipeline = await Pipelines.updatePipeline(pipelineSameObj._id, args, []); + + expect(updatedPipeline).toBeDefined(); + expect(updatedPipeline.templateId).toBe(template._id); + }); + + test('Update pipeline without stages', async () => { + const args = { + boardId: board._id, + name: 'updated', + type: 'deal', + }; + + const updatedPipeline = await Pipelines.updatePipeline(pipeline._id, args); + + expect(updatedPipeline).toBeDefined(); + expect(updatedPipeline.name).toEqual(args.name); + expect(updatedPipeline.type).toEqual(args.type); + }); + test('Update pipeline orders', async () => { const pipelineToOrder = await pipelineFactory({}); @@ -159,12 +250,23 @@ describe('Test board model', () => { }); test('Remove pipeline', async () => { - const doc = { pipelineId: 'pipelineId' }; + const removePipeline = await pipelineFactory(); + const removedStage = await stageFactory({ pipelineId: removePipeline._id }); - await Stages.updateMany({}, { $set: doc }); + const isDeleted = await Pipelines.removePipeline(removePipeline.id, true); - const isDeleted = await Pipelines.removePipeline(pipeline.id); expect(isDeleted).toBeTruthy(); + expect(await Stages.findOne({ _id: removedStage._id })).toBeNull(); + }); + + test('Remove pipeline with stage items', async () => { + const removePipeline = await pipelineFactory(); + const removedStage = await stageFactory({ pipelineId: removePipeline._id }); + + const isDeleted = await Pipelines.removePipeline(removePipeline.id, false); + + expect(isDeleted).toBeTruthy(); + expect(await Stages.findOne({ _id: removedStage._id })).toBeNull(); }); test('Remove pipeline not found', async () => { @@ -179,17 +281,33 @@ describe('Test board model', () => { } }); - test("Can't remove a pipeline", async () => { - expect.assertions(1); + test('Watch pipeline', async () => { + await Pipelines.watchPipeline(pipeline._id, true, user._id); + + const watchedPipeline = await Pipelines.getPipeline(pipeline._id); + + expect(watchedPipeline.watchedUserIds).toContain(user._id); + // testing unwatch + await Pipelines.watchPipeline(pipeline._id, false, user._id); + + const unwatchedPipeline = await Pipelines.getPipeline(pipeline._id); + + expect(unwatchedPipeline.watchedUserIds).not.toContain(user._id); + }); + + test('Get stage', async () => { try { - await Pipelines.removePipeline(pipeline._id); + await Stages.getStage('fakeId'); } catch (e) { - expect(e.message).toEqual("Can't remove a pipeline"); + expect(e.message).toBe('Stage not found'); } + + const response = await Stages.getStage(stage._id); + + expect(response).toBeDefined(); }); - // Test deal stage test('Create stage', async () => { const createdStage = await Stages.createStage({ name: stage.name, @@ -206,26 +324,37 @@ describe('Test board model', () => { expect(createdStage.userId).toEqual(user._id); }); + test('Remove stage', async () => { + const stageNoItem = await stageFactory(); + const isDeleted = await Stages.removeStage(stageNoItem._id); + + expect(isDeleted).toBeTruthy(); + }); + + test('Remove stage with form', async () => { + const form = await formFactory(); + + const stageWithForm = await stageFactory({ formId: form._id }); + + const isDeleted = await Stages.removeStage(stageWithForm._id); + + expect(isDeleted).toBeTruthy(); + expect(await Forms.findOne({ _id: form._id })).toBeNull(); + }); + test('Update stage', async () => { const stageName = 'Update stage name'; const updatedStage = await Stages.updateStage(stage._id, { name: stageName, userId: user._id, type: 'deal', + pipelineId: pipeline._id, }); expect(updatedStage).toBeDefined(); expect(updatedStage.name).toEqual(stageName); }); - test('Change stage', async () => { - const pipelineToUpdate = await pipelineFactory({}); - const changedStage = await Stages.changeStage(stage._id, pipelineToUpdate._id); - - expect(changedStage).toBeDefined(); - expect(changedStage.pipelineId).toEqual(pipelineToUpdate._id); - }); - test('Update stage orders', async () => { const stageToOrder = await stageFactory({}); @@ -238,33 +367,30 @@ describe('Test board model', () => { expect(updatedStageToOrder.order).toBe(9); }); - test('Remove stage', async () => { - await Deals.updateMany({}, { $set: { stageId: 'stageId' } }); - - const isDeleted = await Stages.removeStage(stage.id); + test('Update stage orders when orders length is zero', async () => { + const response = await Stages.updateOrder([]); - expect(isDeleted).toBeTruthy(); + expect(response.length).toBe(0); }); - test('Remove stage not found', async () => { - expect.assertions(1); + test('itemOrder test', async () => { + let aboveItemId = ''; + const newStage = await stageFactory(); + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBe(100); - const fakeStageId = 'fakeStageId'; + const firstDeal = await dealFactory({ stageId: newStage._id, order: 100 }); + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeGreaterThan(0); + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeLessThan(100); - try { - await Stages.removeStage(fakeStageId); - } catch (e) { - expect(e.message).toEqual('Stage not found'); - } - }); + aboveItemId = (await dealFactory({ stageId: newStage._id, order: 99 }))._id; + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeGreaterThan(99); + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeLessThan(100); - test("Can't remove a stage", async () => { - expect.assertions(1); + expect(await getNewOrder({ aboveItemId: firstDeal._id, stageId: newStage._id, collection: Deals })).toBe(110); - try { - await Stages.removeStage(stage._id); - } catch (e) { - expect(e.message).toEqual("Can't remove a stage"); - } + // duplicated then recall getNewOrder + aboveItemId = (await dealFactory({ stageId: newStage._id, order: 99.99999999999999 }))._id; + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeGreaterThan(110); + expect(await getNewOrder({ aboveItemId, stageId: newStage._id, collection: Deals })).toBeLessThan(120); }); }); diff --git a/src/__tests__/boardMutations.test.ts b/src/__tests__/boardMutations.test.ts index fa37a0ff4..653e09b4e 100644 --- a/src/__tests__/boardMutations.test.ts +++ b/src/__tests__/boardMutations.test.ts @@ -1,8 +1,9 @@ import { graphqlRequest } from '../db/connection'; import { boardFactory, pipelineFactory, stageFactory, userFactory } from '../db/factories'; -import { Boards, Deals, Pipelines, Stages } from '../db/models'; +import { Boards, Deals, Pipelines, Stages, Users } from '../db/models'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; +import { BOARD_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('Test boards mutations', () => { @@ -17,7 +18,9 @@ describe('Test boards mutations', () => { $stages: JSON, $type: String! $visibility: String! - $bgColor: String + $bgColor: String, + $excludeCheckUserIds: [String], + $memberIds: [String] `; const commonPipelineParams = ` @@ -26,13 +29,15 @@ describe('Test boards mutations', () => { stages: $stages type: $type visibility: $visibility - bgColor: $bgColor + bgColor: $bgColor, + excludeCheckUserIds: $excludeCheckUserIds, + memberIds: $memberIds `; beforeEach(async () => { // Creating test data board = await boardFactory(); - pipeline = await pipelineFactory({ boardId: board._id }); + pipeline = await pipelineFactory({ boardId: board._id, watchedUserIds: [] }); stage = await stageFactory({ pipelineId: pipeline._id }); context = { user: await userFactory({}) }; }); @@ -43,6 +48,7 @@ describe('Test boards mutations', () => { await Pipelines.deleteMany({}); await Stages.deleteMany({}); await Deals.deleteMany({}); + await Users.deleteMany({}); }); test('Create board', async () => { @@ -77,10 +83,33 @@ describe('Test boards mutations', () => { } `; - const updatedBoard = await graphqlRequest(mutation, 'boardsEdit', args, context); + const response = await graphqlRequest(mutation, 'boardsEdit', args); - expect(updatedBoard.name).toEqual(args.name); - expect(updatedBoard.type).toEqual(args.type); + expect(response._id).toBe(args._id); + expect(response.name).toBe(args.name); + expect(response.type).toBe(args.type); + }); + + test('Update board (Error: Permission required)', async () => { + const args = { _id: board._id, name: 'deal board', type: 'deal' }; + + const mutation = ` + mutation boardsEdit($_id: String!, $name: String!, $type: String!) { + boardsEdit(name: $name, _id: $_id, type: $type) { + _id + name + type + } + } + `; + + const user = await userFactory({ isOwner: false }); + + try { + await graphqlRequest(mutation, 'boardsEdit', args, { user }); + } catch (e) { + expect(e[0].message).toBe('Permission required'); + } }); test('Remove board', async () => { @@ -99,6 +128,9 @@ describe('Test boards mutations', () => { }); test('Create pipeline', async () => { + const user1 = await userFactory(); + const user2 = await userFactory(); + const args = { name: 'deal pipeline', type: 'deal', @@ -106,6 +138,8 @@ describe('Test boards mutations', () => { stages: [stage.toJSON()], visibility: 'public', bgColor: 'aaa', + excludeCheckUserIds: [user1._id], + memberIds: [user2._id], }; const mutation = ` @@ -117,6 +151,8 @@ describe('Test boards mutations', () => { boardId bgColor visibility + excludeCheckUserIds + memberIds } } `; @@ -136,6 +172,8 @@ describe('Test boards mutations', () => { expect(createdPipeline.visibility).toEqual(args.visibility); expect(createdPipeline.boardId).toEqual(board._id); expect(createdPipeline.bgColor).toEqual(args.bgColor); + expect(createdPipeline.excludeCheckUserIds.length).toBe(1); + expect(createdPipeline.memberIds.length).toBe(1); }); test('Update pipeline', async () => { @@ -183,7 +221,10 @@ describe('Test boards mutations', () => { const pipelineToUpdate = await pipelineFactory({}); const args = { - orders: [{ _id: pipeline._id, order: 9 }, { _id: pipelineToUpdate._id, order: 3 }], + orders: [ + { _id: pipeline._id, order: 9 }, + { _id: pipelineToUpdate._id, order: 3 }, + ], }; const mutation = ` @@ -245,16 +286,22 @@ describe('Test boards mutations', () => { } `; - await graphqlRequest(mutation, 'pipelinesRemove', { _id: pipeline._id }, context); + const user = await userFactory(); + const pipe = await pipelineFactory({ watchedUserIds: [user._id] }); + + await graphqlRequest(mutation, 'pipelinesRemove', { _id: pipe._id }, context); - expect(await Pipelines.findOne({ _id: pipeline._id })).toBe(null); + expect(await Pipelines.findOne({ _id: pipe._id })).toBe(null); }); test('Stage update orders', async () => { const stageToUpdate = await stageFactory({}); const args = { - orders: [{ _id: stage._id, order: 9 }, { _id: stageToUpdate._id, order: 3 }], + orders: [ + { _id: stage._id, order: 9 }, + { _id: stageToUpdate._id, order: 3 }, + ], }; const mutation = ` @@ -271,4 +318,35 @@ describe('Test boards mutations', () => { expect(updatedStage.order).toBe(3); expect(updatedStageToOrder.order).toBe(9); }); + + test('Edit stage', async () => { + const mutation = ` + mutation stagesEdit($_id: String!, $type: String, $name: String) { + stagesEdit(_id: $_id, type: $type, name: $name) { + _id + name + } + } + `; + + const updated = await graphqlRequest(mutation, 'stagesEdit', { + _id: stage._id, + type: BOARD_TYPES.DEAL, + name: 'updated', + }); + + expect(updated.name).toBe('updated'); + }); + + test('Remove stage', async () => { + const mutation = ` + mutation stagesRemove($_id: String!) { + stagesRemove(_id: $_id) + } + `; + + await graphqlRequest(mutation, 'stagesRemove', { _id: stage._id }); + + expect(await Stages.findOne({ _id: stage._id })).toBe(null); + }); }); diff --git a/src/__tests__/boardQueries.test.ts b/src/__tests__/boardQueries.test.ts index ab23a28d9..45ee77b28 100644 --- a/src/__tests__/boardQueries.test.ts +++ b/src/__tests__/boardQueries.test.ts @@ -1,7 +1,19 @@ import { graphqlRequest } from '../db/connection'; -import { boardFactory, pipelineFactory, stageFactory } from '../db/factories'; +import { + boardFactory, + conversationFactory, + dealFactory, + pipelineFactory, + productFactory, + stageFactory, + taskFactory, + ticketFactory, + userFactory, +} from '../db/factories'; import { Boards, Pipelines, Stages } from '../db/models'; +import moment = require('moment'); +import { BOARD_STATUSES, BOARD_TYPES, PIPELINE_VISIBLITIES, PROBABILITY } from '../db/models/definitions/constants'; import './setup.ts'; describe('boardQueries', () => { @@ -9,20 +21,63 @@ describe('boardQueries', () => { _id name type + pipelines { + _id + } `; const commonPipelineTypes = ` _id name type + visibility + members { _id } + isWatched + state + itemsTotalCount `; const commonStageTypes = ` _id name type + amount + itemsTotalCount + initialDealsTotalCount + inProcessDealsTotalCount + stayedDealsTotalCount + compareNextStage `; + const detailQuery = ` + query boardDetail($_id: String!) { + boardDetail(_id: $_id) { + ${commonBoardTypes} + } + } + `; + + const pipelineQry = ` + query pipelines($boardId: String, $type: String, $perPage: Int, $page: Int) { + pipelines(boardId: $boardId, type: $type, perPage: $perPage, page: $page) { + ${commonPipelineTypes} + } + } + `; + + const stateCountQry = ` + query pipelineStateCount($boardId: String, $type: String) { + pipelineStateCount(boardId: $boardId, type: $type) + } + `; + + const dateBuilder = day => + new Date( + moment() + .add(day, 'days') + .format('YYYY-MM-DD'), + ); + afterEach(async () => { // Clearing test data await Boards.deleteMany({}); @@ -48,22 +103,62 @@ describe('boardQueries', () => { expect(response.length).toBe(3); }); - test('Board detail', async () => { - const board = await boardFactory(); + test('Board count', async () => { + const boardOne = await boardFactory({ name: 'A' }); + await pipelineFactory({ boardId: boardOne._id }); + await pipelineFactory({ boardId: boardOne._id }); + + const boardTwo = await boardFactory({ name: 'B' }); + await pipelineFactory({ boardId: boardTwo._id }); - const args = { _id: board._id }; + const boardThree = await boardFactory({ name: 'C' }); const qry = ` - query boardDetail($_id: String!) { - boardDetail(_id: $_id) { - ${commonBoardTypes} + query boardCounts($type: String!) { + boardCounts(type: $type) { + _id + name + count } } `; - const response = await graphqlRequest(qry, 'boardDetail', args); + const response = await graphqlRequest(qry, 'boardCounts', { type: 'deal' }); + + expect(response[0].name).toBe('All'); + expect(response[0].count).toBe(3); + + expect(response[1].name).toBe(boardOne.name); + expect(response[1].count).toBe(2); + + expect(response[2].name).toBe(boardTwo.name); + expect(response[2].count).toBe(1); + + expect(response[3].name).toBe(boardThree.name); + expect(response[3].count).toBe(0); + }); + + test('Board detail', async () => { + const board = await boardFactory(); + + const response = await graphqlRequest(detailQuery, 'boardDetail', { _id: board._id }); expect(response._id).toBe(board._id); + expect(response.name).toBe(board.name); + expect(response.type).toBe(board.type); + expect(response.pipelines.length).toBe(0); + }); + + test('Board detail (private pipeline)', async () => { + const board = await boardFactory(); + + await pipelineFactory({ boardId: board._id }); + await pipelineFactory({ boardId: board._id, visibility: 'private' }); + + const user = await userFactory({ isOwner: false }); + const response = await graphqlRequest(detailQuery, 'boardDetail', { _id: board._id }, { user }); + + expect(response.pipelines.length).toBe(1); }); test('Board get last', async () => { @@ -77,51 +172,194 @@ describe('boardQueries', () => { } `; - const response = await graphqlRequest(qry, 'boardGetLast', { type: 'deal' }); + const response = await graphqlRequest(qry, 'boardGetLast', { type: BOARD_TYPES.DEAL }); expect(board._id).toBe(response._id); }); test('Pipelines', async () => { + await pipelineFactory(); + await pipelineFactory(); + await pipelineFactory(); + + const response = await graphqlRequest(pipelineQry, 'pipelines'); + + expect(response.length).toBe(3); + }); + + test('Pipelines with filter', async () => { const board = await boardFactory(); + const args = { boardId: board._id, type: 'deal' }; - const args = { boardId: board._id }; + await pipelineFactory(args); + await pipelineFactory(args); + await pipelineFactory(); + + const response = await graphqlRequest(pipelineQry, 'pipelines', args); + + expect(response.length).toBe(2); + }); + + test('Pipelines with pagination', async () => { + const board = await boardFactory(); + const args = { boardId: board._id, type: 'deal' }; await pipelineFactory(args); await pipelineFactory(args); await pipelineFactory(args); + const response = await graphqlRequest(pipelineQry, 'pipelines', { ...args, perPage: 2, page: 1 }); + + expect(response.length).toBe(2); + }); + + test('Pipeline detail', async () => { const qry = ` - query pipelines($boardId: String!) { - pipelines(boardId: $boardId) { + query pipelineDetail($_id: String!) { + pipelineDetail(_id: $_id) { ${commonPipelineTypes} } } `; + const user = await userFactory(); - const response = await graphqlRequest(qry, 'pipelines', args); + const dealPipeline = await pipelineFactory({ + type: BOARD_TYPES.DEAL, + visibility: PIPELINE_VISIBLITIES.PRIVATE, + memberIds: [user._id], + watchedUserIds: [user._id], + }); - expect(response.length).toBe(3); + let response = await graphqlRequest(qry, 'pipelineDetail', { _id: dealPipeline._id }, { user }); + + expect(response._id).toBe(dealPipeline._id); + expect(response.visibility).toBe(PIPELINE_VISIBLITIES.PRIVATE); + expect(response.members[0]._id).toBe(user._id); + expect(response.isWatched).toBe(true); + + const ticketPipeline = await pipelineFactory({ type: BOARD_TYPES.TICKET }); + response = await graphqlRequest(qry, 'pipelineDetail', { _id: ticketPipeline._id }); + + expect(response._id).toBe(ticketPipeline._id); + expect(response.itemsTotalCount).toBe(0); + + const taskPipeline = await pipelineFactory({ type: BOARD_TYPES.TASK }); + response = await graphqlRequest(qry, 'pipelineDetail', { _id: taskPipeline._id }); + + expect(response._id).toBe(taskPipeline._id); + expect(response.itemsTotalCount).toBe(0); + + const growthHackPipeline = await pipelineFactory({ type: BOARD_TYPES.GROWTH_HACK }); + response = await graphqlRequest(qry, 'pipelineDetail', { _id: growthHackPipeline._id }); + + expect(response._id).toBe(growthHackPipeline._id); + expect(response.itemsTotalCount).toBe(0); + }); + + test('Get state by startDate and endDate', async () => { + const qry = ` + query pipelineDetail($_id: String!) { + pipelineDetail(_id: $_id) { + ${commonPipelineTypes} + } + } + `; + const user = await userFactory(); + + let startDate = new Date( + moment() + .add(-2, 'days') + .format('YYYY-MM-DD'), + ); + let endDate = new Date( + moment() + .add(-1, 'days') + .format('YYYY-MM-DD'), + ); + + const completedPipeline = await pipelineFactory({ startDate, endDate }); + + let response = await graphqlRequest(qry, 'pipelineDetail', { _id: completedPipeline._id }, { user }); + + expect(response._id).toBe(completedPipeline._id); + expect(response.state).toBe('Completed'); + + startDate = new Date( + moment() + .add(-2, 'days') + .format('YYYY-MM-DD'), + ); + endDate = new Date( + moment() + .add(5, 'days') + .format('YYYY-MM-DD'), + ); + + const inProgressPipeline = await pipelineFactory({ startDate, endDate }); + + response = await graphqlRequest(qry, 'pipelineDetail', { _id: inProgressPipeline._id }, { user }); + + expect(response._id).toBe(inProgressPipeline._id); + expect(response.state).toBe('In progress'); + + startDate = new Date( + moment() + .add(2, 'days') + .format('YYYY-MM-DD'), + ); + endDate = new Date( + moment() + .add(5, 'days') + .format('YYYY-MM-DD'), + ); + + const notStartedPipeline = await pipelineFactory({ startDate, endDate }); + + response = await graphqlRequest(qry, 'pipelineDetail', { _id: notStartedPipeline._id }, { user }); + + expect(response._id).toBe(notStartedPipeline._id); + expect(response.state).toBe('Not started'); }); test('Stages', async () => { const pipeline = await pipelineFactory(); - const args = { pipelineId: pipeline._id }; + const args = { pipelineId: pipeline._id, probability: PROBABILITY.LOST, status: BOARD_STATUSES.ACTIVE }; await stageFactory(args); await stageFactory(args); await stageFactory(args); const qry = ` - query stages($pipelineId: String!) { - stages(pipelineId: $pipelineId) { + query stages($pipelineId: String!, $isNotLost: Boolean, $isAll: Boolean) { + stages(pipelineId: $pipelineId, isNotLost: $isNotLost, isAll: $isAll) { ${commonStageTypes} } } `; - const response = await graphqlRequest(qry, 'stages', args); + const filter = { pipelineId: pipeline._id, isNotLost: false, isAll: false }; + + let response = await graphqlRequest(qry, 'stages', filter); + + expect(response.length).toBe(3); + + args.probability = PROBABILITY.WON; + + await stageFactory({ ...args, order: 1 }); + await stageFactory({ ...args, order: 2 }); + + filter.isNotLost = true; + response = await graphqlRequest(qry, 'stages', filter); + + expect(response.length).toBe(2); + + args.status = BOARD_STATUSES.ARCHIVED; + + await stageFactory(args); + + filter.isAll = true; + response = await graphqlRequest(qry, 'stages', filter); expect(response.length).toBe(3); }); @@ -143,4 +381,210 @@ describe('boardQueries', () => { expect(response._id).toBe(stage._id); }); + + test('Stage detail itemsTotalCount by type', async () => { + const qry = ` + query stageDetail($_id: String!) { + stageDetail(_id: $_id) { + ${commonStageTypes} + } + } + `; + + const dealStage = await stageFactory({ type: BOARD_TYPES.DEAL }); + let response = await graphqlRequest(qry, 'stageDetail', { _id: dealStage._id }); + + expect(response._id).toBe(dealStage._id); + + const taskStage = await stageFactory({ type: BOARD_TYPES.TASK }); + response = await graphqlRequest(qry, 'stageDetail', { _id: taskStage._id }); + + expect(response._id).toBe(taskStage._id); + + const ticketStage = await stageFactory({ type: BOARD_TYPES.TICKET }); + response = await graphqlRequest(qry, 'stageDetail', { _id: ticketStage._id }); + + expect(response._id).toBe(ticketStage._id); + + const growthHackStage = await stageFactory({ type: BOARD_TYPES.GROWTH_HACK }); + response = await graphqlRequest(qry, 'stageDetail', { _id: growthHackStage._id }); + + expect(response._id).toBe(growthHackStage._id); + }); + + test('Stage detail (amount)', async () => { + const qry = ` + query stageDetail($_id: String!) { + stageDetail(_id: $_id) { + ${commonStageTypes} + } + } + `; + + const stage = await stageFactory({ type: BOARD_TYPES.DEAL }); + + const product = await productFactory(); + const productsData = [ + { + productId: product._id, + currency: 'USD', + amount: 200, + tickUsed: true, + }, + { + productId: product._id, + currency: 'USD', + }, + { + productId: product._id, + }, + ]; + + await dealFactory({ productsData, stageId: stage._id }); + + const response = await graphqlRequest(qry, 'stageDetail', { _id: stage._id }); + + expect(response._id).toBe(stage._id); + }); + + test('Pipeline state count', async () => { + // Not started pipelines + await pipelineFactory({ startDate: dateBuilder(1), endDate: dateBuilder(2) }); + await pipelineFactory({ startDate: dateBuilder(2), endDate: dateBuilder(3) }); + + // In progress pipelines + await pipelineFactory({ startDate: dateBuilder(-1), endDate: dateBuilder(1) }); + await pipelineFactory({ startDate: dateBuilder(-2), endDate: dateBuilder(2) }); + + // Not started pipelines + await pipelineFactory({ startDate: dateBuilder(-2), endDate: dateBuilder(-1) }); + + const response = await graphqlRequest(stateCountQry, 'pipelineStateCount', { type: BOARD_TYPES.DEAL }); + + expect(response.All).toBe(5); + expect(response['Not started']).toBe(2); + expect(response['In progress']).toBe(2); + expect(response.Completed).toBe(1); + }); + + test('Pipeline state count with boardId', async () => { + const board = await pipelineFactory({}); + + // Not started pipelines + await pipelineFactory({ startDate: dateBuilder(3), endDate: dateBuilder(5), boardId: board._id }); + + // In progress pipelines + await pipelineFactory({ startDate: dateBuilder(-3), endDate: dateBuilder(3), boardId: board._id }); + + // Not started pipelines + await pipelineFactory({ startDate: dateBuilder(-4), endDate: dateBuilder(-3), boardId: board._id }); + await pipelineFactory({ startDate: dateBuilder(-5), endDate: dateBuilder(-2) }); + + const response = await graphqlRequest(stateCountQry, 'pipelineStateCount', { boardId: board._id }); + + expect(response.All).toBe(3); + expect(response['Not started']).toBe(1); + expect(response['In progress']).toBe(1); + expect(response.Completed).toBe(1); + }); + + test('Convert to info', async () => { + const conversation = await conversationFactory(); + + const qry = ` + query convertToInfo($conversationId: String!) { + convertToInfo(conversationId: $conversationId) { + dealUrl + ticketUrl + taskUrl + } + } + `; + + const dealBoard = await boardFactory({ type: BOARD_TYPES.DEAL }); + const dealPipeline = await pipelineFactory({ type: BOARD_TYPES.DEAL, boardId: dealBoard._id }); + const dealStage = await stageFactory({ type: BOARD_TYPES.DEAL, pipelineId: dealPipeline._id }); + + const deal = await dealFactory({ sourceConversationId: conversation._id, stageId: dealStage._id }); + + const taskBoard = await boardFactory({ type: BOARD_TYPES.TASK }); + const taskPipeline = await pipelineFactory({ type: BOARD_TYPES.TASK, boardId: taskBoard._id }); + const taskStage = await stageFactory({ type: BOARD_TYPES.TASK, pipelineId: taskPipeline._id }); + const task = await taskFactory({ sourceConversationId: conversation._id, stageId: taskStage._id }); + + const ticketBoard = await boardFactory({ type: BOARD_TYPES.DEAL }); + const ticketPipeline = await pipelineFactory({ type: BOARD_TYPES.DEAL, boardId: ticketBoard._id }); + const ticketStage = await stageFactory({ type: BOARD_TYPES.DEAL, pipelineId: ticketPipeline._id }); + const ticket = await ticketFactory({ sourceConversationId: conversation._id, stageId: ticketStage._id }); + + let response = await graphqlRequest(qry, 'convertToInfo', { conversationId: conversation._id }); + + expect(response.dealUrl).toBe(`/deal/board?_id=${dealBoard._id}&pipelineId=${dealPipeline._id}&itemId=${deal._id}`); + expect(response.taskUrl).toBe(`/task/board?_id=${taskBoard._id}&pipelineId=${taskPipeline._id}&itemId=${task._id}`); + expect(response.ticketUrl).toBe( + `/inbox/ticket/board?_id=${ticketBoard._id}&pipelineId=${ticketPipeline._id}&itemId=${ticket._id}`, + ); + + response = await graphqlRequest(qry, 'convertToInfo', { conversationId: 'fakeId' }); + + expect(response.dealUrl).toBe(''); + expect(response.ticketUrl).toBe(''); + expect(response.taskUrl).toBe(''); + }); + + test('Archived stages', async () => { + const pipeline = await pipelineFactory(); + + const params = { + pipelineId: pipeline._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + const stage1 = await stageFactory(params); + await stageFactory(params); + await stageFactory(params); + + const qry = ` + query archivedStages($pipelineId: String!, $search: String, $page: Int, $perPage: Int) { + archivedStages(pipelineId: $pipelineId, search: $search, page: $page, perPage: $perPage) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'archivedStages', { pipelineId: pipeline._id }); + + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'archivedStages', { pipelineId: pipeline._id, search: stage1.name }); + + expect(response.length).toBe(1); + }); + + test('Archived stages count ', async () => { + const pipeline = await pipelineFactory(); + + const params = { + pipelineId: pipeline._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + const stage1 = await stageFactory(params); + await stageFactory(params); + await stageFactory(params); + + const qry = ` + query archivedStagesCount($pipelineId: String!, $search: String) { + archivedStagesCount(pipelineId: $pipelineId, search: $search) + } + `; + + let response = await graphqlRequest(qry, 'archivedStagesCount', { pipelineId: pipeline._id }); + + expect(response).toBe(3); + + response = await graphqlRequest(qry, 'archivedStagesCount', { pipelineId: pipeline._id, search: stage1.name }); + + expect(response).toBe(1); + }); }); diff --git a/src/__tests__/brandDb.test.ts b/src/__tests__/brandDb.test.ts index 95eec4e4a..bdabf0584 100644 --- a/src/__tests__/brandDb.test.ts +++ b/src/__tests__/brandDb.test.ts @@ -30,6 +30,18 @@ describe('Brands db', () => { expect(code).toBeDefined(); }); + test('Get brand', async () => { + try { + await Brands.getBrand('fakeId'); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + + const brandObj = await Brands.getBrand(_brand._id); + + expect(brandObj).toBeDefined(); + }); + test('Create brand', async () => { const brandObj = await Brands.createBrand({ name: _brand.name, diff --git a/src/__tests__/brandMutations.test.ts b/src/__tests__/brandMutations.test.ts index 90fd7b41a..5444f868e 100644 --- a/src/__tests__/brandMutations.test.ts +++ b/src/__tests__/brandMutations.test.ts @@ -1,14 +1,12 @@ import { graphqlRequest } from '../db/connection'; -import { brandFactory, integrationFactory, userFactory } from '../db/factories'; +import { brandFactory, integrationFactory } from '../db/factories'; import { Brands, Integrations, Users } from '../db/models'; import './setup.ts'; describe('Brands mutations', () => { let _brand; - let _user; let _integration; - let context; const commonParamDefs = ` $name: String! @@ -23,10 +21,7 @@ describe('Brands mutations', () => { beforeEach(async () => { // Creating test data _brand = await brandFactory({}); - _user = await userFactory({}); _integration = await integrationFactory({}); - - context = { user: _user }; }); afterEach(async () => { @@ -52,7 +47,7 @@ describe('Brands mutations', () => { } `; - const brand = await graphqlRequest(mutation, 'brandsAdd', args, context); + const brand = await graphqlRequest(mutation, 'brandsAdd', args); expect(brand.name).toEqual(args.name); expect(brand.description).toEqual(args.description); @@ -75,7 +70,7 @@ describe('Brands mutations', () => { } `; - const brand = await graphqlRequest(mutation, 'brandsEdit', args, context); + const brand = await graphqlRequest(mutation, 'brandsEdit', args); expect(brand.name).toBe(args.name); expect(brand.description).toBe(args.description); @@ -88,7 +83,7 @@ describe('Brands mutations', () => { } `; - await graphqlRequest(mutation, 'brandsRemove', { _id: _brand._id }, context); + await graphqlRequest(mutation, 'brandsRemove', { _id: _brand._id }); expect(await Brands.findOne({ _id: _brand._id })).toBe(null); }); @@ -108,7 +103,7 @@ describe('Brands mutations', () => { } `; - const brand = await graphqlRequest(mutation, 'brandsConfigEmail', args, context); + const brand = await graphqlRequest(mutation, 'brandsConfigEmail', args); expect(brand._id).toBe(args._id); expect(brand.emailConfig.toJSON()).toEqual(args.emailConfig.toJSON()); @@ -129,7 +124,7 @@ describe('Brands mutations', () => { } `; - const [integration] = await graphqlRequest(mutation, 'brandsManageIntegrations', args, context); + const [integration] = await graphqlRequest(mutation, 'brandsManageIntegrations', args); expect(integration.brandId).toBe(args._id); }); diff --git a/src/__tests__/brandQueries.test.ts b/src/__tests__/brandQueries.test.ts index 11b4222d2..818967112 100644 --- a/src/__tests__/brandQueries.test.ts +++ b/src/__tests__/brandQueries.test.ts @@ -1,93 +1,104 @@ -import { graphqlRequest } from '../db/connection'; -import { brandFactory } from '../db/factories'; -import { Brands } from '../db/models'; - -import './setup.ts'; - -describe('brandQueries', () => { - afterEach(async () => { - // Clearing test data - await Brands.deleteMany({}); - }); - - test('Brands', async () => { - const args = { - page: 1, - perPage: 2, - }; - - await brandFactory({}); - await brandFactory({}); - await brandFactory({}); - - const qry = ` - query brands($page: Int $perPage: Int) { - brands(page: $page perPage: $perPage) { - _id - name - description - code - userId - createdAt - emailConfig - integrations { _id } - } - } - `; - - const response = await graphqlRequest(qry, 'brands', args); - - expect(response.length).toBe(2); - }); - - test('Brand detail', async () => { - const qry = ` - query brandDetail($_id: String!) { - brandDetail(_id: $_id) { - _id - } - } - `; - - const brand = await brandFactory({}); - - const response = await graphqlRequest(qry, 'brandDetail', { _id: brand._id }); - - expect(response._id).toBe(brand._id); - }); - - test('Get brand total count', async () => { - const qry = ` - query brandsTotalCount { - brandsTotalCount - } - `; - - await brandFactory({}); - await brandFactory({}); - await brandFactory({}); - - const brandsCount = await graphqlRequest(qry, 'brandsTotalCount'); - - expect(brandsCount).toBe(3); - }); - - test('Get last brand', async () => { - const qry = ` - query brandsGetLast { - brandsGetLast { - _id - } - } - `; - - await brandFactory({}); - await brandFactory({}); - - const brand = await brandFactory({}); - - const lastBrand = await graphqlRequest(qry, 'brandsGetLast'); - - expect(lastBrand._id).toBe(brand._id); - }); -}); +import { graphqlRequest } from '../db/connection'; +import { brandFactory } from '../db/factories'; +import { Brands } from '../db/models'; + +import './setup.ts'; + +describe('brandQueries', () => { + afterEach(async () => { + // Clearing test data + await Brands.deleteMany({}); + }); + + test('Brands', async () => { + await brandFactory({ name: 'name 1' }); + await brandFactory({ name: 'name 2' }); + await brandFactory({ name: 'name 3' }); + + const qry = ` + query brands($searchValue: String) { + brands(searchValue: $searchValue) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'brands'); + + expect(response.length).toBe(3); + + await brandFactory({ name: 'search 1' }); + await brandFactory({ name: 'search 2' }); + + const args = { + searchValue: 'search', + }; + + response = await graphqlRequest(qry, 'brands', args); + + expect(response.length).toBe(2); + }); + + test('Brand detail', async () => { + const qry = ` + query brandDetail($_id: String!) { + brandDetail(_id: $_id) { + _id + integrations { + _id + } + } + } + `; + + const brand = await brandFactory({}); + + const response = await graphqlRequest(qry, 'brandDetail', { _id: brand._id }); + + expect(response._id).toBe(brand._id); + }); + + test('Get brand total count', async () => { + const qry = ` + query brandsTotalCount { + brandsTotalCount + } + `; + + await brandFactory({}); + await brandFactory({}); + await brandFactory({}); + + const brandsCount = await graphqlRequest(qry, 'brandsTotalCount'); + + expect(brandsCount).toBe(3); + }); + + test('Get last brand', async () => { + const qry = ` + query brandsGetLast { + brandsGetLast { + _id + } + } + `; + + const brand = await brandFactory({}); + + const lastBrand = await graphqlRequest(qry, 'brandsGetLast'); + + expect(lastBrand._id).toBe(brand._id); + }); + + test('Default email template', async () => { + const qry = ` + query brandsGetDefaultEmailConfig { + brandsGetDefaultEmailConfig + } + `; + + const template = await graphqlRequest(qry, 'brandsGetDefaultEmailConfig'); + + expect(template).toBeDefined(); + }); +}); diff --git a/src/__tests__/channelDb.test.ts b/src/__tests__/channelDb.test.ts index 3cb3721b2..d9ca770dd 100644 --- a/src/__tests__/channelDb.test.ts +++ b/src/__tests__/channelDb.test.ts @@ -8,6 +8,7 @@ describe('test channel creation error', () => { try { Channels.createChannel({ name: 'Channel test', + integrationIds: [], }); } catch (e) { expect(e.message).toBe('userId must be supplied'); @@ -30,6 +31,20 @@ describe('channel creation', () => { await Integrations.deleteMany({}); }); + test('Get channel', async () => { + const channel = await channelFactory(); + + try { + await Channels.getChannel('fakeId'); + } catch (e) { + expect(e.message).toBe('Channel not found'); + } + + const response = await Channels.getChannel(channel._id); + + expect(response).toBeDefined(); + }); + test('check if channel is getting created successfully', async () => { const user = await userFactory({}); @@ -128,6 +143,7 @@ describe('channel update', () => { // testing whether the updated field is not overwriting whole document ======== channel = await Channels.updateChannel(channel._id, { name: 'Channel test 2', + integrationIds: [], }); expect(channel.description).toBe('Channel test description'); @@ -142,6 +158,7 @@ describe('channel remove', () => { _channel = await Channels.createChannel( { name: 'Channel test', + integrationIds: [], }, user._id, ); diff --git a/src/__tests__/channelMutations.test.ts b/src/__tests__/channelMutations.test.ts index a5e357b57..a58accdfb 100644 --- a/src/__tests__/channelMutations.test.ts +++ b/src/__tests__/channelMutations.test.ts @@ -12,8 +12,8 @@ describe('mutations', () => { beforeEach(async () => { // Creating test data - _channel = await channelFactory({}); - _integration = await integrationFactory({}); + _integration = await integrationFactory(); + _channel = await channelFactory({ integrationIds: [_integration._id] }); _user = await userFactory({}); context = { user: _user }; @@ -101,6 +101,12 @@ describe('mutations', () => { expect(channel.description).toBe(args.description); expect(channel.memberIds).toEqual([member._id]); expect(channel.integrationIds).toEqual(args.integrationIds); + + // if channel integrationIds changed + args.integrationIds = ['updatedId']; + const channelNoIntegration = await graphqlRequest(mutation, 'channelsEdit', args, context); + + expect(channelNoIntegration.integrationIds).toEqual(args.integrationIds); }); test('Remove channel', async () => { diff --git a/src/__tests__/channelQueries.test.ts b/src/__tests__/channelQueries.test.ts index a9328e4f6..071a924fa 100644 --- a/src/__tests__/channelQueries.test.ts +++ b/src/__tests__/channelQueries.test.ts @@ -22,32 +22,22 @@ describe('channelQueries', () => { await channelFactory(); const qry = ` - query channels($page: Int $perPage: Int $memberIds: [String]) { - channels(page: $page perPage: $perPage memberIds: $memberIds) { + query channels($memberIds: [String]) { + channels(memberIds: $memberIds) { _id - name - description - integrationIds - memberIds - createdAt - userId - conversationCount - openConversationCount - integrations { _id } } } `; // channels response ================== - const args = { page: 1, perPage: 3 }; - let responses = await graphqlRequest(qry, 'channels', args); + let responses = await graphqlRequest(qry, 'channels'); - expect(responses.length).toBe(3); + expect(responses.length).toBe(6); // channels response by memberIds ===== - responses = await graphqlRequest(qry, 'channels', { - memberIds: [user._id], - }); + const memberIds = [user._id]; + + responses = await graphqlRequest(qry, 'channels', { memberIds }); expect(responses.length).toBe(2); }); @@ -60,6 +50,16 @@ describe('channelQueries', () => { query channelDetail($_id: String!) { channelDetail(_id: $_id) { _id + name + description + integrationIds + memberIds + createdAt + userId + conversationCount + openConversationCount + integrations { _id } + members { _id } } } `; @@ -90,9 +90,6 @@ describe('channelQueries', () => { test('Get last channel', async () => { // Create test data - await channelFactory(); - await channelFactory(); - const channel = await channelFactory(); const qry = ` diff --git a/src/__tests__/checklistDb.test.ts b/src/__tests__/checklistDb.test.ts new file mode 100644 index 000000000..010a3a029 --- /dev/null +++ b/src/__tests__/checklistDb.test.ts @@ -0,0 +1,165 @@ +import * as faker from 'faker'; +import { checklistFactory, checklistItemFactory, dealFactory, userFactory } from '../db/factories'; +import { ChecklistItems, Checklists, Users } from '../db/models'; +import { ACTIVITY_CONTENT_TYPES } from '../db/models/definitions/constants'; + +import './setup.ts'; + +/* + * Generate test data + */ +const generateData = () => ({ + contentType: 'deal', + contentTypeId: 'DFDFAFSFSDDSF', + title: faker.random.word(), +}); + +const generateItemData = checklistId => ({ + checklistId, + content: faker.random.word(), + isChecked: false, +}); + +/* + * Check values + */ +const checkValues = (checklistObj, doc) => { + expect(checklistObj.contentType).toBe(doc.contentType); + expect(checklistObj.contentTypeId).toBe(doc.contentTypeId); + expect(checklistObj.title).toBe(doc.title); +}; + +describe('Checklists model test', () => { + let _user; + let _checklist; + let _checklistItem; + + beforeEach(async () => { + // Creating test data + _user = await userFactory({}); + _checklist = await checklistFactory({}); + _checklistItem = await checklistItemFactory({ checklistId: _checklist._id }); + }); + + afterEach(async () => { + // Clearing test data + await ChecklistItems.deleteMany({}); + await Checklists.deleteMany({}); + await Users.deleteMany({}); + }); + + test('Get checklist', async () => { + try { + await Checklists.getChecklist('fakeId'); + } catch (e) { + expect(e.message).toBe('Checklist not found'); + } + + const response = await Checklists.getChecklist(_checklist._id); + + expect(response).toBeDefined(); + }); + + test('Get checklist item', async () => { + try { + await ChecklistItems.getChecklistItem('fakeId'); + } catch (e) { + expect(e.message).toBe('Checklist item not found'); + } + + const response = await ChecklistItems.getChecklistItem(_checklistItem._id); + + expect(response).toBeDefined(); + }); + + test('Create Checklist, item', async () => { + // valid + const doc = generateData(); + + const checklistObj = await Checklists.createChecklist(doc, _user); + + checkValues(checklistObj, doc); + expect(checklistObj.createdUserId).toBe(_user._id); + + const docItem = generateItemData(checklistObj._id); + + const checklistItemObj = await ChecklistItems.createChecklistItem(docItem, _user); + + checkValues(checklistItemObj, docItem); + expect(checklistItemObj.createdUserId).toBe(_user._id); + }); + + test('Edit checklist, item valid', async () => { + const doc = generateData(); + + const checklistObj = await Checklists.updateChecklist(_checklist._id, doc); + + const docItem = generateItemData(checklistObj._id); + const checklistItemObj = await ChecklistItems.updateChecklistItem(_checklistItem._id, docItem); + + checkValues(checklistObj, doc); + checkValues(checklistItemObj, docItem); + }); + + test('Remove checklist and item valid', async () => { + try { + await Checklists.removeChecklist('DFFFDSFD'); + } catch (e) { + expect(e.message).toBe('Checklist not found with id DFFFDSFD'); + } + + try { + await ChecklistItems.removeChecklistItem('DFFFDSFD'); + } catch (e) { + expect(e.message).toBe("Checklist's item not found with id DFFFDSFD"); + } + + let count = await Checklists.find({ _id: _checklist._id }).countDocuments(); + + await checklistItemFactory({ checklistId: _checklist._id }); + await checklistItemFactory({ checklistId: _checklist._id }); + + let itemCount = await ChecklistItems.find({ checklistId: _checklist._id }).countDocuments(); + expect(count).toBe(1); + expect(itemCount).toBe(3); + + await ChecklistItems.removeChecklistItem(_checklistItem._id); + + itemCount = await ChecklistItems.find({ checklistId: _checklist._id }).countDocuments(); + expect(itemCount).toBe(2); + + await Checklists.removeChecklist(_checklist._id); + + count = await Checklists.find({ _id: _checklist._id }).countDocuments(); + itemCount = await ChecklistItems.find({ checklistId: _checklist._id }).countDocuments(); + expect(count).toBe(0); + expect(itemCount).toBe(0); + }); + + test('remove Deal and to remove Checklists', async () => { + const deal = await dealFactory({}); + + const checklist = await checklistFactory({ + contentType: ACTIVITY_CONTENT_TYPES.DEAL, + contentTypeId: deal._id, + }); + + await checklistItemFactory({ checklistId: checklist._id }); + + await Checklists.removeChecklists(ACTIVITY_CONTENT_TYPES.DEAL, deal._id); + + const checklists = await Checklists.find({ + contentType: ACTIVITY_CONTENT_TYPES.DEAL, + contentTypeId: deal._id, + }); + + const checklistItems = await ChecklistItems.find({ checklistId: checklist._id }); + + expect(checklists).toHaveLength(0); + expect(checklistItems).toHaveLength(0); + + const response = await Checklists.removeChecklists(ACTIVITY_CONTENT_TYPES.COMPANY, deal._id); + + expect(response).toBeUndefined(); + }); +}); diff --git a/src/__tests__/checklistMutations.test.ts b/src/__tests__/checklistMutations.test.ts new file mode 100644 index 000000000..134e1479e --- /dev/null +++ b/src/__tests__/checklistMutations.test.ts @@ -0,0 +1,228 @@ +import { graphqlRequest } from '../db/connection'; +import { + checklistFactory, + checklistItemFactory, + dealFactory, + pipelineFactory, + stageFactory, + userFactory, +} from '../db/factories'; +import { ChecklistItems, Checklists, Users } from '../db/models'; + +import './setup.ts'; + +describe('Checklists mutations', () => { + let _user; + let _checklist; + let _checklistItem; + let context; + + beforeEach(async () => { + // Creating test data + _user = await userFactory({}); + _checklist = await checklistFactory({ createdUserId: _user._d }); + _checklistItem = await checklistItemFactory({ checklistId: _checklist._id, order: 0 }); + + await checklistItemFactory({ checklistId: _checklist._id, order: 1 }); + + context = { user: _user }; + }); + + afterEach(async () => { + // Clearing test data + await ChecklistItems.deleteMany({}); + await Checklists.deleteMany({}); + await Users.deleteMany({}); + }); + + test('Add checklist', async () => { + const user = await userFactory({}); + const pipeline = await pipelineFactory({}); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const deal = await dealFactory({ stageId: stage._id, assignedUserIds: [user._id] }); + + if (!user || !user.details) { + throw new Error('User not found'); + } + + const args = { + contentType: 'deal', + contentTypeId: deal._id, + title: `Checklist title`, + }; + + const mutation = ` + mutation checklistsAdd( + $contentType: String + $contentTypeId: String + $title: String + ) { + checklistsAdd( + contentType: $contentType + contentTypeId: $contentTypeId + title: $title + ) { + contentType + contentTypeId + title + } + } + `; + + const checklist = await graphqlRequest(mutation, 'checklistsAdd', args, context); + + expect(checklist.contentType).toBe(args.contentType); + expect(checklist.contentTypeId).toBe(args.contentTypeId); + expect(checklist.title).toBe(args.title); + }); + + test('Add checklist item', async () => { + const user = await userFactory({}); + const pipeline = await pipelineFactory({}); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const deal = await dealFactory({ stageId: stage._id }); + const checklist = await checklistFactory({ contentType: 'deal', contentTypeId: deal._id }); + + if (!user || !user.details) { + throw new Error('User not found'); + } + + const args = { + checklistId: checklist._id, + isChecked: false, + content: `@${user.details.fullName}`, + }; + + const mutation = ` + mutation checklistItemsAdd( + $checklistId: String, + $isChecked: Boolean, + $content: String, + ) { + checklistItemsAdd( + checklistId: $checklistId, + isChecked: $isChecked, + content: $content, + ) { + _id + isChecked + content + } + } + `; + + const checklistItem = await graphqlRequest(mutation, 'checklistItemsAdd', args, context); + + expect(checklistItem.content).toBe(args.content); + expect(checklistItem.isChecked).toBe(args.isChecked); + }); + + test('Edit checklist', async () => { + const { _id, title } = _checklist; + const args = { _id, title }; + + const mutation = ` + mutation checklistsEdit( + $_id: String! + $title: String + ) { + checklistsEdit( + _id: $_id + title: $title + ) { + _id + title + } + } + `; + + const checklist = await graphqlRequest(mutation, 'checklistsEdit', args, context); + + expect(checklist._id).toBe(args._id); + expect(checklist.title).toBe(args.title); + }); + + test('Edit checklist item', async () => { + const { _id, content, isChecked } = _checklistItem; + const args = { _id, content, isChecked }; + + const mutation = ` + mutation checklistItemsEdit( + $_id: String! + $content: String + $isChecked: Boolean + ) { + checklistItemsEdit( + _id: $_id + content: $content + isChecked: $isChecked + ) { + _id + content + isChecked + } + } + `; + + const checklist = await graphqlRequest(mutation, 'checklistItemsEdit', args, context); + + expect(checklist._id).toBe(args._id); + expect(checklist.content).toBe(args.content); + expect(checklist.isChecked).toBe(args.isChecked); + }); + + test('Remove checklist', async () => { + const taskChecklist = await checklistFactory({ contentType: 'task' }); + const ticketChecklist = await checklistFactory({ contentType: 'ticket' }); + + await checklistItemFactory({ checklistId: _checklist._id }); + + const mutation = ` + mutation checklistsRemove($_id: String!) { + checklistsRemove(_id: $_id) { + _id + } + } + `; + + await graphqlRequest(mutation, 'checklistsRemove', { _id: _checklist._id }, context); + await graphqlRequest(mutation, 'checklistsRemove', { _id: taskChecklist._id }, context); + await graphqlRequest(mutation, 'checklistsRemove', { _id: ticketChecklist._id }, context); + + expect(await Checklists.findOne({ _id: _checklist._id })).toBe(null); + expect(await Checklists.findOne({ _id: taskChecklist._id })).toBe(null); + expect(await Checklists.findOne({ _id: ticketChecklist._id })).toBe(null); + expect(await ChecklistItems.find({ checklistId: _checklist._id })).toEqual([]); + }); + + test('Remove checklist item', async () => { + const mutation = ` + mutation checklistItemsRemove($_id: String!) { + checklistItemsRemove(_id: $_id) { + _id + } + } + `; + + await graphqlRequest(mutation, 'checklistItemsRemove', { _id: _checklistItem._id }, context); + + expect(await ChecklistItems.findOne({ _id: _checklistItem._id })).toBe(null); + }); + + test('Order checklist items', async () => { + const mutation = ` + mutation checklistItemsOrder($_id: String!, $destinationIndex: Int) { + checklistItemsOrder(_id: $_id, destinationIndex: $destinationIndex) { + _id + order + } + } + `; + + await graphqlRequest(mutation, 'checklistItemsOrder', { _id: _checklistItem._id, destinationIndex: 1 }, context); + + const item = await ChecklistItems.findOne({ _id: _checklistItem._id }).lean(); + + expect(item.order).toBe(1); + }); +}); diff --git a/src/__tests__/checklistQueries.test.ts b/src/__tests__/checklistQueries.test.ts new file mode 100644 index 000000000..4dd315ec1 --- /dev/null +++ b/src/__tests__/checklistQueries.test.ts @@ -0,0 +1,81 @@ +import * as faker from 'faker'; +import { graphqlRequest } from '../db/connection'; +import { checklistFactory, checklistItemFactory } from '../db/factories'; +import { ChecklistItems, Checklists } from '../db/models'; + +import './setup.ts'; + +describe('checklistQueries', () => { + afterEach(async () => { + // Clearing test data + await Checklists.deleteMany({}); + await ChecklistItems.deleteMany({}); + }); + + test('checklists', async () => { + // Creating test data + const contentTypeId = (faker && faker.random ? faker.random.number() : 999).toString(); + + const checklist1 = await checklistFactory({ contentType: 'deal', contentTypeId }); + await checklistFactory({ contentType: 'task', contentTypeId }); + + await checklistItemFactory({ checklistId: checklist1._id, isChecked: true }); + await checklistItemFactory({ checklistId: checklist1._id, isChecked: true }); + await checklistItemFactory({ checklistId: checklist1._id }); + await checklistItemFactory({ checklistId: checklist1._id }); + + const qry = ` + query checklists($contentType: String! $contentTypeId: String) { + checklists(contentType: $contentType contentTypeId: $contentTypeId) { + _id + contentType + contentTypeId + title + createdUserId + createdDate + items { + _id + isChecked + content + } + percent + } + } + `; + + // deal =========================== + let responses = await graphqlRequest(qry, 'checklists', { + contentType: 'deal', + contentTypeId, + }); + + expect(responses.length).toBe(1); + expect(responses[0].items.length).toBe(4); + expect(responses[0].percent).toBe(50); + + // task ============================ + responses = await graphqlRequest(qry, 'checklists', { + contentType: 'task', + contentTypeId, + }); + + expect(responses.length).toBe(1); + expect(responses[0].percent).toBe(0); + }); + + test('Checklist detail', async () => { + const qry = ` + query checklistDetail($_id: String!) { + checklistDetail(_id: $_id) { + _id + } + } + `; + + const checklist = await checklistFactory({}); + + const response = await graphqlRequest(qry, 'checklistDetail', { _id: checklist._id }); + + expect(response._id).toBe(checklist._id); + }); +}); diff --git a/src/__tests__/companyDb.test.ts b/src/__tests__/companyDb.test.ts index 817cfa809..70590ca31 100644 --- a/src/__tests__/companyDb.test.ts +++ b/src/__tests__/companyDb.test.ts @@ -1,7 +1,15 @@ -import { companyFactory, customerFactory, dealFactory, fieldFactory, internalNoteFactory } from '../db/factories'; -import { Companies, Customers, Deals, InternalNotes } from '../db/models'; +import { + companyFactory, + conformityFactory, + customerFactory, + dealFactory, + fieldFactory, + internalNoteFactory, + userFactory, +} from '../db/factories'; +import { Companies, Conformities, Customers, Deals, InternalNotes } from '../db/models'; import { ICompany, ICompanyDocument } from '../db/models/definitions/companies'; -import { ACTIVITY_CONTENT_TYPES, STATUSES } from '../db/models/definitions/constants'; +import { ACTIVITY_CONTENT_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; @@ -40,6 +48,7 @@ describe('Companies model tests', () => { _company = await companyFactory({ primaryName: 'companyname', names: ['companyname', 'companyname1'], + code: 'code', }); }); @@ -48,8 +57,20 @@ describe('Companies model tests', () => { await Companies.deleteMany({}); }); + test('Get company', async () => { + try { + await Companies.getCompany('fakeId'); + } catch (e) { + expect(e.message).toBe('Company not found'); + } + + const response = await Companies.getCompany(_company._id); + + expect(response).toBeDefined(); + }); + test('Create company', async () => { - expect.assertions(15); + expect.assertions(5); // check duplication ============== try { @@ -64,11 +85,23 @@ describe('Companies model tests', () => { expect(e.message).toBe('Duplicated name'); } + try { + await Companies.createCompany({ code: 'code' }); + } catch (e) { + expect(e.message).toBe('Duplicated code'); + } + + let companyObj = await Companies.createCompany({}, await userFactory()); + + expect(companyObj).toBeDefined(); + const doc = generateDoc(); + // primary name is empty + doc.primaryName = ''; - const companyObj = await Companies.createCompany(doc); + companyObj = await Companies.createCompany(doc); - check(companyObj, doc); + expect(companyObj.primaryName).toBe(''); }); test('Create company: with company fields validation error', async () => { @@ -83,7 +116,7 @@ describe('Companies model tests', () => { try { await Companies.createCompany({ primaryName: 'name', - customFieldsData: { [field._id]: 'invalid number' }, + customFieldsData: [{ field: field._id, value: 'invalid number' }], }); } catch (e) { expect(e.message).toBe(`${field.text}: Invalid number`); @@ -124,61 +157,65 @@ describe('Companies model tests', () => { try { await Companies.updateCompany(_company._id, { primaryName: 'name', - customFieldsData: { [field._id]: 'invalid number' }, + customFieldsData: [{ field: field._id, value: 'invalid number' }], }); } catch (e) { expect(e.message).toBe(`${field.text}: Invalid number`); } }); - test('Update company customers', async () => { - const customerIds = ['12313qwrqwe', '123', '11234']; - - await Companies.updateCustomers(_company._id, customerIds); - - for (const customerId of customerIds) { - const customerObj = await Customers.findOne({ _id: customerId }); - - if (!customerObj) { - throw new Error('Customer not found'); - } - - expect(customerObj.companyIds).toContain(_company._id); - } - }); - test('removeCompany', async () => { const company = await companyFactory({}); - await customerFactory({ companyIds: [company._id] }); + const customer = await customerFactory({}); + + await conformityFactory({ + mainType: 'company', + mainTypeId: company._id, + relType: 'customer', + relTypeId: customer._id, + }); + + await internalNoteFactory({ + contentType: ACTIVITY_CONTENT_TYPES.COMPANY, + contentTypeId: company._id, + }); await internalNoteFactory({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, contentTypeId: company._id, }); - await Companies.removeCompany(company._id); + await Companies.removeCompanies([company._id]); - const internalNote = await InternalNotes.find({ + const internalNotes = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, contentTypeId: company._id, }); const customers = await Customers.find({ - companyIds: { $in: [company._id] }, + _id: { $in: [customer._id] }, }); - expect(customers).toHaveLength(0); - expect(internalNote).toHaveLength(0); + const conformities = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: company._id, + relTypes: ['customer'], + }); + + expect(customers).toHaveLength(1); + expect(internalNotes).toHaveLength(0); + expect(conformities).toHaveLength(0); }); test('mergeCompanies', async () => { - expect.assertions(21); + expect.assertions(19); const company1 = await companyFactory({ tagIds: ['123', '456', '1234'], names: ['company1'], phones: ['phone1'], emails: ['email1'], + scopeBrandIds: ['123'], }); const company2 = await companyFactory({ @@ -188,15 +225,25 @@ describe('Companies model tests', () => { emails: ['email2'], }); - const customer1 = await customerFactory({ - companyIds: [company1._id], + const company3 = await companyFactory(); + + const customer1 = await customerFactory({}); + await conformityFactory({ + mainType: 'customer', + mainTypeId: customer1._id, + relType: 'company', + relTypeId: company1._id, }); - const customer2 = await customerFactory({ - companyIds: [company2._id], + const customer2 = await customerFactory({}); + await conformityFactory({ + mainType: 'customer', + mainTypeId: customer2._id, + relType: 'company', + relTypeId: company2._id, }); - const companyIds = [company1._id, company2._id]; + const companyIds = [company1._id, company2._id, company3._id]; const mergedTagIds = ['123', '456', '1234', '1231', 'asd12']; // test duplication ================= @@ -214,8 +261,14 @@ describe('Companies model tests', () => { contentTypeId: companyIds[0], }); - await dealFactory({ - companyIds, + const deal1 = await dealFactory({}); + companyIds.map(async companyId => { + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal1._id, + relType: 'company', + relTypeId: companyId, + }); }); const doc = { @@ -242,25 +295,9 @@ describe('Companies model tests', () => { // Checking old company datas deleted const oldCompany = (await Companies.findOne({ _id: companyIds[0] })) || { status: '' }; - expect(oldCompany.status).toBe(STATUSES.DELETED); + expect(oldCompany.status).toBe('deleted'); expect(updatedCompany.tagIds).toEqual(expect.arrayContaining(mergedTagIds)); - const customerObj1 = await Customers.findOne({ _id: customer1._id }); - - if (!customerObj1) { - throw new Error('Customer not found'); - } - - expect(customerObj1.companyIds).not.toContain(company1._id); - - const customerObj2 = await Customers.findOne({ _id: customer2._id }); - - if (!customerObj2) { - throw new Error('Customer not found'); - } - - expect(customerObj2.companyIds).not.toContain(company2._id); - let internalNote = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, contentTypeId: companyIds[0], @@ -271,9 +308,6 @@ describe('Companies model tests', () => { // Checking new company datas updated expect(updatedCompany.tagIds).toEqual(expect.arrayContaining(mergedTagIds)); - expect(customerObj1.companyIds).toContain(updatedCompany._id); - expect(customerObj2.companyIds).toContain(updatedCompany._id); - internalNote = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, contentTypeId: updatedCompany._id, @@ -281,18 +315,42 @@ describe('Companies model tests', () => { expect(internalNote).not.toHaveLength(0); - const deals = await Deals.find({ - companyIds: { $in: companyIds }, + const cusRelTypeIds = await Conformities.filterConformity({ + mainType: 'company', + mainTypeIds: companyIds, + relType: 'customer', }); + expect(cusRelTypeIds.length).toBe(0); - expect(deals.length).toBe(0); + const newCusRelTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: updatedCompany._id, + relTypes: ['customer'], + }); - const deal = await Deals.findOne({ - companyIds: { $in: [updatedCompany._id] }, + const customers = await Customers.find({ + _id: { $in: newCusRelTypeIds }, }); - if (!deal) { - throw new Error('Deal not found'); - } - expect(deal.companyIds).toContain(updatedCompany._id); + + expect(customers).toHaveLength(2); + + const relTypeIds = await Conformities.filterConformity({ + mainType: 'company', + mainTypeIds: companyIds, + relType: 'deal', + }); + expect(relTypeIds.length).toBe(0); + + const newRelTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: updatedCompany._id, + relTypes: ['deal'], + }); + + const deals = await Deals.find({ + _id: { $in: newRelTypeIds }, + }); + + expect(deals).toHaveLength(1); }); }); diff --git a/src/__tests__/companyMutations.test.ts b/src/__tests__/companyMutations.test.ts index 5fb8a9232..16132f864 100644 --- a/src/__tests__/companyMutations.test.ts +++ b/src/__tests__/companyMutations.test.ts @@ -1,13 +1,12 @@ import * as faker from 'faker'; import { graphqlRequest } from '../db/connection'; -import { companyFactory, customerFactory, userFactory } from '../db/factories'; +import { companyFactory, tagsFactory, userFactory } from '../db/factories'; import { Companies, Customers, Users } from '../db/models'; import './setup.ts'; describe('Companies mutations', () => { let _company; - let _customer; let _user; let context; @@ -22,6 +21,8 @@ describe('Companies mutations', () => { $industry: String $tagIds: [String] $customFieldsData: JSON + $parentCompanyId: String + $ownerId: String `; const commonParams = ` @@ -35,12 +36,13 @@ describe('Companies mutations', () => { industry: $industry tagIds: $tagIds customFieldsData: $customFieldsData + parentCompanyId: $parentCompanyId + ownerId: $ownerId `; beforeEach(async () => { // Creating test data _company = await companyFactory({}); - _customer = await customerFactory({}); _user = await userFactory({}); context = { user: _user }; @@ -54,6 +56,9 @@ describe('Companies mutations', () => { }); test('Add company', async () => { + const parent = await companyFactory(); + const company = await companyFactory(); + const args = { primaryName: faker.company.companyName(), names: [faker.company.companyName()], @@ -63,8 +68,8 @@ describe('Companies mutations', () => { emails: [faker.internet.email()], size: faker.random.number(), industry: 'Airlines', - tagIds: _company.tagIds, - customFieldsData: {}, + tagIds: company.tagIds, + parentCompanyId: parent._id, }; const mutation = ` @@ -80,27 +85,39 @@ describe('Companies mutations', () => { industry tagIds customFieldsData + parentCompanyId } } `; - const company = await graphqlRequest(mutation, 'companiesAdd', args, context); - - expect(company.primaryName).toBe(args.primaryName); - expect(company.primaryPhone).toBe(args.primaryPhone); - expect(company.primaryEmail).toBe(args.primaryEmail); - expect(company.names).toEqual(expect.arrayContaining(args.names)); - expect(company.phones).toEqual(expect.arrayContaining(args.phones)); - expect(company.emails).toEqual(expect.arrayContaining(args.emails)); - expect(company.size).toBe(args.size); - expect(company.industry).toBe(args.industry); - expect(expect.arrayContaining(company.tagIds)).toEqual(args.tagIds); - expect(company.customFieldsData).toEqual(args.customFieldsData); + const result = await graphqlRequest(mutation, 'companiesAdd', args, context); + + expect(result.primaryName).toBe(args.primaryName); + expect(result.primaryPhone).toBe(args.primaryPhone); + expect(result.primaryEmail).toBe(args.primaryEmail); + expect(result.names).toEqual(expect.arrayContaining(args.names)); + expect(result.phones).toEqual(expect.arrayContaining(args.phones)); + expect(result.emails).toEqual(expect.arrayContaining(args.emails)); + expect(result.size).toBe(args.size); + expect(result.industry).toBe(args.industry); + expect(expect.arrayContaining(result.tagIds)).toEqual(args.tagIds); + expect(result.customFieldsData.length).toEqual(0); + expect(result.parentCompanyId).toBe(parent._id); }); test('Edit company', async () => { + const parent = await companyFactory(); + const tag1 = await tagsFactory(); + const tag2 = await tagsFactory(); + + const merged = await companyFactory(); + const company = await companyFactory({ + tagIds: [tag1._id], + mergedIds: [merged._id], + }); + const args = { - _id: _company._id, + _id: company._id, primaryName: faker.company.companyName(), names: [faker.company.companyName()], primaryPhone: faker.random.number().toString(), @@ -109,8 +126,9 @@ describe('Companies mutations', () => { emails: [faker.internet.email()], size: faker.random.number(), industry: faker.random.word(), - tagIds: _company.tagIds, - customFieldsData: {}, + ownerId: _user._id, + parentCompanyId: parent._id, + tagIds: [tag2._id], }; const mutation = ` @@ -127,54 +145,29 @@ describe('Companies mutations', () => { industry tagIds customFieldsData + ownerId + parentCompanyId + mergedIds } } `; - const company = await graphqlRequest(mutation, 'companiesEdit', args, context); - - expect(company._id).toBe(args._id); - expect(company.primaryName).toBe(args.primaryName); - expect(company.primaryPhone).toBe(args.primaryPhone); - expect(company.primaryEmail).toBe(args.primaryEmail); - expect(company.names).toEqual(expect.arrayContaining(args.names)); - expect(company.phones).toEqual(expect.arrayContaining(args.phones)); - expect(company.emails).toEqual(expect.arrayContaining(args.emails)); - expect(company.size).toBe(args.size); - expect(company.industry).toBe(args.industry); - expect(expect.arrayContaining(company.tagIds)).toEqual(args.tagIds); - expect(company.customFieldsData).toEqual(args.customFieldsData); - }); - - test('Edit customer of company', async () => { - const args = { - _id: _company._id, - customerIds: [_customer._id], - }; - - const mutation = ` - mutation companiesEditCustomers( - $_id: String! - $customerIds: [String] - ) { - companiesEditCustomers( - _id: $_id - customerIds: $customerIds - ) { - _id - } - } - `; - - await graphqlRequest(mutation, 'companiesEditCustomers', args, context); - - const customer = await Customers.findOne({ _id: _customer._id }); - - if (!customer) { - throw new Error('Customer not found'); - } - - expect(customer.companyIds).toContain(_company._id); + const result = await graphqlRequest(mutation, 'companiesEdit', args, context); + + expect(result._id).toBe(args._id); + expect(result.primaryName).toBe(args.primaryName); + expect(result.primaryPhone).toBe(args.primaryPhone); + expect(result.primaryEmail).toBe(args.primaryEmail); + expect(result.names).toEqual(expect.arrayContaining(args.names)); + expect(result.phones).toEqual(expect.arrayContaining(args.phones)); + expect(result.emails).toEqual(expect.arrayContaining(args.emails)); + expect(result.size).toBe(args.size); + expect(result.industry).toBe(args.industry); + expect(expect.arrayContaining(result.tagIds)).toEqual(args.tagIds); + expect(result.customFieldsData.length).toEqual(0); + expect(result.ownerId).toBe(_user._id); + expect(result.parentCompanyId).toBe(parent._id); + expect(result.mergedIds.length).toBe(1); }); test('Remove company', async () => { @@ -184,9 +177,17 @@ describe('Companies mutations', () => { } `; - await graphqlRequest(mutation, 'companiesRemove', { companyIds: [_company._id] }, context); + const tag = await tagsFactory(); + + const company = await companyFactory({ + ownerId: _user._id, + mergedIds: [_company._id], + tagIds: [tag._id], + }); + + await graphqlRequest(mutation, 'companiesRemove', { companyIds: [company._id] }, context); - expect(await Companies.find({ companyIds: [_company._id] })).toEqual([]); + expect(await Companies.find({ companyIds: [company._id] })).toEqual([]); }); test('Merge company', async () => { diff --git a/src/__tests__/companyQueries.test.ts b/src/__tests__/companyQueries.test.ts index c5755b12f..26dab4a88 100644 --- a/src/__tests__/companyQueries.test.ts +++ b/src/__tests__/companyQueries.test.ts @@ -1,20 +1,9 @@ import { graphqlRequest } from '../db/connection'; -import { - brandFactory, - companyFactory, - customerFactory, - integrationFactory, - segmentFactory, - tagsFactory, -} from '../db/factories'; +import { brandFactory, companyFactory, integrationFactory, segmentFactory, tagsFactory } from '../db/factories'; import { Companies, Segments, Tags } from '../db/models'; import './setup.ts'; -const count = response => { - return Object.keys(response).length; -}; - describe('companyQueries', () => { const commonParamDefs = ` $page: Int @@ -23,8 +12,6 @@ describe('companyQueries', () => { $tag: String $ids: [String] $searchValue: String - $lifecycleState: String - $leadStatus: String $brand: String `; @@ -35,8 +22,6 @@ describe('companyQueries', () => { tag: $tag ids: $ids searchValue: $searchValue - lifecycleState: $lifecycleState - leadStatus: $leadStatus brand: $brand `; @@ -44,44 +29,6 @@ describe('companyQueries', () => { query companies(${commonParamDefs}) { companies(${commonParams}) { _id - createdAt - modifiedAt - - primaryName - names - size - industry - plan - - parentCompanyId - primaryEmail - emails - ownerId - primaryPhone - phones - leadStatus - lifecycleState - businessType - description - doNotDisturb - links { - linkedIn - twitter - facebook - github - youtube - website - } - owner { _id } - parentCompany { _id } - - tagIds - - customFieldsData - - customers { _id } - deals { _id } - getTags { _id } } } `; @@ -108,9 +55,6 @@ describe('companyQueries', () => { } `; - const name = 'companyName'; - const plan = 'plan'; - afterEach(async () => { // Clearing test data await Companies.deleteMany({}); @@ -126,147 +70,7 @@ describe('companyQueries', () => { await companyFactory({}); const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCompanies, 'companies', args); - - expect(responses.length).toBe(3); - }); - - test('Companies filtered by ids', async () => { - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - const company3 = await companyFactory({}); - - await companyFactory({}); - await companyFactory({}); - await companyFactory({}); - - const ids = [company1._id, company2._id, company3._id]; - - const responses = await graphqlRequest(qryCompanies, 'companies', { ids }); - - expect(responses.length).toBe(3); - }); - - test('Companies filtered by tag', async () => { - const tag = await tagsFactory(); - - await companyFactory(); - await companyFactory(); - await companyFactory({ tagIds: [tag._id] }); - await companyFactory({ tagIds: [tag._id] }); - - const tagResponse = await Tags.findOne({}, '_id'); - - if (!tagResponse) { - throw new Error('Tag response does not exist'); - } - - const responses = await graphqlRequest(qryCompanies, 'companies', { - tag: tagResponse._id, - }); - - expect(responses.length).toBe(2); - }); - - test('Companies filtered by leadStatus', async () => { - await companyFactory(); - await companyFactory(); - await companyFactory({ leadStatus: 'new' }); - await companyFactory({ leadStatus: 'new' }); - - const responses = await graphqlRequest(qryCompanies, 'companies', { - leadStatus: 'new', - }); - - expect(responses.length).toBe(2); - }); - - test('Companies filtered by lifecycleState', async () => { - await companyFactory(); - await companyFactory(); - await companyFactory({ lifecycleState: 'subscriber' }); - await companyFactory({ lifecycleState: 'subscriber' }); - - const responses = await graphqlRequest(qryCompanies, 'companies', { - lifecycleState: 'subscriber', - }); - - expect(responses.length).toBe(2); - }); - - test('Companies filtered by segment', async () => { - await companyFactory({ names: [name], primaryName: name }); - await companyFactory(); - await companyFactory(); - - const args = { - contentType: 'company', - conditions: [ - { - field: 'primaryName', - operator: 'c', - value: name, - type: 'string', - }, - ], - }; - - const segment = await segmentFactory(args); - - const response = await graphqlRequest(qryCompanies, 'companies', { - segment: segment._id, - }); - - expect(response.length).toBe(1); - }); - - test('Companies filtered by search value', async () => { - await companyFactory({ names: [name], primaryName: name }); - await companyFactory({ plan }); - await companyFactory({ industry: 'Banks' }); - - // companies by name ============== - let responses = await graphqlRequest(qryCompanies, 'companies', { - searchValue: name, - }); - - expect(responses.length).toBe(1); - expect(responses[0].primaryName).toBe(name); - - // companies by industry ========== - responses = await graphqlRequest(qryCompanies, 'companies', { - searchValue: 'Banks', - }); - - expect(responses.length).toBe(1); - expect(responses[0].industry).toBe('Banks'); - - // companies by plan ============== - responses = await graphqlRequest(qryCompanies, 'companies', { - searchValue: plan, - }); - - expect(responses.length).toBe(1); - expect(responses[0].plan).toBe(plan); - }); - - test('Companies filtered by brandId', async () => { - const brand = await brandFactory({}); - const integration = await integrationFactory({ brandId: brand._id }); - const integrationId = integration._id; - - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - await companyFactory({}); - - await customerFactory({ integrationId, companyIds: [company1._id] }); - await customerFactory({ integrationId, companyIds: [company2._id] }); - - const responses = await graphqlRequest(qryCompanies, 'companies', { - brand: brand._id, - }); - - expect(responses.length).toBe(2); + await graphqlRequest(qryCompanies, 'companies', args); }); test('Main companies', async () => { @@ -276,99 +80,33 @@ describe('companyQueries', () => { await companyFactory({}); const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCompaniesMain, 'companiesMain', args); - - expect(responses.list.length).toBe(3); - expect(responses.totalCount).toBe(4); + await graphqlRequest(qryCompaniesMain, 'companiesMain', args); }); - test('Count companies', async () => { - // Creating test data - await companyFactory({}); - await companyFactory({}); - + test('Count companies by segment', async () => { await segmentFactory({ contentType: 'company' }); - const response = await graphqlRequest(qryCount, 'companyCounts', { + await graphqlRequest(qryCount, 'companyCounts', { only: 'bySegment', }); - - expect(count(response.bySegment)).toBe(1); }); test('Company count by tag', async () => { - await companyFactory({}); - await companyFactory({}); - await tagsFactory({ type: 'company' }); await tagsFactory({ type: 'customer' }); - const response = await graphqlRequest(qryCount, 'companyCounts', { + await graphqlRequest(qryCount, 'companyCounts', { only: 'byTag', }); - - expect(count(response.byTag)).toBe(1); - }); - - test('Company count by leadStatus', async () => { - await companyFactory({}); - await companyFactory({}); - await companyFactory({ leadStatus: 'new' }); - await companyFactory({ leadStatus: 'new' }); - - const response = await graphqlRequest(qryCount, 'companyCounts', { - only: 'byLeadStatus', - }); - - expect(response.byLeadStatus.open).toBe(2); - expect(response.byLeadStatus.new).toBe(2); - }); - - test('Company count by lifecycleState', async () => { - await companyFactory({}); - await companyFactory({}); - await companyFactory({ lifecycleState: 'subscriber' }); - await companyFactory({ lifecycleState: 'subscriber' }); - - const response = await graphqlRequest(qryCount, 'companyCounts', { - only: 'byLifecycleState', - }); - - expect(response.byLifecycleState.subscriber).toBe(2); - expect(response.byLifecycleState.lead).toBe(2); - }); - - test('Company count by segment', async () => { - await companyFactory({}); - await companyFactory({}); - - await segmentFactory({ contentType: 'company' }); - await segmentFactory(); - - const response = await graphqlRequest(qryCount, 'companyCounts', { - only: 'bySegment', - }); - - expect(count(response.bySegment)).toBe(1); }); test('Company count by brand', async () => { const brand = await brandFactory({}); - const integration = await integrationFactory({ brandId: brand._id }); - const integrationId = integration._id; + await integrationFactory({ brandId: brand._id }); - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - await companyFactory({}); - - await customerFactory({ integrationId, companyIds: [company1._id] }); - await customerFactory({ integrationId, companyIds: [company2._id] }); - - const response = await graphqlRequest(qryCount, 'companyCounts', { + await graphqlRequest(qryCount, 'companyCounts', { only: 'byBrand', }); - - expect(response.byBrand[brand._id]).toBe(2); }); test('Company detail', async () => { @@ -378,6 +116,34 @@ describe('companyQueries', () => { query companyDetail($_id: String!) { companyDetail(_id: $_id) { _id + createdAt + modifiedAt + + primaryName + names + size + industry + plan + + parentCompanyId + primaryEmail + emails + ownerId + primaryPhone + phones + businessType + description + doNotDisturb + links + customers { _id } + owner { _id } + parentCompany { _id } + + tagIds + + customFieldsData + + getTags { _id } } } `; diff --git a/src/__tests__/configDb.test.ts b/src/__tests__/configDb.test.ts index 1efe73ad4..d88ce7922 100644 --- a/src/__tests__/configDb.test.ts +++ b/src/__tests__/configDb.test.ts @@ -1,5 +1,6 @@ import { Configs } from '../db/models'; +import { configFactory } from '../db/factories'; import './setup.ts'; describe('Test configs model', () => { @@ -8,6 +9,20 @@ describe('Test configs model', () => { await Configs.deleteMany({}); }); + test('Get config', async () => { + try { + await Configs.getConfig('fakeId'); + } catch (e) { + expect(e.message).toBe('Config not found'); + } + + const config = await configFactory(); + + const response = await Configs.getConfig(config.code); + + expect(response).toBeDefined(); + }); + test('Create or update config', async () => { const code = 'dealCurrency'; const value = ['MNT', 'USD', 'KRW']; @@ -32,4 +47,8 @@ describe('Test configs model', () => { expect(updateConfig.value.length).toEqual(1); expect(updateConfig.value[0]).toEqual('update'); }); + + test('constants', async () => { + Configs.constants(); + }); }); diff --git a/src/__tests__/configMutations.test.ts b/src/__tests__/configMutations.test.ts index 936510392..6564d33ec 100644 --- a/src/__tests__/configMutations.test.ts +++ b/src/__tests__/configMutations.test.ts @@ -1,30 +1,44 @@ +import * as sinon from 'sinon'; +import * as utils from '../data/utils'; import { graphqlRequest } from '../db/connection'; -import { userFactory } from '../db/factories'; +import { Configs } from '../db/models'; import './setup.ts'; describe('Test configs mutations', () => { - test('Insert config', async () => { - const context = { user: await userFactory({}) }; - - const args = { - code: 'dealUOM', - value: ['MNT'], - }; - + test('Update configs', async () => { const mutation = ` - mutation configsInsert($code: String!, $value: [String]!) { - configsInsert(code: $code, value: $value) { - _id - code - value - } + mutation configsUpdate($configsMap: JSON!) { + configsUpdate(configsMap: $configsMap) } `; - const config = await graphqlRequest(mutation, 'configsInsert', args, context); + await graphqlRequest(mutation, 'configsUpdate', { configsMap: { dealUOM: ['MNT'], '': '' } }); + + const uomConfig = await Configs.getConfig('dealUOM'); + + expect(uomConfig.value.length).toEqual(1); + expect(uomConfig.value[0]).toEqual('MNT'); + + // if code is not dealUOM and dealCurrency + await graphqlRequest(mutation, 'configsUpdate', { configsMap: { code: ['USD'] } }); + + const codeConfig = await Configs.getConfig('code'); + + expect(codeConfig.value.length).toEqual(1); + expect(codeConfig.value[0]).toEqual('USD'); + + const firebaseMock = sinon.stub(utils, 'initFirebase').callsFake(); + + await graphqlRequest(mutation, 'configsUpdate', { + configsMap: { GOOGLE_APPLICATION_CREDENTIALS_JSON: ['serviceAccount'] }, + }); + + const googleConfig = await Configs.getConfig('GOOGLE_APPLICATION_CREDENTIALS_JSON'); + + expect(googleConfig.value.length).toEqual(1); + expect(googleConfig.value[0]).toEqual('serviceAccount'); - expect(config.value.length).toEqual(1); - expect(config.value[0]).toEqual('MNT'); + firebaseMock.restore(); }); }); diff --git a/src/__tests__/configQueries.test.ts b/src/__tests__/configQueries.test.ts index 4dbc6807d..85f8bf774 100644 --- a/src/__tests__/configQueries.test.ts +++ b/src/__tests__/configQueries.test.ts @@ -1,26 +1,84 @@ import { graphqlRequest } from '../db/connection'; -import { configFactory } from '../db/factories'; +import * as sinon from 'sinon'; +import * as utils from '../data/utils'; +import { configFactory } from '../db/factories'; import './setup.ts'; describe('configQueries', () => { - it('config detail', async () => { - const config = await configFactory(); - - const args = { code: config.code }; + test('configs', async () => { + await configFactory({}); const qry = ` - query configsDetail($code: String!) { - configsDetail(code: $code) { + query configs { + configs { _id - code - value } } `; - const response = await graphqlRequest(qry, 'configsDetail', args); + const response = await graphqlRequest(qry, 'configs'); + + expect(response.length).toBe(1); + }); + + test('config get env', async () => { + process.env.USE_BRAND_RESTRICTIONS = 'true'; + + const qry = ` + query configsGetEnv { + configsGetEnv { + USE_BRAND_RESTRICTIONS + } + } + `; + + const response = await graphqlRequest(qry, 'configsGetEnv'); + + expect(response.USE_BRAND_RESTRICTIONS).toBe('true'); + }); + + test('configsStatus', async () => { + const qry = ` + query configsStatus { + configsStatus { + erxes { + packageVersion + } + erxesApi { + packageVersion + } + erxesIntegration { + packageVersion + } + } + } + `; + + let config = await graphqlRequest(qry, 'configsStatus'); + + expect(config.erxes.packageVersion).toBe('-'); + expect(config.erxesIntegration.packageVersion).toBeDefined(); + + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve({ packageVersion: '-' }); + }); + + config = await graphqlRequest(qry, 'configsStatus'); + + expect(config.erxes.packageVersion).toBe('-'); + expect(config.erxesIntegration.packageVersion).toBe('-'); + + mock.restore(); + }); + + test('configsConstants', async () => { + const qry = ` + query configsConstants { + configsConstants + } + `; - expect(response.code).toBe(config.code); + await graphqlRequest(qry, 'configsConstants'); }); }); diff --git a/src/__tests__/conformityMutations.test.ts b/src/__tests__/conformityMutations.test.ts new file mode 100644 index 000000000..202aa60b1 --- /dev/null +++ b/src/__tests__/conformityMutations.test.ts @@ -0,0 +1,216 @@ +import { graphqlRequest } from '../db/connection'; +import { companyFactory, conformityFactory, customerFactory, dealFactory, taskFactory } from '../db/factories'; +import { Companies, Conformities, Customers, Deals } from '../db/models'; + +import './setup.ts'; + +describe('mutations', () => { + let _company; + let _customer; + + beforeEach(async () => { + // Creating test data + _company = await companyFactory({}); + _customer = await customerFactory({}); + }); + + afterEach(async () => { + // Clearing test data + Companies.deleteMany({}); + Customers.deleteMany({}); + Deals.deleteMany({}); + Conformities.deleteMany({}); + }); + + test('Edit conformity mutations', async () => { + conformityFactory({ + mainType: 'company', + mainTypeId: _company._id, + relType: 'customer', + relTypeId: _customer._id, + }); + + const company1 = await companyFactory({}); + const customer1 = await customerFactory({}); + const customer2 = await customerFactory({}); + const customer3 = await customerFactory({}); + + const mutation = ` + mutation conformityEdit( + $mainType: String! + $mainTypeId: String! + $relType: String! + $relTypeIds: [String]! + ) { + conformityEdit( + mainType: $mainType + mainTypeId: $mainTypeId + relType: $relType + relTypeIds: $relTypeIds + ) { + success + } + } + `; + + let args = { + mainType: 'company', + mainTypeId: _company._id, + relType: 'customer', + relTypeIds: [customer3._id], + }; + await graphqlRequest(mutation, 'conformityEdit', args); + + let relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relTypes: ['customer'], + }); + + let savedCustomer = await Customers.find({ _id: { $in: relTypeIds } }); + expect(savedCustomer.length).toEqual(1); + + args = { + mainType: 'company', + mainTypeId: _company._id, + relType: 'customer', + relTypeIds: [_customer._id, customer1._id, customer2._id], + }; + await graphqlRequest(mutation, 'conformityEdit', args); + + relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relTypes: ['customer'], + }); + + savedCustomer = await Customers.find({ _id: { $in: relTypeIds } }); + expect(savedCustomer.length).toEqual(3); + + args = { + mainType: 'customer', + mainTypeId: _customer._id, + relType: 'company', + relTypeIds: [_company._id, company1._id], + }; + await graphqlRequest(mutation, 'conformityEdit', args); + + relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relTypes: ['customer'], + }); + + savedCustomer = await Customers.find({ _id: { $in: relTypeIds } }); + expect(savedCustomer.length).toEqual(3); + + relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: company1._id, + relTypes: ['customer'], + }); + + savedCustomer = await Customers.find({ _id: { $in: relTypeIds } }); + expect(savedCustomer.length).toEqual(1); + + args = { + mainType: 'customer', + mainTypeId: customer2._id, + relType: 'company', + relTypeIds: [], + }; + await graphqlRequest(mutation, 'conformityEdit', args); + + relTypeIds = await Conformities.savedConformity({ + mainType: 'customer', + mainTypeId: customer2._id, + relTypes: ['customer'], + }); + + let relatedCompanies = await Companies.find({ _id: { $in: relTypeIds } }); + expect(relatedCompanies.length).toEqual(0); + + const deal = await dealFactory({}); + args = { + mainType: 'customer', + mainTypeId: _customer._id, + relType: 'deal', + relTypeIds: [deal._id], + }; + await graphqlRequest(mutation, 'conformityEdit', args); + + relTypeIds = await Conformities.relatedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relType: 'deal', + }); + + const relatedDeals = await Deals.find({ _id: { $in: relTypeIds } }); + expect(relatedDeals.length).toEqual(1); + + relTypeIds = await Conformities.relatedConformity({ + mainType: 'deal', + mainTypeId: deal._id, + relType: 'company', + }); + + relatedCompanies = await Companies.find({ _id: { $in: relTypeIds } }); + expect(relatedCompanies.length).toEqual(2); + + await taskFactory({}); + await taskFactory({}); + + relTypeIds = await Conformities.relatedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relType: 'task', + }); + expect(relTypeIds.length).toEqual(0); + + relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: _company._id, + relTypes: ['customer'], + }); + + expect(relTypeIds.length).toEqual(2); + }); + + test('Add conformity mutations', async () => { + const company = await companyFactory({}); + const customer = await customerFactory({}); + + const mutation = ` + mutation conformityAdd( + $mainType: String! + $mainTypeId: String! + $relType: String! + $relTypeId: String! + ) { + conformityAdd( + mainType: $mainType + mainTypeId: $mainTypeId + relType: $relType + relTypeId: $relTypeId + ) { + _id + } + } + `; + + const args = { + mainType: 'company', + mainTypeId: company._id, + relType: 'customer', + relTypeId: customer._id, + }; + await graphqlRequest(mutation, 'conformityAdd', args); + + const relTypeIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: company._id, + relTypes: ['customer'], + }); + expect(relTypeIds.length).toEqual(1); + }); +}); diff --git a/src/__tests__/conversationCronJob.test.ts b/src/__tests__/conversationCronJob.test.ts index 1eb62766d..8417b7364 100644 --- a/src/__tests__/conversationCronJob.test.ts +++ b/src/__tests__/conversationCronJob.test.ts @@ -1,3 +1,4 @@ +import * as faker from 'faker'; import * as moment from 'moment'; import { sendMessageEmail } from '../cronJobs/conversations'; import utils from '../data/utils'; @@ -23,8 +24,7 @@ describe('Cronjob conversation send email', () => { beforeEach(async () => { // Creating test data - - _customer = await customerFactory({}); + _customer = await customerFactory({ primaryEmail: faker.internet.email() }); _brand = await brandFactory({}); _user = await userFactory({}); @@ -57,18 +57,19 @@ describe('Cronjob conversation send email', () => { process.env.COMPANY_EMAIL_FROM = ' '; const spyEmail = jest.spyOn(utils, 'sendEmail'); + spyEmail.mockImplementation(); const spyNewOrOpenConversation = jest.spyOn(Conversations, 'newOrOpenConversation'); spyNewOrOpenConversation.mockImplementation(() => [_conversation]); const spyGetNonAsnweredMessage = jest.spyOn(ConversationMessages, 'getNonAsnweredMessage'); - spyGetNonAsnweredMessage.mockImplementation(() => _conversationMessage); + spyGetNonAsnweredMessage.mockImplementation(() => Promise.resolve(_conversationMessage)); const spyGetAdminMessages = jest.spyOn(ConversationMessages, 'getAdminMessages'); - spyGetAdminMessages.mockImplementation(() => [_conversationMessage]); + spyGetAdminMessages.mockImplementation(() => Promise.resolve([_conversationMessage])); const spyMarkSentAsReadMessages = jest.spyOn(ConversationMessages, 'markSentAsReadMessages'); - spyMarkSentAsReadMessages.mockImplementation(() => jest.fn()); + spyMarkSentAsReadMessages.mockImplementation(); // create fake emailSignatures =================== _user.emailSignatures = [{ brandId: _brand.id, signature: 'test' }]; @@ -118,14 +119,15 @@ describe('Cronjob conversation send email', () => { const calledArgs = spyEmail.mock.calls[0][0]; - expect(expectedArgs.toEmails[0]).toBe(calledArgs.toEmails[0]); + expect(expectedArgs.toEmails[0]).toBe(calledArgs.toEmails && calledArgs.toEmails[0]); expect(expectedArgs.title).toBe(calledArgs.title); - expect(expectedArgs.template.name).toBe(calledArgs.template.name); - expect(expectedArgs.template.isCustom).toBe(calledArgs.template.isCustom); - expect(expectedArgs.template.data.question.toJSON()).toEqual(calledArgs.template.data.question); - expect(expectedArgs.template.data.brand.toJSON()).toEqual(calledArgs.template.data.brand.toJSON()); - expect(expectedArgs.template.data.customer.toJSON()).toEqual(calledArgs.template.data.customer.toJSON()); + if (calledArgs.template) { + expect(expectedArgs.template.name).toBe(calledArgs.template.name); + expect(expectedArgs.template.data.question.toJSON()).toEqual(calledArgs.template.data.question); + expect(expectedArgs.template.data.brand.toJSON()).toEqual(calledArgs.template.data.brand.toJSON()); + expect(expectedArgs.template.data.customer.toJSON()).toEqual(calledArgs.template.data.customer.toJSON()); + } // mark as read: check called parameters =============== expect(spyMarkSentAsReadMessages.mock.calls.length).toBe(1); @@ -147,7 +149,8 @@ describe('Cronjob conversation send email', () => { }); test('Conversations utils without answer messages', async () => { - ConversationMessages.getAdminMessages = jest.fn(() => []); + const spyGetAdminMessages = jest.spyOn(ConversationMessages, 'getAdminMessages'); + spyGetAdminMessages.mockImplementation(() => Promise.resolve([])); await sendMessageEmail(); }); diff --git a/src/__tests__/conversationDb.test.ts b/src/__tests__/conversationDb.test.ts index e3fc9790e..40c0ec7af 100644 --- a/src/__tests__/conversationDb.test.ts +++ b/src/__tests__/conversationDb.test.ts @@ -1,4 +1,10 @@ -import { conversationFactory, conversationMessageFactory, customerFactory, userFactory } from '../db/factories'; +import { + conversationFactory, + conversationMessageFactory, + customerFactory, + engageDataFactory, + userFactory, +} from '../db/factories'; import { ConversationMessages, Conversations, Users } from '../db/models'; import { CONVERSATION_STATUSES } from '../db/models/definitions/constants'; @@ -15,6 +21,8 @@ describe('Conversation db', () => { _conversation = await conversationFactory({}); _conversationMessage = await conversationMessageFactory({ conversationId: _conversation._id, + internal: false, + content: 'content', }); _user = await userFactory({}); @@ -31,6 +39,30 @@ describe('Conversation db', () => { await Users.deleteMany({}); }); + test('Get conversation message', async () => { + try { + await ConversationMessages.getMessage('fakeId'); + } catch (e) { + expect(e.message).toBe('Conversation message not found'); + } + + const response = await ConversationMessages.getMessage(_conversationMessage._id); + + expect(response).toBeDefined(); + }); + + test('Get conversation', async () => { + try { + await Conversations.getConversation('fakeId'); + } catch (e) { + expect(e.message).toBe('Conversation not found'); + } + + const response = await Conversations.getConversation(_conversation._id); + + expect(response).toBeDefined(); + }); + test('Create conversation', async () => { const _number = (await Conversations.find().countDocuments()) + 1; const conversation = await Conversations.createConversation({ @@ -39,6 +71,7 @@ describe('Conversation db', () => { assignedUserId: _user._id, participatedUserIds: [_user._id], readUserIds: [_user._id], + status: CONVERSATION_STATUSES.NEW, }); expect(conversation).toBeDefined(); @@ -65,8 +98,6 @@ describe('Conversation db', () => { }); test('Create conversation message', async () => { - expect.assertions(17); - // setting updatedAt to null to check when new message updatedAt field // must be setted _conversation.updatedAt = null; @@ -88,10 +119,10 @@ describe('Conversation db', () => { } expect(updatedConversation.updatedAt).toEqual(expect.any(Date)); + expect(updatedConversation.messageCount).toBe(2); expect(messageObj.content).toBe(_conversationMessage.content); - expect(messageObj.attachments.length).toBe(1); - expect(messageObj.attachments[0].toJSON()).toEqual(_conversationMessage.attachments[0].toJSON()); + expect(messageObj.attachments.length).toBe(0); expect(messageObj.mentionedUserIds[0]).toBe(_conversationMessage.mentionedUserIds[0]); expect(messageObj.conversationId).toBe(_conversation._id); @@ -140,6 +171,21 @@ describe('Conversation db', () => { // check if message count increase expect(afterConversationObj.messageCount).toBe(2); + + // Do not update conversation message count when bot message + _doc.fromBot = true; + _doc.content = 'content'; + _doc.conversationId = messageObj.conversationId; + + let conversation = await Conversations.getConversation(_doc.conversationId); + const prevMessageCount = conversation.messageCount; + + await ConversationMessages.addMessage(_doc, _user); + + conversation = await Conversations.getConversation(_doc.conversationId); + const updatedMessageCount = conversation.messageCount; + + expect(prevMessageCount).toBe(updatedMessageCount); }); // if user assigned to conversation @@ -229,9 +275,28 @@ describe('Conversation db', () => { expect(conversationObj.status).toBe('open'); }); + test('Resolve all conversation', async () => { + // try closing ======================== + const updated = await Conversations.resolveAllConversation({}, _user._id); + + const conversationObj = await Conversations.findOne({ + _id: _conversation._id, + }); + + if (!conversationObj) { + throw new Error('Conversation not found'); + } + + const conversationCount = await Conversations.find().count(); + + expect(conversationObj.closedAt).toEqual(expect.any(Date)); + expect(conversationObj.status).toBe('closed'); + expect(updated.nModified).toBe(conversationCount); + }); + test('Conversation mark as read', async () => { // first user read this conversation - _conversation.readUserIds = ''; + _conversation.readUserIds = []; await _conversation.save(); await Conversations.markAsReadConversation(_conversation._id, _user._id); @@ -246,6 +311,12 @@ describe('Conversation db', () => { expect(conversationObj.readUserIds).toContain(_user._id); + // if current user is in read users list + _conversation.readUserIds = [_user._id]; + await _conversation.save(); + + await Conversations.markAsReadConversation(_conversation._id, _user._id); + const secondUser = await userFactory({}); // multiple users read conversation @@ -260,6 +331,8 @@ describe('Conversation db', () => { }); test('Conversation message', async () => { + await conversationMessageFactory({ conversationId: _conversation._id, internal: false }); + // non answered messages ========= const nonAnweredMessage = await ConversationMessages.getNonAsnweredMessage(_conversation._id); @@ -273,7 +346,7 @@ describe('Conversation db', () => { const adminMessages = await ConversationMessages.getAdminMessages(_conversation._id); - expect(adminMessages.length).toBe(1); + expect(adminMessages.length).toBe(2); // mark sent as read messages ================== await ConversationMessages.updateMany( @@ -346,9 +419,77 @@ describe('Conversation db', () => { customerId: customer._id, }); - await Conversations.removeCustomerConversations(customer._id); + await conversationFactory({ + customerId: customer._id, + }); + + await Conversations.removeCustomersConversations([customer._id]); expect(await Conversations.find({ customerId: customer._id })).toHaveLength(0); expect(await ConversationMessages.find({ conversationId: conversation._id })).toHaveLength(0); }); + + test('forceReadCustomerPreviousEngageMessages', async () => { + const customerId = '_id'; + + // isCustomRead is defined =============== + await conversationMessageFactory({ + customerId, + engageData: engageDataFactory({ messageId: '_id' }), + isCustomerRead: false, + }); + + await ConversationMessages.forceReadCustomerPreviousEngageMessages(customerId); + + let messages = await ConversationMessages.find({ + customerId, + engageData: { $exists: true }, + isCustomerRead: true, + }); + + expect(messages.length).toBe(1); + + // isCustomRead is undefined =============== + await ConversationMessages.deleteMany({}); + + await conversationMessageFactory({ + customerId, + engageData: engageDataFactory({ messageId: '_id' }), + }); + + await ConversationMessages.forceReadCustomerPreviousEngageMessages(customerId); + + messages = await ConversationMessages.find({ + customerId, + engageData: { $exists: true }, + isCustomerRead: true, + }); + + expect(messages.length).toBe(1); + }); + + test('widgetsUnreadMessagesQuery', async () => { + const conversation = await conversationFactory({}); + + const response = await Conversations.widgetsUnreadMessagesQuery([conversation]); + + expect(JSON.stringify(response)).toBe( + JSON.stringify({ + conversationId: { $in: [conversation._id] }, + userId: { $exists: true }, + internal: false, + isCustomerRead: { $ne: true }, + }), + ); + }); + + test('updateConversation', async () => { + const conversation = await conversationFactory({}); + + await Conversations.updateConversation(conversation._id, { content: 'updated' }); + + const updated = await Conversations.findOne({ _id: conversation._id }); + + expect(updated && updated.content).toBe('updated'); + }); }); diff --git a/src/__tests__/conversationMutations.test.ts b/src/__tests__/conversationMutations.test.ts index 2c6138d2b..e5f5940c7 100644 --- a/src/__tests__/conversationMutations.test.ts +++ b/src/__tests__/conversationMutations.test.ts @@ -1,15 +1,21 @@ -import utils from '../data/utils'; -import { graphqlRequest } from '../db/connection'; -import { - conversationFactory, - conversationMessageFactory, - customerFactory, - integrationFactory, - userFactory, -} from '../db/factories'; +import './setup.ts'; + +import * as faker from 'faker'; +import * as sinon from 'sinon'; +import messageBroker from '../messageBroker'; + +import { channelFactory, conversationFactory, customerFactory, integrationFactory, userFactory } from '../db/factories'; import { ConversationMessages, Conversations, Customers, Integrations, Users } from '../db/models'; +import { CONVERSATION_OPERATOR_STATUS, CONVERSATION_STATUSES, KIND_CHOICES } from '../db/models/definitions/constants'; -import './setup.ts'; +import { AUTO_BOT_MESSAGES } from '../data/constants'; +import { IntegrationsAPI } from '../data/dataSources'; +import utils from '../data/utils'; +import { graphqlRequest } from '../db/connection'; +import { IConversationDocument } from '../db/models/definitions/conversations'; +import { ICustomerDocument } from '../db/models/definitions/customers'; +import { IIntegrationDocument } from '../db/models/definitions/integrations'; +import { IUserDocument } from '../db/models/definitions/users'; const toJSON = value => { // sometimes object key order is different even though it has same value. @@ -19,33 +25,120 @@ const toJSON = value => { const spy = jest.spyOn(utils, 'sendNotification'); describe('Conversation message mutations', () => { - let _conversation; - let _conversationMessage; - let _user; - let _integration; - let _customer; - let context; + let leadConversation: IConversationDocument; + let facebookConversation: IConversationDocument; + let facebookMessengerConversation: IConversationDocument; + let messengerConversation: IConversationDocument; + let chatfuelConversation: IConversationDocument; + let twitterConversation: IConversationDocument; + let whatsAppConversation: IConversationDocument; + let viberConversation: IConversationDocument; + let telegramConversation: IConversationDocument; + let lineConversation: IConversationDocument; + let twilioConversation: IConversationDocument; + let telnyxConversation: IConversationDocument; + let leadIntegration: IIntegrationDocument; + + let user: IUserDocument; + let customer: ICustomerDocument; + + const addMutation = ` + mutation conversationMessageAdd( + $conversationId: String + $content: String + $mentionedUserIds: [String] + $internal: Boolean + $attachments: [AttachmentInput] + ) { + conversationMessageAdd( + conversationId: $conversationId + content: $content + mentionedUserIds: $mentionedUserIds + internal: $internal + attachments: $attachments + ) { + conversationId + content + mentionedUserIds + internal + attachments { + url + name + type + size + } + } + } + `; + + let dataSources; beforeEach(async () => { - // Creating test data - _conversation = await conversationFactory({}); - _conversationMessage = await conversationMessageFactory({}); - _user = await userFactory({}); - _customer = await customerFactory({}); - _integration = await integrationFactory({ kind: 'form' }); - _conversation.integrationId = _integration._id; - _conversation.customerId = _customer._id; - _conversation.assignedUserId = _user._id; + dataSources = { IntegrationsAPI: new IntegrationsAPI() }; + + user = await userFactory({}); + customer = await customerFactory({ + primaryEmail: faker.internet.email(), + primaryPhone: faker.phone.phoneNumber(), + phoneValidationStatus: 'valid', + }); + + leadIntegration = await integrationFactory({ + kind: KIND_CHOICES.LEAD, + messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, + }); + + leadConversation = await conversationFactory({ + integrationId: leadIntegration._id, + customerId: customer._id, + assignedUserId: user._id, + participatedUserIds: [user._id], + content: 'lead content', + }); + + const facebookIntegration = await integrationFactory({ kind: KIND_CHOICES.FACEBOOK_POST }); + facebookConversation = await conversationFactory({ integrationId: facebookIntegration._id }); + + const facebookMessengerIntegration = await integrationFactory({ kind: KIND_CHOICES.FACEBOOK_MESSENGER }); + facebookMessengerConversation = await conversationFactory({ integrationId: facebookMessengerIntegration._id }); - await _conversation.save(); + const chatfuelIntegration = await integrationFactory({ kind: KIND_CHOICES.CHATFUEL }); + chatfuelConversation = await conversationFactory({ integrationId: chatfuelIntegration._id }); - context = { user: _user }; + const twitterIntegration = await integrationFactory({ kind: KIND_CHOICES.TWITTER_DM }); + twitterConversation = await conversationFactory({ integrationId: twitterIntegration._id }); + + const whatsAppIntegration = await integrationFactory({ kind: KIND_CHOICES.WHATSAPP }); + whatsAppConversation = await conversationFactory({ integrationId: whatsAppIntegration._id }); + + const viberIntegration = await integrationFactory({ kind: KIND_CHOICES.SMOOCH_VIBER }); + viberConversation = await conversationFactory({ integrationId: viberIntegration._id }); + + const telegramIntegration = await integrationFactory({ kind: KIND_CHOICES.SMOOCH_TELEGRAM }); + telegramConversation = await conversationFactory({ integrationId: telegramIntegration._id }); + + const lineIntegration = await integrationFactory({ kind: KIND_CHOICES.SMOOCH_LINE }); + lineConversation = await conversationFactory({ integrationId: lineIntegration._id }); + + const twilioIntegration = await integrationFactory({ kind: KIND_CHOICES.SMOOCH_TWILIO }); + twilioConversation = await conversationFactory({ integrationId: twilioIntegration._id }); + + const telnyxIntegration = await integrationFactory({ kind: KIND_CHOICES.TELNYX }); + telnyxConversation = await conversationFactory({ integrationId: telnyxIntegration._id, customerId: customer._id }); + + const messengerIntegration = await integrationFactory({ kind: 'messenger' }); + messengerConversation = await conversationFactory({ + customerId: customer._id, + firstRespondedUserId: user._id, + firstRespondedDate: new Date(), + integrationId: messengerIntegration._id, + status: CONVERSATION_STATUSES.CLOSED, + }); }); afterEach(async () => { // Clearing test data await Conversations.deleteMany({}); - await ConversationMessages.deleteMany({}); await Users.deleteMany({}); await Integrations.deleteMany({}); await Customers.deleteMany({}); @@ -53,61 +146,241 @@ describe('Conversation message mutations', () => { spy.mockRestore(); }); - test('Add conversation message', async () => { + test('Add internal conversation message', async () => { + const args = { + conversationId: messengerConversation._id, + content: 'content', + internal: true, + }; + + const response = await graphqlRequest(addMutation, 'conversationMessageAdd', args); + + expect(response.conversationId).toBe(args.conversationId); + expect(response.content).toBe(args.content); + expect(response.internal).toBeTruthy(); + }); + + test('Add lead conversation message', async () => { process.env.DEFAULT_EMAIL_SERIVCE = ' '; process.env.COMPANY_EMAIL_FROM = ' '; const args = { - conversationId: _conversation._id, - content: _conversationMessage.content, - mentionedUserIds: [_user._id], + conversationId: leadConversation._id, + content: 'content', + mentionedUserIds: [user._id], internal: false, attachments: [{ url: 'url', name: 'name', type: 'doc', size: 10 }], }; - const mutation = ` - mutation conversationMessageAdd( + const response = await graphqlRequest(addMutation, 'conversationMessageAdd', args); + + expect(response.content).toBe(args.content); + expect(response.attachments[0]).toEqual({ url: 'url', name: 'name', type: 'doc', size: 10 }); + expect(toJSON(response.mentionedUserIds)).toEqual(toJSON(args.mentionedUserIds)); + expect(response.internal).toBe(args.internal); + }); + + test('Add messenger conversation message', async () => { + const args = { + conversationId: messengerConversation._id, + content: 'content', + fromBot: true, + }; + + const response = await graphqlRequest(addMutation, 'conversationMessageAdd', args); + + expect(response.conversationId).toBe(messengerConversation._id); + }); + + test('Add conversation message using third party integration', async () => { + const mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + return Promise.resolve('success'); + }); + + const args = { conversationId: facebookConversation._id, content: 'content' }; + + const response = await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources: {} }); + + expect(response).toBeDefined(); + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = facebookMessengerConversation._id; + args.content = ''; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = chatfuelConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = twitterConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = whatsAppConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = viberConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = telegramConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = lineConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + args.conversationId = twilioConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + // telnyx + args.conversationId = telnyxConversation._id; + + try { + await graphqlRequest(addMutation, 'conversationMessageAdd', args, { dataSources }); + } catch (e) { + expect(e).toBeDefined(); + } + + mock.restore(); + }); + + test('Reply facebook comment', async () => { + const commentMutation = ` + mutation conversationsReplyFacebookComment( $conversationId: String + $commentId: String $content: String - $mentionedUserIds: [String] - $internal: Boolean - $attachments: [AttachmentInput] ) { - conversationMessageAdd( + conversationsReplyFacebookComment( conversationId: $conversationId + commentId: $commentId content: $content - mentionedUserIds: $mentionedUserIds - internal: $internal - attachments: $attachments ) { conversationId - content - mentionedUserIds - internal - attachments { - url - name - type - size - } + commentId } } `; - const spySendMobileNotification = jest.spyOn(utils, 'sendMobileNotification').mockReturnValueOnce({}); - const message = await graphqlRequest(mutation, 'conversationMessageAdd', args); + let mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + return Promise.resolve('success'); + }); - const calledArgs = spySendMobileNotification.mock.calls[0][0]; + const comment = await integrationFactory({ kind: 'facebook-post' }); - expect(calledArgs.title).toBe('You have a new message.'); - expect(calledArgs.body).toBe(args.content); - expect(calledArgs.receivers).toEqual([_conversation.assignedUserId]); - expect(calledArgs.customerId).toEqual(_conversation.customerId); + const args = { + conversationId: facebookConversation._id, + content: 'content', + commentId: comment._id, + }; - expect(message.content).toBe(args.content); - expect(message.attachments[0]).toEqual({ url: 'url', name: 'name', type: 'doc', size: 10 }); - expect(toJSON(message.mentionedUserIds)).toEqual(toJSON(args.mentionedUserIds)); - expect(message.internal).toBe(args.internal); + const response = await graphqlRequest(commentMutation, 'conversationsReplyFacebookComment', args, { + dataSources: {}, + }); + + expect(response).toBeDefined(); + + mock.restore(); + + mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + throw new Error(); + }); + + try { + await graphqlRequest(commentMutation, 'conversationsReplyFacebookComment', args, { + dataSources: {}, + }); + } catch (e) { + expect(e).toBeDefined(); + } + + mock.restore(); + }); + + test('Change status facebook comment', async () => { + const mutation = ` + mutation conversationsChangeStatusFacebookComment( + $commentId: String, + ) { + conversationsChangeStatusFacebookComment( + commentId: $commentId, + ) { + commentId + } + } + `; + + let mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + return Promise.resolve('success'); + }); + + const comment = await integrationFactory({ kind: 'facebook-post' }); + + const args = { + commentId: comment._id, + }; + + const response = await graphqlRequest(mutation, 'conversationsChangeStatusFacebookComment', args, { + dataSources: {}, + }); + + expect(response).toBeDefined(); + + mock.restore(); + + mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + throw new Error(); + }); + + try { + await graphqlRequest(mutation, 'conversationsChangeStatusFacebookComment', args, { + dataSources: {}, + }); + } catch (e) { + expect(e).toBeDefined(); + } }); test('Assign conversation', async () => { @@ -115,8 +388,8 @@ describe('Conversation message mutations', () => { process.env.COMPANY_EMAIL_FROM = ' '; const args = { - conversationIds: [_conversation._id], - assignedUserId: _user._id, + conversationIds: [leadConversation._id], + assignedUserId: user._id, }; const mutation = ` @@ -135,7 +408,7 @@ describe('Conversation message mutations', () => { } `; - const [conversation] = await graphqlRequest(mutation, 'conversationsAssign', args, context); + const [conversation] = await graphqlRequest(mutation, 'conversationsAssign', args); expect(conversation.assignedUser._id).toEqual(args.assignedUserId); }); @@ -151,9 +424,14 @@ describe('Conversation message mutations', () => { } `; - const [conversation] = await graphqlRequest(mutation, 'conversationsUnassign', { - _ids: [_conversation._id], - }); + const [conversation] = await graphqlRequest( + mutation, + 'conversationsUnassign', + { + _ids: [leadConversation._id], + }, + { user }, + ); expect(conversation.assignedUser).toBe(null); }); @@ -163,7 +441,7 @@ describe('Conversation message mutations', () => { process.env.COMPANY_EMAIL_FROM = ' '; const args = { - _ids: [_conversation._id], + _ids: [leadConversation._id, messengerConversation._id], status: 'closed', }; @@ -178,6 +456,54 @@ describe('Conversation message mutations', () => { const [conversation] = await graphqlRequest(mutation, 'conversationsChangeStatus', args); expect(conversation.status).toEqual(args.status); + + // if status is not closed + args.status = CONVERSATION_STATUSES.OPEN; + + const [openConversation] = await graphqlRequest(mutation, 'conversationsChangeStatus', args); + + expect(openConversation.status).toEqual(args.status); + }); + + test('Resolve all conversation', async () => { + const mutation = ` + mutation conversationResolveAll( + $channelId: String + $status: String + $unassigned: String + $brandId: String + $tag: String + $integrationType: String + $participating: String + $starred: String + $startDate: String + $endDate: String + ) { + conversationResolveAll( + channelId:$channelId + status:$status + unassigned:$unassigned + brandId:$brandId + tag:$tag + integrationType:$integrationType + participating:$participating + starred:$starred + startDate:$startDate + endDate:$endDate + ) + } + `; + + await channelFactory({ integrationIds: [leadIntegration._id], userId: user._id }); + + const updatedConversationCount = await graphqlRequest( + mutation, + 'conversationResolveAll', + { integrationType: leadIntegration.kind }, + { user }, + ); + + expect(updatedConversationCount).toEqual(1); }); test('Mark conversation as read', async () => { @@ -193,8 +519,137 @@ describe('Conversation message mutations', () => { } `; - const conversation = await graphqlRequest(mutation, 'conversationMarkAsRead', { _id: _conversation._id }, context); + const conversation = await graphqlRequest( + mutation, + 'conversationMarkAsRead', + { _id: leadConversation._id }, + { user }, + ); + + expect(conversation.readUserIds).toContain(user._id); + }); + + test('Delete video chat room', async () => { + const mutation = ` + mutation conversationDeleteVideoChatRoom($name: String!) { + conversationDeleteVideoChatRoom(name: $name) + } + `; + + try { + await graphqlRequest(mutation, 'conversationDeleteVideoChatRoom', { name: 'fakeId' }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const mock = sinon.stub(dataSources.IntegrationsAPI, 'deleteDailyVideoChatRoom').callsFake(() => { + return Promise.resolve(true); + }); + + mock.restore(); + }); + + test('Create video chat room', async () => { + const mutation = ` + mutation conversationCreateVideoChatRoom($_id: String!) { + conversationCreateVideoChatRoom(_id: $_id) { + url + name + status + } + } + `; + + const conversation = await conversationFactory(); + + try { + await graphqlRequest(mutation, 'conversationCreateVideoChatRoom', { _id: conversation._id }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const mock = sinon.stub(dataSources.IntegrationsAPI, 'createDailyVideoChatRoom').callsFake(() => { + return Promise.resolve({ status: 'ongoing' }); + }); + + const response = await graphqlRequest( + mutation, + 'conversationCreateVideoChatRoom', + { _id: conversation._id }, + { dataSources }, + ); + + expect(response.status).toBe('ongoing'); + + mock.restore(); + }); + + test('Create product board note', async () => { + const mutation = ` + mutation conversationCreateProductBoardNote($_id: String!) { + conversationCreateProductBoardNote(_id: $_id) + } + `; + + const conversation = await conversationFactory(); + + try { + await graphqlRequest(mutation, 'conversationCreateProductBoardNote', { _id: conversation._id }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const mock = sinon.stub(dataSources.IntegrationsAPI, 'createProductBoardNote').callsFake(() => { + return Promise.resolve('productBoardLink'); + }); + + const response = await graphqlRequest( + mutation, + 'conversationCreateProductBoardNote', + { _id: conversation._id }, + { dataSources }, + ); + + expect(response).toBe('productBoardLink'); + + mock.restore(); + }); + + test('Change conversation operator status', async () => { + const conversation = await conversationFactory({ operatorStatus: CONVERSATION_OPERATOR_STATUS.BOT }); + + const mutation = ` + mutation changeConversationOperator($_id: String!, $operatorStatus: String!) { + changeConversationOperator(_id: $_id, operatorStatus: $operatorStatus) + } + `; - expect(conversation.readUserIds).toContain(_user._id); + await graphqlRequest( + mutation, + 'changeConversationOperator', + { _id: conversation._id, operatorStatus: CONVERSATION_OPERATOR_STATUS.OPERATOR }, + { dataSources }, + ); + + const message = await ConversationMessages.findOne({ conversationId: conversation._id }); + + if (message) { + expect(message.botData).toEqual([ + { + type: 'text', + text: AUTO_BOT_MESSAGES.CHANGE_OPERATOR, + }, + ]); + } else { + fail('Auto message not found'); + } + + const updatedConversation = await Conversations.findOne({ _id: conversation._id }); + + if (updatedConversation) { + expect(updatedConversation.operatorStatus).toBe(CONVERSATION_OPERATOR_STATUS.OPERATOR); + } else { + fail('Conversation not found to update operator status'); + } }); }); diff --git a/src/__tests__/conversationQueries.test.ts b/src/__tests__/conversationQueries.test.ts index 26f249157..7c408be48 100644 --- a/src/__tests__/conversationQueries.test.ts +++ b/src/__tests__/conversationQueries.test.ts @@ -11,6 +11,8 @@ import { } from '../db/factories'; import { Brands, Channels, Conversations, Integrations, Tags, Users } from '../db/models'; +import { IntegrationsAPI } from '../data/dataSources'; +import { MESSAGE_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('conversationQueries', () => { @@ -32,6 +34,7 @@ describe('conversationQueries', () => { $ids: [String] $startDate: String $endDate: String + $awaitingResponse: String `; const commonParams = ` @@ -47,12 +50,33 @@ describe('conversationQueries', () => { ids: $ids startDate: $startDate endDate: $endDate + awaitingResponse: $awaitingResponse `; const qryConversations = ` query conversations(${commonParamDefs}) { conversations(${commonParams}) { _id + } + } + `; + + const qryCount = ` + query conversationCounts(${commonParamDefs}, $only: String) { + conversationCounts(${commonParams}, only: $only) + } + `; + + const qryTotalCount = ` + query conversationsTotalCount(${commonParamDefs}) { + conversationsTotalCount(${commonParams}) + } + `; + + const qryConversationDetail = ` + query conversationDetail($_id: String!) { + conversationDetail(_id: $_id) { + _id content integrationId customerId @@ -66,6 +90,12 @@ describe('conversationQueries', () => { messageCount number tagIds + productBoardLink + videoCallData { + url + name + status + } messages { _id content @@ -101,26 +131,11 @@ describe('conversationQueries', () => { assignedUser { _id } participatedUsers { _id } participatorCount - } - } - `; - - const qryCount = ` - query conversationCounts(${commonParamDefs}, $only: String) { - conversationCounts(${commonParams}, only: $only) - } - `; - - const qryTotalCount = ` - query conversationsTotalCount(${commonParamDefs}) { - conversationsTotalCount(${commonParams}) - } - `; - - const qryConversationDetail = ` - query conversationDetail($_id: String!) { - conversationDetail(_id: $_id) { - _id + idleTime + facebookPost { + postId + } + callProAudio } } `; @@ -139,6 +154,27 @@ describe('conversationQueries', () => { } `; + const qryConversationMessage = ` + query conversationMessages($conversationId: String! $skip: Int $limit: Int) { + conversationMessages(conversationId: $conversationId skip: $skip limit: $limit) { + _id + internal + user { _id } + customer { _id } + mailData { + messageId + } + videoCallData { + url + name + status + } + } + } + `; + + let dataSources; + beforeEach(async () => { brand = await brandFactory(); user = await userFactory({}); @@ -152,6 +188,8 @@ describe('conversationQueries', () => { memberIds: [user._id], integrationIds: [integration._id], }); + + dataSources = { IntegrationsAPI: new IntegrationsAPI() }; }); afterEach(async () => { @@ -172,21 +210,171 @@ describe('conversationQueries', () => { await conversationMessageFactory({ conversationId: conversation._id }); await conversationMessageFactory({ conversationId: conversation._id }); - const qry = ` - query conversationMessages($conversationId: String! $skip: Int $limit: Int) { - conversationMessages(conversationId: $conversationId skip: $skip limit: $limit) { - _id - } - } - `; - - const responses = await graphqlRequest(qry, 'conversationMessages', { + let responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { conversationId: conversation._id, skip: 1, limit: 3, }); expect(responses.length).toBe(3); + + responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: conversation._id, + limit: 3, + }); + + expect(responses.length).toBe(3); + + responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: conversation._id, + }); + + expect(responses.length).toBe(4); + + // conversation is fake + responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: 'fakeConversationId', + }); + + expect(responses.length).toBe(0); + + // internal is true + responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: conversation._id, + }); + + expect(responses.length).toBe(4); + }); + + test('Conversation message video call', async () => { + const conversation = await conversationFactory(); + await conversationMessageFactory({ internal: false, conversationId: conversation._id }); + await conversationMessageFactory({ conversationId: conversation._id }); + + await conversationMessageFactory({ + conversationId: conversation._id, + contentType: MESSAGE_TYPES.VIDEO_CALL, + internal: false, + }); + + let responses = await graphqlRequest( + qryConversationMessage, + 'conversationMessages', + { + conversationId: conversation._id, + }, + { dataSources }, + ); + + expect(responses[0].videoCallData).toBeNull(); + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve({})); + + responses = await graphqlRequest( + qryConversationMessage, + 'conversationMessages', + { + conversationId: conversation._id, + }, + { dataSources }, + ); + + responses = await graphqlRequest( + qryConversationMessage, + 'conversationMessages', + { + conversationId: conversation._id, + }, + { dataSources }, + ); + + expect(responses[0].videoCallData).toBeNull(); + + spy.mockRestore(); + }); + + test('Conversation messages (messenger kind)', async () => { + const messageIntegration = await integrationFactory({ kind: 'messenger' }); + const messageIntegrationConversation = await conversationFactory({ integrationId: messageIntegration._id }); + + await conversationMessageFactory({ conversationId: messageIntegrationConversation._id, internal: false }); + + const responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: messageIntegrationConversation._id, + }); + + expect(responses.length).toBe(1); + }); + + test('Conversation messages (No integration)', async () => { + // no integration + const noIntegrationConversation = await conversationFactory(); + + await conversationMessageFactory({ conversationId: noIntegrationConversation._id, internal: false }); + + const responses = await graphqlRequest(qryConversationMessage, 'conversationMessages', { + conversationId: noIntegrationConversation._id, + }); + + expect(responses.length).toBe(1); + }); + + test('Conversation messages (Integrations api is not running)', async () => { + const nyalsGmailIntegration = await integrationFactory({ kind: 'nylas-gmail' }); + const nyalsGmailConversation = await conversationFactory({ integrationId: nyalsGmailIntegration._id }); + + await conversationMessageFactory({ conversationId: nyalsGmailConversation._id, internal: false }); + + const gmailIntegration = await integrationFactory({ kind: 'gmail' }); + const gmailConversation = await conversationFactory({ integrationId: gmailIntegration._id }); + + await conversationMessageFactory({ conversationId: gmailConversation._id, internal: false }); + + try { + await graphqlRequest( + qryConversationMessage, + 'conversationMessages', + { + conversationId: nyalsGmailConversation._id, + }, + { dataSources }, + ); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + try { + await graphqlRequest( + qryConversationMessage, + 'conversationMessages', + { + conversationId: gmailConversation._id, + }, + { dataSources }, + ); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + }); + + test('Conversation messages total count', async () => { + const conversation = await conversationFactory(); + + await conversationMessageFactory({ conversationId: conversation._id }); + await conversationMessageFactory({ conversationId: conversation._id }); + await conversationMessageFactory({ conversationId: conversation._id }); + await conversationMessageFactory({ conversationId: conversation._id }); + + const qry = ` + query conversationMessagesTotalCount($conversationId: String!) { + conversationMessagesTotalCount(conversationId: $conversationId) + } + `; + + const responses = await graphqlRequest(qry, 'conversationMessagesTotalCount', { conversationId: conversation._id }); + + expect(responses).toBe(4); }); test('Conversations filtered by ids', async () => { @@ -211,11 +399,21 @@ describe('conversationQueries', () => { await conversationFactory(); await conversationFactory(); - const responses = await graphqlRequest(qryConversations, 'conversations', { + let responses = await graphqlRequest(qryConversations, 'conversations', { channelId: channel._id, }); expect(responses.length).toBe(1); + + const channelNoIntegration = await channelFactory({ + memberIds: [user._id], + }); + + responses = await graphqlRequest(qryConversations, 'conversations', { + channelId: channelNoIntegration._id, + }); + + expect(responses.length).toBe(0); }); test('Conversations filtered by brand', async () => { @@ -244,6 +442,15 @@ describe('conversationQueries', () => { expect(responses.length).toBe(1); }); + test('Conversations filtered by awaiting response', async () => { + const conv = await conversationFactory({ integrationId: integration._id }); + await conversationMessageFactory({ conversationId: conv._id, customerId: 'customerId' }); + + const responses = await graphqlRequest(qryConversations, 'conversations', { awaitingResponse: 'true' }, { user }); + + expect(responses.length).toBe(1); + }); + test('Conversations filtered by status', async () => { await conversationFactory({ status: 'closed', @@ -313,8 +520,8 @@ describe('conversationQueries', () => { }); test('Conversations filtered by integration type', async () => { - const integration1 = await integrationFactory({ kind: 'form' }); - const integration2 = await integrationFactory({ kind: 'form' }); + const integration1 = await integrationFactory({ kind: 'lead' }); + const integration2 = await integrationFactory({ kind: 'lead' }); await conversationFactory({ integrationId: integration._id }); await conversationFactory({ integrationId: integration1._id }); @@ -534,8 +741,8 @@ describe('conversationQueries', () => { }); test('Count conversations by integration type', async () => { - const integration1 = await integrationFactory({ kind: 'form' }); - const integration2 = await integrationFactory({ kind: 'form' }); + const integration1 = await integrationFactory({ kind: 'lead' }); + const integration2 = await integrationFactory({ kind: 'lead' }); // conversation with integration type 'messenger' await conversationFactory({ integrationId: integration._id }); @@ -693,8 +900,8 @@ describe('conversationQueries', () => { }); test('Get total count of conversations by integration type', async () => { - const integration1 = await integrationFactory({ kind: 'form' }); - const integration2 = await integrationFactory({ kind: 'form' }); + const integration1 = await integrationFactory({ kind: 'lead' }); + const integration2 = await integrationFactory({ kind: 'lead' }); // integration with type messenger await conversationFactory({ integrationId: integration._id }); @@ -743,6 +950,129 @@ describe('conversationQueries', () => { ); expect(response._id).toBe(conversation._id); + expect(response.facebookPost).toBe(null); + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve([])); + + const facebookIntegration = await integrationFactory({ kind: 'facebook-post' }); + const facebookConversation = await conversationFactory({ integrationId: facebookIntegration._id }); + + try { + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: facebookConversation._id }, + { user, dataSources }, + ); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + + spy.mockRestore(); + }); + + test('Conversation detail video call', async () => { + const messengerConversation = await conversationFactory(); + + await conversationMessageFactory({ + conversationId: messengerConversation._id, + contentType: MESSAGE_TYPES.VIDEO_CALL, + }); + + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: messengerConversation._id }, + { user, dataSources }, + ); + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve('')); + + const response = await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: messengerConversation._id }, + { user, dataSources }, + ); + + expect(response.videoCallData).not.toBeNull(); + + spy.mockRestore(); + }); + + test('Conversation detail product board', async () => { + const messengerConversation = await conversationFactory(); + await conversationMessageFactory({ + conversationId: messengerConversation._id, + contentType: MESSAGE_TYPES.VIDEO_CALL, + }); + + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: messengerConversation._id }, + { user, dataSources }, + ); + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve('')); + + const response = await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: messengerConversation._id }, + { user, dataSources }, + ); + + expect(response.productBoardLink).not.toBeNull(); + + spy.mockRestore(); + }); + + test('Conversation detail callpro audio', async () => { + const callProIntegration = await integrationFactory({ kind: 'callpro' }); + const callProConverstaion = await conversationFactory({ integrationId: callProIntegration._id }); + + try { + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: callProConverstaion._id }, + { user, dataSources }, + ); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + + spy.mockImplementation(() => Promise.resolve()); + + const normalUser = await userFactory({ isOwner: false }); + + try { + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: callProConverstaion._id }, + { user, dataSources }, + ); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + + try { + await graphqlRequest( + qryConversationDetail, + 'conversationDetail', + { _id: callProConverstaion._id }, + { user: normalUser, dataSources }, + ); + } catch (e) { + expect(e[0].message).toBeDefined(); + } }); test('Get last conversation by channel', async () => { @@ -774,4 +1104,48 @@ describe('conversationQueries', () => { expect(response).toBe(1); }); + + test('Facebook comments', async () => { + const qry = ` + query converstationFacebookComments($postId: String!) { + converstationFacebookComments(postId: $postId) { + postId + } + } + `; + + try { + await graphqlRequest(qry, 'converstationFacebookComments', { postId: 'postId' }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve([])); + + await graphqlRequest(qry, 'converstationFacebookComments', { postId: 'postId' }, { dataSources }); + + spy.mockRestore(); + }); + + test('Facebook comments', async () => { + const qry = ` + query converstationFacebookCommentsCount($postId: String!, $isResolved: Boolean) { + converstationFacebookCommentsCount(postId: $postId, isResolved:$isResolved) + } + `; + + try { + await graphqlRequest(qry, 'converstationFacebookComments', { postId: 'postId' }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'fetchApi'); + spy.mockImplementation(() => Promise.resolve([])); + + await graphqlRequest(qry, 'converstationFacebookComments', { postId: 'postId' }, { dataSources }); + + spy.mockRestore(); + }); }); diff --git a/src/__tests__/customerDb.test.ts b/src/__tests__/customerDb.test.ts index 406f82434..879ec38dd 100644 --- a/src/__tests__/customerDb.test.ts +++ b/src/__tests__/customerDb.test.ts @@ -1,4 +1,5 @@ import { + conformityFactory, conversationFactory, conversationMessageFactory, customerFactory, @@ -6,10 +7,22 @@ import { fieldFactory, integrationFactory, internalNoteFactory, + userFactory, } from '../db/factories'; -import { ConversationMessages, Conversations, Customers, Deals, ImportHistory, InternalNotes } from '../db/models'; -import { ACTIVITY_CONTENT_TYPES, STATUSES } from '../db/models/definitions/constants'; - +import { + Conformities, + ConversationMessages, + Conversations, + Customers, + Deals, + ImportHistory, + InternalNotes, +} from '../db/models'; +import { ACTIVITY_CONTENT_TYPES } from '../db/models/definitions/constants'; + +import * as sinon from 'sinon'; +import * as utils from '../data/utils'; +import { ICustomer, ICustomerDocument } from '../db/models/definitions/customers'; import './setup.ts'; describe('Customers model tests', () => { @@ -19,8 +32,9 @@ describe('Customers model tests', () => { _customer = await customerFactory({ primaryEmail: 'email@gmail.com', emails: ['email@gmail.com', 'otheremail@gmail.com'], - primaryPhone: '99922210', - phones: ['99922210', '99922211'], + primaryPhone: '+99922210', + phones: ['+99922210', '+99922211'], + code: 'code', }); }); @@ -30,10 +44,55 @@ describe('Customers model tests', () => { await ImportHistory.deleteMany({}); }); - test('Create customer', async () => { - expect.assertions(12); + test('Get customer', async () => { + try { + await Customers.getCustomer('fakeId'); + } catch (e) { + expect(e.message).toBe('Customer not found'); + } + + const response = await Customers.getCustomer(_customer._id); + + expect(response).toBeDefined(); + }); + + test('Get customer name', async () => { + let customer = await customerFactory({}); + let response = await Customers.getCustomerName(customer); + expect(response).toBe('Unknown'); + + customer = await customerFactory({ firstName: 'firstName' }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('firstName '); + + customer = await customerFactory({ lastName: 'lastName' }); + response = await Customers.getCustomerName(customer); + expect(response).toBe(' lastName'); + customer = await customerFactory({ firstName: 'firstName', lastName: 'lastName' }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('firstName lastName'); + + customer = await customerFactory({ primaryEmail: 'primaryEmail' }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('primaryEmail'); + + customer = await customerFactory({ primaryPhone: 'primaryPhone' }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('primaryPhone'); + + customer = await customerFactory({ visitorContactInfo: { phone: 8880, email: 'email@yahoo.com' } }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('8880'); + + customer = await customerFactory({ visitorContactInfo: { email: 'email@yahoo.com' } }); + response = await Customers.getCustomerName(customer); + expect(response).toBe('email@yahoo.com'); + }); + + test('Create customer', async () => { // check duplication =============== + try { await Customers.createCustomer({ primaryEmail: 'email@gmail.com' }); } catch (e) { @@ -47,37 +106,81 @@ describe('Customers model tests', () => { } try { - await Customers.createCustomer({ primaryPhone: '99922210' }); + await Customers.createCustomer({ primaryPhone: '+99922210' }); } catch (e) { expect(e.message).toBe('Duplicated phone'); } try { - await Customers.createCustomer({ primaryPhone: '99922211' }); + await Customers.createCustomer({ primaryPhone: '+99922211' }); } catch (e) { expect(e.message).toBe('Duplicated phone'); } + try { + await Customers.createCustomer({ code: 'code' }); + } catch (e) { + expect(e.message).toBe('Duplicated code'); + } + // Create without any error - const doc = { + const doc: ICustomer = { primaryEmail: 'dombo@yahoo.com', emails: ['dombo@yahoo.com'], firstName: 'firstName', lastName: 'lastName', - primaryPhone: '12312132', + primaryPhone: '+12312132', phones: ['12312132'], + code: 'code1234', + state: 'lead', }; - const customerObj = await Customers.createCustomer(doc); + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + let customerObj = await Customers.createCustomer(doc); expect(customerObj.createdAt).toBeDefined(); expect(customerObj.modifiedAt).toBeDefined(); expect(customerObj.firstName).toBe(doc.firstName); expect(customerObj.lastName).toBe(doc.lastName); expect(customerObj.primaryEmail).toBe(doc.primaryEmail); - expect(customerObj.emails).toEqual(expect.arrayContaining(doc.emails)); + expect(customerObj.emails).toEqual(expect.arrayContaining(doc.emails || [])); expect(customerObj.primaryPhone).toBe(doc.primaryPhone); - expect(customerObj.phones).toEqual(expect.arrayContaining(doc.phones)); + expect(customerObj.phones).toEqual(expect.arrayContaining(doc.phones || [])); + expect(customerObj.searchText).toEqual('dombo@yahoo.com 12312132 firstName lastName code1234'); + + customerObj = await Customers.createCustomer( + { + visitorContactInfo: {}, + }, + await userFactory(), + ); + + expect(customerObj).toBeDefined(); + mock.restore(); + }); + + test('Create visitor', async () => { + const customerId = await Customers.createVisitor(); + + expect(customerId).toBeDefined(); + }); + + test('Create customer: searchText', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const doc = { + primaryEmail: 'dombo@yahoo.com', + primaryPhone: '+12312132', + }; + + const customerObj = await Customers.createCustomer(doc); + + expect(customerObj.searchText).toEqual('dombo@yahoo.com +12312132'); + mock.restore(); }); test('Create customer: with customer fields validation error', async () => { @@ -93,7 +196,7 @@ describe('Customers model tests', () => { await Customers.createCustomer({ primaryEmail: 'email', emails: ['dombo@yahoo.com'], - customFieldsData: { [field._id]: 'invalid number' }, + customFieldsData: [{ field: field._id, value: 'invalid number' }], }); } catch (e) { expect(e.message).toBe(`${field.text}: Invalid number`); @@ -101,7 +204,11 @@ describe('Customers model tests', () => { }); test('Update customer', async () => { - expect.assertions(5); + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + expect.assertions(6); const previousCustomer = await customerFactory({ primaryEmail: 'dombo@yahoo.com', @@ -112,7 +219,7 @@ describe('Customers model tests', () => { firstName: 'Dombo', primaryEmail: 'dombo@yahoo.com', emails: ['dombo@yahoo.com'], - primaryPhone: '242442200', + primaryPhone: '+242442200', phones: ['242442200'], }; @@ -126,27 +233,33 @@ describe('Customers model tests', () => { // remove previous duplicated entry await Customers.deleteOne({ _id: previousCustomer._id }); - const customerObj = await Customers.updateCustomer(_customer._id, doc); + let customerObj = await Customers.updateCustomer(_customer._id, doc); expect(customerObj.modifiedAt).toBeDefined(); expect(customerObj.firstName).toBe(doc.firstName); expect(customerObj.primaryEmail).toBe(doc.primaryEmail); expect(customerObj.primaryPhone).toBe(doc.primaryPhone); + + customerObj = await Customers.updateCustomer(_customer._id, { primaryEmail: '' }); + + expect(customerObj.primaryEmail).toBe(''); + + mock.restore(); }); test('Mark customer as inactive', async () => { const customer = await customerFactory({ - messengerData: { isActive: true, lastSeenAt: null }, + isOnline: true, }); const customerObj = await Customers.markCustomerAsNotActive(customer._id); - if (!customerObj || !customerObj.messengerData) { + if (!customerObj || !customerObj) { throw new Error('Customer not found'); } - expect(customerObj.messengerData.isActive).toBe(false); - expect(customerObj.messengerData.lastSeenAt).toBeDefined(); + expect(customerObj.isOnline).toBe(false); + expect(customerObj.lastSeenAt).toBeDefined(); }); test('Update customer: with customer fields validation error', async () => { @@ -162,21 +275,13 @@ describe('Customers model tests', () => { await Customers.updateCustomer(_customer._id, { primaryEmail: 'email', emails: ['dombo@yahoo.com'], - customFieldsData: { [field._id]: 'invalid number' }, + customFieldsData: [{ field: field._id, value: 'invalid number' }], }); } catch (e) { expect(e.message).toBe(`${field.text}: Invalid number`); } }); - test('Update customer companies', async () => { - const companyIds = ['12313qwrqwe', '123', '11234']; - - const customerObj = await Customers.updateCompanies(_customer._id, companyIds); - - expect(customerObj.companyIds).toEqual(expect.arrayContaining(companyIds)); - }); - test('removeCustomer', async () => { const customer = await customerFactory({}); @@ -189,12 +294,16 @@ describe('Customers model tests', () => { customerId: customer._id, }); + await conversationFactory({ + customerId: customer._id, + }); + await conversationMessageFactory({ conversationId: conversation._id, customerId: customer._id, }); - await Customers.removeCustomer(customer._id); + await Customers.removeCustomers([customer._id]); const internalNote = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, @@ -208,6 +317,9 @@ describe('Customers model tests', () => { }); test('Merge customers: without emails or phones', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); const visitor1 = await customerFactory({}); const visitor2 = await customerFactory({}); @@ -215,43 +327,76 @@ describe('Customers model tests', () => { const merged = await Customers.mergeCustomers(customerIds, { primaryEmail: 'merged@gmail.com', - primaryPhone: '2555225', + primaryPhone: '+2555225', }); expect(merged.emails).toContain('merged@gmail.com'); - expect(merged.phones).toContain('2555225'); + expect(merged.phones).toContain('+2555225'); + mock.restore(); + }); + + test('Merge customers: without primaryEmail and primaryPhone', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const visitor1 = await customerFactory({}); + const visitor2 = await customerFactory({}); + + const customerIds = [visitor1._id, visitor2._id]; + + const merged = await Customers.mergeCustomers(customerIds, {}); + + expect(merged.emails).toHaveLength(0); + expect(merged.phones).toHaveLength(0); + mock.restore(); }); test('Merge customers', async () => { - expect.assertions(20); + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + expect.assertions(19); const integration = await integrationFactory({}); const customer1 = await customerFactory({ - companyIds: ['123', '1234', '12345'], tagIds: ['2343', '234', '234'], integrationId: integration._id, }); - const customer2 = await customerFactory({ - companyIds: ['123', '456', '45678'], tagIds: ['qwe', '2343', '123'], integrationId: integration._id, }); - if (!customer1 || !customer1.companyIds || !customer1.tagIds) { + await ['123', '1234', '12345'].map(async item => { + await conformityFactory({ + mainType: 'company', + mainTypeId: item, + relType: 'customer', + relTypeId: customer1._id, + }); + }); + + await ['123', '456', '45678'].map(async item => { + await conformityFactory({ + mainType: 'customer', + mainTypeId: customer2._id, + relType: 'company', + relTypeId: item, + }); + }); + + if (!customer1 || !customer1.tagIds) { throw new Error('Customer1 not found'); } - if (!customer2 || !customer2.companyIds || !customer2.tagIds) { + if (!customer2 || !customer2.tagIds) { throw new Error('Customer2 not found'); } const customerIds = [customer1._id, customer2._id]; - // Merging both customers companyIds and tagIds - const mergedCompanyIds = Array.from(new Set(customer1.companyIds.concat(customer2.companyIds))); - const mergedTagIds = Array.from(new Set(customer1.tagIds.concat(customer2.tagIds))); try { @@ -276,15 +421,29 @@ describe('Customers model tests', () => { customerId: customerIds[0], }); - await dealFactory({ - customerIds, + const deal1 = await dealFactory({}); + + customerIds.map(async customerId => { + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal1._id, + relType: 'customer', + relTypeId: customerId, + }); + }); + + // Merging both customers companyIds and tagIds + const mergedCompanyIds = await Conformities.filterConformity({ + mainType: 'customer', + mainTypeIds: customerIds, + relType: 'company', }); const doc = { firstName: 'Test first name', lastName: 'Test last name', primaryEmail: 'Test email', - primaryPhone: 'Test phone', + primaryPhone: '+12049124', messengerData: { sessionCount: 6, }, @@ -295,9 +454,9 @@ describe('Customers model tests', () => { ownerId: '456', }; - const mergedCustomer = await Customers.mergeCustomers(customerIds, doc); + const mergedCustomer = await Customers.mergeCustomers([...customerIds, 'fakeId'], doc); - if (!mergedCustomer || !mergedCustomer.messengerData || !mergedCustomer.visitorContactInfo) { + if (!mergedCustomer || !mergedCustomer.visitorContactInfo) { throw new Error('Merged customer not found'); } @@ -306,8 +465,14 @@ describe('Customers model tests', () => { expect(mergedCustomer.lastName).toBe(doc.lastName); expect(mergedCustomer.primaryEmail).toBe(doc.primaryEmail); expect(mergedCustomer.primaryPhone).toBe(doc.primaryPhone); - expect(mergedCustomer.messengerData.toJSON()).toEqual(doc.messengerData); - expect(mergedCustomer.companyIds).toEqual(expect.arrayContaining(mergedCompanyIds)); + + const companyIds = await Conformities.savedConformity({ + mainType: 'customer', + relTypes: ['company'], + mainTypeId: mergedCustomer._id, + }); + expect(mergedCompanyIds.sort()).toEqual(companyIds.sort()); + expect(mergedCustomer.tagIds).toEqual(expect.arrayContaining(mergedTagIds)); expect(mergedCustomer.visitorContactInfo.toJSON()).toEqual(doc.visitorContactInfo); expect(mergedCustomer.ownerId).toBe('456'); @@ -315,7 +480,7 @@ describe('Customers model tests', () => { // Checking old customers datas to be deleted const oldCustomer = (await Customers.findOne({ _id: customerIds[0] })) || { status: '' }; - expect(oldCustomer.status).toBe(STATUSES.DELETED); + expect(oldCustomer.status).toBe('deleted'); expect(await Conversations.find({ customerId: customerIds[0] })).toHaveLength(0); expect(await ConversationMessages.find({ customerId: customerIds[0] })).toHaveLength(0); @@ -337,18 +502,259 @@ describe('Customers model tests', () => { expect(internalNote).not.toHaveLength(0); + const relTypeIds = await Conformities.filterConformity({ + mainType: 'customer', + mainTypeIds: customerIds, + relType: 'deal', + }); + expect(relTypeIds.length).toBe(0); + + const newRelTypeIds = await Conformities.savedConformity({ + mainType: 'customer', + mainTypeId: mergedCustomer._id, + relTypes: ['deal'], + }); + const deals = await Deals.find({ - customerIds: { $in: customerIds }, + _id: { $in: newRelTypeIds }, + }); + + expect(deals).toHaveLength(1); + mock.restore(); + }); + + test('Update profile score', async () => { + const customer = await customerFactory({}); + + const response = await Customers.calcPSS({ ...customer }); + + expect(response.profileScore).toBe(0); + }); + + test('Mark as active', async () => { + const customer = await customerFactory({}); + + const response = await Customers.markCustomerAsActive(customer._id); + + expect(response.isOnline).toBeTruthy(); + }); + + test('createMessengerCustomer() must return a new customer', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const now = new Date(); + + const email = 'uniqueEmail@gmail.com'; + const phone = '+422999'; + + const customer = await Customers.createMessengerCustomer({ + doc: { + integrationId: _customer.integrationId, + email, + phone, + isUser: _customer.isUser, + }, + customData: { + firstName: 'firstName', + }, + }); + + expect(customer).toBeDefined(); + + expect(customer.createdAt).toBeDefined(); + expect(customer.searchText).toBeDefined(); + expect(customer.profileScore).toBeDefined(); + expect(customer.modifiedAt).toBeDefined(); + expect(customer.primaryEmail).toBe(email); + expect(customer.emails).toContain(email); + + expect(customer.primaryPhone).toBe(phone); + expect(customer.phones).toContain(phone); + + expect(customer.createdAt >= now).toBe(true); + + expect(customer.lastSeenAt).toBeDefined(); + expect(customer.isOnline).toBe(true); + expect(customer.sessionCount).toBe(1); + + expect(customer.firstName).toBe('firstName'); + mock.restore(); + }); + + test('updateMessengerCustomer()', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const integration = await integrationFactory(); + + try { + await Customers.updateMessengerCustomer({ _id: '_id', doc: { integrationId: integration._id }, customData: {} }); + } catch (e) { + expect(e.message).toBe('Customer not found'); + } + + const email = 'uniqueEmail@gmail.com'; + const phone = '+422999'; + const deviceToken = 'token'; + + _customer.isUser = true; + await _customer.save(); + + const customer = await Customers.updateMessengerCustomer({ + _id: _customer._id, + doc: { + integrationId: integration._id, + email, + phone, + isUser: true, + deviceToken, + }, + }); + + expect(customer.primaryEmail).toBe(email); + expect(customer.emails).toContain(email); + expect(customer.deviceTokens).toContain(deviceToken); + + expect(customer.primaryPhone).toBe(phone); + expect(customer.phones).toContain(phone); + mock.restore(); + }); + + test('getWidgetCustomer()', async () => { + // emails, primaryEmail ============== + let customer: ICustomerDocument | null = await customerFactory({ + primaryEmail: 'customer@gmail.com', + emails: ['main@gmail.com'], + }); + + let foundCustomer = await Customers.getWidgetCustomer({ + email: 'customer@gmail.com', + }); + + expect(foundCustomer && foundCustomer._id).toBe(customer._id); + + // phones + customer = await customerFactory({ + phones: ['911111'], + }); + + foundCustomer = await Customers.getWidgetCustomer({ + phone: '911111', + }); + + expect(foundCustomer && foundCustomer._id).toBe(customer._id); + + // primaryPhone + customer = await customerFactory({ + primaryPhone: '24244242', + }); + + foundCustomer = await Customers.getWidgetCustomer({ + phone: '24244242', + }); + + expect(foundCustomer && foundCustomer._id).toBe(customer._id); + + // code + customer = await customerFactory({ + code: '24244242', + }); + + foundCustomer = await Customers.getWidgetCustomer({ + code: '24244242', }); - expect(deals.length).toBe(0); + expect(foundCustomer && foundCustomer._id).toBe(customer._id); - const deal = await Deals.findOne({ - customerIds: { $in: [mergedCustomer._id] }, + // cached customer id + foundCustomer = await Customers.getWidgetCustomer({ + cachedCustomerId: customer._id, }); - if (!deal) { - throw new Error('Deal not found'); + + expect(foundCustomer && foundCustomer._id).toBe(customer._id); + + // related integrationIds + + customer = await customerFactory({ + relatedIntegrationIds: ['123'], + code: '1234', + }); + + foundCustomer = await Customers.getWidgetCustomer({ + integrationId: '1234', + code: '1234', + }); + + expect(foundCustomer && foundCustomer._id).toBe(customer._id); + }); + + test('updateSession()', async () => { + try { + await Customers.updateSession('_id'); + } catch (e) { + expect(e.message).toBe('Customer not found'); } - expect(deal.customerIds).toContain(mergedCustomer._id); + + const now = new Date(); + + const customer = await Customers.updateSession(_customer._id); + + expect(customer.isOnline).toBeTruthy(); + expect(customer.lastSeenAt && customer.lastSeenAt.getTime() >= now.getTime()).toBeTruthy(); + }); + + test('saveVisitorContactInfo()', async () => { + // email ========== + let customer = await Customers.saveVisitorContactInfo({ + customerId: _customer._id, + type: 'email', + value: 'test@gmail.com', + }); + + let visitorContactInfo: any = customer.visitorContactInfo || {}; + + expect(visitorContactInfo.email).toBe('test@gmail.com'); + + // phone =============== + customer = await Customers.saveVisitorContactInfo({ + customerId: _customer._id, + type: 'phone', + value: '985435353', + }); + + visitorContactInfo = customer.visitorContactInfo || {}; + + // check company in companyIds + expect(visitorContactInfo.phone).toBe('985435353'); + }); + + test('updateLocation()', async () => { + const updated = await Customers.updateLocation(_customer._id, { + language: 'en', + }); + + expect(updated.location && updated.location.language).toBe('en'); + }); + + test('changeState()', async () => { + const updated = await Customers.changeState(_customer._id, 'state'); + + expect(updated.state).toBe('state'); + }); + + test('changeVerificationStatus()', async () => { + const phoneResult = await Customers.updateVerificationStatus([_customer._id], 'phone', 'valid'); + + phoneResult.forEach(c => { + expect(c.phoneValidationStatus).toBe('valid'); + }); + + const emailResult = await Customers.updateVerificationStatus([_customer._id], 'email', 'valid'); + + emailResult.forEach(c => { + expect(c.emailValidationStatus).toBe('valid'); + }); }); }); diff --git a/src/__tests__/customerMutations.test.ts b/src/__tests__/customerMutations.test.ts index b945d25bf..4c7c5bce9 100644 --- a/src/__tests__/customerMutations.test.ts +++ b/src/__tests__/customerMutations.test.ts @@ -1,8 +1,9 @@ import * as faker from 'faker'; +import * as sinon from 'sinon'; +import * as utils from '../data/utils'; import { graphqlRequest } from '../db/connection'; -import { customerFactory, userFactory } from '../db/factories'; -import { Customers, Users } from '../db/models'; - +import { customerFactory, integrationFactory, userFactory } from '../db/factories'; +import { Brands, Customers, Integrations, Users } from '../db/models'; import './setup.ts'; /* @@ -18,11 +19,10 @@ const args = { ownerId: faker.random.word(), position: faker.random.word(), department: faker.random.word(), - leadStatus: 'open', - lifecycleState: 'lead', - hasAuthority: faker.random.word(), + leadStatus: 'new', + hasAuthority: 'No', description: faker.random.word(), - doNotDisturb: faker.random.word(), + doNotDisturb: 'Yes', links: { linkedIn: 'linkedIn', twitter: 'twitter', @@ -31,13 +31,30 @@ const args = { github: 'github', website: 'website', }, - customFieldsData: {}, +}; + +const checkCustomer = src => { + expect(src.firstName).toBe(args.firstName); + expect(src.lastName).toBe(args.lastName); + expect(src.primaryEmail).toBe(args.primaryEmail); + expect(src.emails).toEqual(expect.arrayContaining(args.emails)); + expect(src.primaryPhone).toBe(args.primaryPhone); + expect(src.phones).toEqual(expect.arrayContaining(args.phones)); + expect(src.ownerId).toBe(args.ownerId); + expect(src.position).toBe(args.position); + expect(src.department).toBe(args.department); + expect(src.leadStatus).toBe(args.leadStatus); + expect(src.hasAuthority).toBe(args.hasAuthority); + expect(src.description).toBe(args.description); + expect(src.doNotDisturb).toBe(args.doNotDisturb); + expect(src.links).toEqual(args.links); }; describe('Customers mutations', () => { let _user; let _customer; let context; + let integration; const commonParamDefs = ` $firstName: String @@ -50,7 +67,6 @@ describe('Customers mutations', () => { $position: String $department: String $leadStatus: String - $lifecycleState: String $hasAuthority: String $description: String $doNotDisturb: String @@ -69,7 +85,6 @@ describe('Customers mutations', () => { position: $position department: $department leadStatus: $leadStatus - lifecycleState: $lifecycleState hasAuthority: $hasAuthority description: $description doNotDisturb: $doNotDisturb @@ -79,8 +94,9 @@ describe('Customers mutations', () => { beforeEach(async () => { // Creating test data + integration = await integrationFactory(); _user = await userFactory({}); - _customer = await customerFactory({}); + _customer = await customerFactory({ integrationId: integration._id }); context = { user: _user }; }); @@ -89,9 +105,15 @@ describe('Customers mutations', () => { // Clearing test data await Users.deleteMany({}); await Customers.deleteMany({}); + await Brands.deleteMany({}); + await Integrations.deleteMany({}); }); test('Add customer', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const mutation = ` mutation customersAdd(${commonParamDefs}){ customersAdd(${commonParams}) { @@ -105,18 +127,10 @@ describe('Customers mutations', () => { position department leadStatus - lifecycleState hasAuthority description doNotDisturb - links { - linkedIn - twitter - facebook - youtube - github - website - } + links customFieldsData } } @@ -124,25 +138,19 @@ describe('Customers mutations', () => { const customer = await graphqlRequest(mutation, 'customersAdd', args, context); - expect(customer.firstName).toBe(args.firstName); - expect(customer.lastName).toBe(args.lastName); - expect(customer.primaryEmail).toBe(args.primaryEmail); - expect(customer.emails).toEqual(expect.arrayContaining(args.emails)); - expect(customer.primaryPhone).toBe(args.primaryPhone); - expect(customer.phones).toEqual(expect.arrayContaining(args.phones)); - expect(customer.ownerId).toBe(args.ownerId); - expect(customer.position).toBe(args.position); - expect(customer.department).toBe(args.department); - expect(customer.leadStatus).toBe(args.leadStatus); - expect(customer.lifecycleState).toBe(args.lifecycleState); - expect(customer.hasAuthority).toBe(args.hasAuthority); - expect(customer.description).toBe(args.description); - expect(customer.doNotDisturb).toBe(args.doNotDisturb); - expect(customer.links).toEqual(args.links); - expect(customer.customFieldsData).toEqual(args.customFieldsData); + checkCustomer(customer); + expect(customer.emailValidationStatus).toBe(undefined); + expect(customer.phoneValidationStatus).toBe(undefined); + expect(customer.customFieldsData.length).toEqual(0); + + mock.restore(); }); test('Edit customer', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const mutation = ` mutation customersEdit($_id: String! ${commonParamDefs}){ customersEdit(_id: $_id ${commonParams}) { @@ -157,18 +165,10 @@ describe('Customers mutations', () => { position department leadStatus - lifecycleState hasAuthority description doNotDisturb - links { - linkedIn - twitter - facebook - youtube - github - website - } + links customFieldsData } } @@ -177,47 +177,12 @@ describe('Customers mutations', () => { const customer = await graphqlRequest(mutation, 'customersEdit', { _id: _customer._id, ...args }, context); expect(customer._id).toBe(_customer._id); - expect(customer.firstName).toBe(args.firstName); - expect(customer.lastName).toBe(args.lastName); - expect(customer.primaryEmail).toBe(args.primaryEmail); - expect(customer.emails).toEqual(expect.arrayContaining(args.emails)); - expect(customer.primaryPhone).toBe(args.primaryPhone); - expect(customer.phones).toEqual(expect.arrayContaining(args.phones)); - expect(customer.ownerId).toBe(args.ownerId); - expect(customer.position).toBe(args.position); - expect(customer.department).toBe(args.department); - expect(customer.leadStatus).toBe(args.leadStatus); - expect(customer.lifecycleState).toBe(args.lifecycleState); - expect(customer.hasAuthority).toBe(args.hasAuthority); - expect(customer.description).toBe(args.description); - expect(customer.doNotDisturb).toBe(args.doNotDisturb); - expect(customer.links).toEqual(args.links); - expect(customer.customFieldsData).toEqual({}); - }); - - test('Edit company of customer', async () => { - const params = { - _id: _customer._id, - companyIds: [faker.random.uuid()], - }; + expect(customer.emailValidationStatus).toBe(undefined); + expect(customer.phoneValidationStatus).toBe(undefined); - const mutation = ` - mutation customersEditCompanies($_id: String! $companyIds: [String]) { - customersEditCompanies(_id: $_id companyIds: $companyIds) { - _id - } - } - `; - - await graphqlRequest(mutation, 'customersEditCompanies', params, context); - - const customer = await Customers.findOne({ _id: params._id }); - - if (!customer) { - throw new Error('Customer not found'); - } - - expect(customer.companyIds).toContain(params.companyIds); + checkCustomer(customer); + expect(customer.customFieldsData.length).toEqual(0); + mock.restore(); }); test('Remove customer', async () => { @@ -252,4 +217,62 @@ describe('Customers mutations', () => { expect(customer.firstName).toBe(params.customerFields.firstName); }); + + test('Change state', async () => { + const mutation = ` + mutation customersChangeState($_id: String!, $value: String!) { + customersChangeState(_id: $_id, value: $value) { + _id + state + } + } + `; + + await graphqlRequest(mutation, 'customersChangeState', { _id: _customer._id, value: 'customer' }, context); + + const updatedCustomer = await Customers.getCustomer(_customer._id); + + expect(updatedCustomer.state).toBe('customer'); + }); + + test('Verify emails', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const mutation = ` + mutation customersVerify($verificationType: String!) { + customersVerify(verificationType: $verificationType) + } + `; + + await graphqlRequest(mutation, 'customersVerify', { verificationType: 'email' }, context); + + mock.restore(); + }); + + test('Change verification status', async () => { + const mutation = ` + mutation customersChangeVerificationStatus($customerIds: [String], $type: String!, $status: String!) { + customersChangeVerificationStatus(customerIds: $customerIds, type: $type, status: $status) { + _id + state + emailValidationStatus + phoneValidationStatus + } + } + `; + + await graphqlRequest( + mutation, + 'customersChangeVerificationStatus', + { customerIds: [_customer._id], type: 'email', status: 'valid' }, + context, + ); + + const updatedCustomers = await Customers.find({ _id: { $in: [_customer._id] } }); + updatedCustomers.forEach(c => { + expect(c.emailValidationStatus).toBe('valid'); + }); + }); }); diff --git a/src/__tests__/customerQueries.test.ts b/src/__tests__/customerQueries.test.ts index e4c48e617..34c6d548e 100644 --- a/src/__tests__/customerQueries.test.ts +++ b/src/__tests__/customerQueries.test.ts @@ -1,474 +1,267 @@ -import * as faker from 'faker'; -import * as moment from 'moment'; -import { graphqlRequest } from '../db/connection'; -import { customerFactory, formFactory, integrationFactory, segmentFactory, tagsFactory } from '../db/factories'; -import { Customers, Segments, Tags } from '../db/models'; - -import './setup.ts'; - -const count = response => { - return Object.keys(response).length; -}; - -describe('customerQueries', () => { - const commonParamDefs = ` - $page: Int, - $perPage: Int, - $segment: String, - $tag: String, - $ids: [String], - $searchValue: String, - $form: String, - $startDate: String, - $endDate: String, - $lifecycleState: String, - $leadStatus: String - `; - - const commonParams = ` - page: $page - perPage: $perPage - segment: $segment - tag: $tag - ids: $ids - searchValue: $searchValue - form: $form - startDate: $startDate - endDate: $endDate - lifecycleState: $lifecycleState - leadStatus: $leadStatus - `; - - const qryCustomers = ` - query customers(${commonParamDefs}) { - customers(${commonParams}) { - _id - createdAt - modifiedAt - integrationId - firstName - lastName - primaryEmail - emails - primaryPhone - phones - isUser - tagIds - remoteAddress - internalNotes - location - visitorContactInfo - customFieldsData - messengerData - ownerId - position - department - leadStatus - lifecycleState - hasAuthority - description - doNotDisturb - links { - linkedIn - twitter - facebook - youtube - github - website - } - companies { _id } - conversations { _id } - deals { _id } - getIntegrationData - getMessengerCustomData - getTags { _id } - owner { _id } - } - } - `; - - const qryCustomersMain = ` - query customersMain(${commonParamDefs}) { - customersMain(${commonParams}) { - list { - _id - firstName - lastName - primaryEmail - primaryPhone - tagIds - } - totalCount - } - } - `; - - const qryCount = ` - query customerCounts(${commonParamDefs} $byFakeSegment: JSON, $only: String) { - customerCounts(${commonParams} byFakeSegment: $byFakeSegment, only: $only) - } - `; - - const firstName = faker.name.firstName(); - const lastName = faker.name.lastName(); - const primaryEmail = faker.internet.email(); - const primaryPhone = '12345678'; - - afterEach(async () => { - // Clearing test data - await Customers.deleteMany({}); - await Segments.deleteMany({}); - await Tags.deleteMany({}); - }); - - test('Customers', async () => { - const integration = await integrationFactory(); - await customerFactory({ integrationId: integration._id }, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCustomers, 'customers', args); - - expect(responses.length).toBe(3); - }); - - test('Customers filtered by ids', async () => { - const customer1 = await customerFactory({}, true); - const customer2 = await customerFactory({}, true); - const customer3 = await customerFactory({}, true); - - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const ids = [customer1._id, customer2._id, customer3._id]; - - const responses = await graphqlRequest(qryCustomers, 'customers', { ids }); - - expect(responses.length).toBe(3); - }); - - test('Customers filtered by tag', async () => { - const tag = await tagsFactory({}); - - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ tagIds: [tag._id] }, true); - await customerFactory({ tagIds: [tag._id] }, true); - - const tagResponse = await Tags.findOne({}, '_id'); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - tag: tagResponse ? tagResponse._id : '', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by leadStatus', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ leadStatus: 'new' }, true); - await customerFactory({ leadStatus: 'new' }, true); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - leadStatus: 'new', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by lifecycleState', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - - const responses = await graphqlRequest(qryCustomers, 'customers', { - lifecycleState: 'subscriber', - }); - - expect(responses.length).toBe(2); - }); - - test('Customers filtered by segment', async () => { - await customerFactory({ firstName }, true); - await customerFactory({}, true); - - const args = { - contentType: 'customer', - conditions: [ - { - field: 'firstName', - operator: 'c', - value: firstName, - type: 'string', - }, - ], - }; - - const segment = await segmentFactory(args); - - const response = await graphqlRequest(qryCustomers, 'customers', { - segment: segment._id, - }); - - expect(response.length).toBe(1); - }); - - test('Customers filtered by search value', async () => { - await customerFactory({ firstName }, true); - await customerFactory({ lastName }, true); - await customerFactory({ primaryPhone, phones: [primaryPhone] }, true); - await customerFactory({ primaryEmail, emails: [primaryEmail] }, true); - - // customers by firstName ============== - let responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: firstName, - }); - - expect(responses.length).toBe(1); - expect(responses[0].firstName).toBe(firstName); - - // customers by lastName =========== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: lastName, - }); - - expect(responses.length).toBe(1); - expect(responses[0].lastName).toBe(lastName); - - // customers by email ========== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: primaryEmail, - }); - - expect(responses.length).toBe(1); - expect(responses[0].primaryEmail).toBe(primaryEmail); - - // customers by phone ============== - responses = await graphqlRequest(qryCustomers, 'customers', { - searchValue: primaryPhone, - }); - - expect(responses.length).toBe(1); - expect(responses[0].primaryPhone).toBe(primaryPhone); - }); - - test('Main customers', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({}, true); - - const args = { page: 1, perPage: 3 }; - const responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(3); - expect(responses.totalCount).toBe(4); - }); - - test('Count customers', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - // Creating test data - await segmentFactory({ contentType: 'customer' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'bySegment', - }); - - expect(count(response.bySegment)).toBe(1); - }); - - test('Customer count by tag', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - await tagsFactory({ type: 'company' }); - await tagsFactory({ type: 'customer' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byTag', - }); - - expect(count(response.byTag)).toBe(1); - }); - - test('Customer count by segment', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - - await segmentFactory({ contentType: 'customer' }); - await segmentFactory({ contentType: 'company' }); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'bySegment', - }); - - expect(count(response.bySegment)).toBe(1); - }); - - test('Customer count by fake segment', async () => { - await customerFactory({ lastName }, true); - - const byFakeSegment = { - contentType: 'customer', - conditions: [ - { - field: 'lastName', - operator: 'c', - value: lastName, - type: 'string', - }, - ], - }; - - const response = await graphqlRequest(qryCount, 'customerCounts', { - byFakeSegment, - }); - - expect(response.byFakeSegment).toBe(1); - }); - - test('Customer count by leadStatus', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ leadStatus: 'new' }, true); - await customerFactory({ leadStatus: 'new' }, true); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byLeadStatus', - }); - - expect(response.byLeadStatus.open).toBe(2); - expect(response.byLeadStatus.new).toBe(2); - }); - - test('Customer count by lifecycleState', async () => { - await customerFactory({}, true); - await customerFactory({}, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - await customerFactory({ lifecycleState: 'subscriber' }, true); - - const response = await graphqlRequest(qryCount, 'customerCounts', { - only: 'byLifecycleState', - }); - - expect(response.byLifecycleState.subscriber).toBe(2); - expect(response.byLifecycleState.lead).toBe(2); - }); - - test('Customer detail', async () => { - const customer = await customerFactory({}, true); - - const qry = ` - query customerDetail($_id: String!) { - customerDetail(_id: $_id) { - _id - } - } - `; - - const response = await graphqlRequest(qry, 'customerDetail', { - _id: customer._id, - }); - - expect(response._id).toBe(customer._id); - }); - - test('Customer filtered by submitted form', async () => { - const customer = await customerFactory({}, true); - let submissions = [{ customerId: customer._id, submittedAt: new Date() }]; - const form = await formFactory({ submissions }); - - const testCustomer = await customerFactory({}, true); - - submissions = [ - { customerId: testCustomer._id, submittedAt: new Date() }, - { customerId: customer._id, submittedAt: new Date() }, - ]; - - const testForm = await formFactory({ submissions }); - - let responses = await graphqlRequest(qryCustomersMain, 'customersMain', { - form: form._id, - }); - - expect(responses.list.length).toBe(1); - - responses = await graphqlRequest(qryCustomersMain, 'customersMain', { - form: testForm._id, - }); - - expect(responses.list.length).toBe(2); - }); - - test('Customer filtered by submitted form with startDate and endDate', async () => { - const customer = await customerFactory({}, true); - const customer1 = await customerFactory({}, true); - const customer2 = await customerFactory({}, true); - - const startDate = '2018-04-03 10:00'; - const endDate = '2018-04-03 18:00'; - - // Creating 3 submissions for form - const submissions = [ - { - customerId: customer._id, - submittedAt: moment(startDate) - .add(5, 'days') - .toDate(), - }, - { - customerId: customer1._id, - submittedAt: moment(startDate) - .add(20, 'days') - .toDate(), - }, - { - customerId: customer2._id, - submittedAt: moment(startDate) - .add(1, 'hours') - .toDate(), - }, - ]; - - const form = await formFactory({ submissions }); - - let args = { - startDate, - endDate, - form: form._id, - }; - - let responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(1); - - args = { - startDate, - endDate: moment(endDate) - .add(25, 'days') - .format('YYYY-MM-DD HH:mm'), - form: form._id, - }; - - responses = await graphqlRequest(qryCustomersMain, 'customersMain', args); - - expect(responses.list.length).toBe(3); - }); - - test('Customer filtered by default selector', async () => { - const integration = await integrationFactory({}); - await Customers.createCustomer({ integrationId: integration._id }); - await customerFactory({}, true); - await customerFactory({}, true); - - const responses = await graphqlRequest(qryCustomersMain, 'customersMain', {}); - - expect(responses.list.length).toBe(2); - }); -}); +import * as moment from 'moment'; +import * as sinon from 'sinon'; +import { graphqlRequest } from '../db/connection'; +import { + brandFactory, + customerFactory, + formFactory, + formSubmissionFactory, + integrationFactory, + segmentFactory, + tagsFactory, +} from '../db/factories'; +import { Customers, FormSubmissions, Segments, Tags } from '../db/models'; +import * as elk from '../elasticsearch'; + +import './setup.ts'; + +describe('customerQueries', () => { + const commonParamDefs = ` + $page: Int, + $perPage: Int, + $segment: String, + $tag: String, + $ids: [String], + $searchValue: String, + $form: String, + $startDate: String, + $endDate: String, + $leadStatus: String + `; + + const commonParams = ` + page: $page + perPage: $perPage + segment: $segment + tag: $tag + ids: $ids + searchValue: $searchValue + form: $form + startDate: $startDate + endDate: $endDate + leadStatus: $leadStatus + `; + + const qryCustomers = ` + query customers(${commonParamDefs}) { + customers(${commonParams}) { + _id + } + } + `; + + const qryCustomersMain = ` + query customersMain(${commonParamDefs}) { + customersMain(${commonParams}) { + list { + _id + firstName + lastName + primaryEmail + primaryPhone + tagIds + } + totalCount + } + } + `; + + const qryCount = ` + query customerCounts(${commonParamDefs} $only: String) { + customerCounts(${commonParams} only: $only) + } + `; + + afterEach(async () => { + // Clearing test data + await Customers.deleteMany({}); + await Segments.deleteMany({}); + await Tags.deleteMany({}); + await FormSubmissions.deleteMany({}); + }); + + test('Customers', async () => { + await graphqlRequest(qryCustomers, 'customers', {}); + }); + + test('Customers filtered by ids', async () => { + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + const customer3 = await customerFactory({}, true); + + await customerFactory({}, true); + await customerFactory({}, true); + await customerFactory({}, true); + + const ids = [customer1._id, customer2._id, customer3._id]; + + await graphqlRequest(qryCustomers, 'customers', { ids }); + }); + + test('Main customers', async () => { + await graphqlRequest(qryCustomersMain, 'customersMain', {}); + }); + + test('Count customers', async () => { + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + const customer3 = await customerFactory({}, true); + + // Creating test data + await segmentFactory({ contentType: 'customer' }); + + const args = { only: 'bySegment', ids: [customer1._id, customer2._id, customer3._id] }; + + await graphqlRequest(qryCount, 'customerCounts', args); + }); + + test('Count customers by segment', async () => { + // Creating test data + await segmentFactory({ contentType: 'customer' }); + + const args = { only: 'bySegment' }; + + await graphqlRequest(qryCount, 'customerCounts', args); + }); + + test('Count customers by brand', async () => { + await brandFactory({}); + + await graphqlRequest(qryCount, 'customerCounts', { + only: 'byBrand', + }); + }); + + test('Customer count by tag', async () => { + await tagsFactory({ type: 'company' }); + await tagsFactory({ type: 'customer' }); + + await graphqlRequest(qryCount, 'customerCounts', { + only: 'byTag', + }); + }); + + test('Customer count by form', async () => { + await formFactory({}); + + await graphqlRequest(qryCount, 'customerCounts', { + only: 'byForm', + }); + }); + + test('Customer count by leadStatus', async () => { + await graphqlRequest(qryCount, 'customerCounts', { + only: 'byLeadStatus', + }); + }); + + test('Customer count by IntegrationType', async () => { + await integrationFactory({ kind: '' }); + + await graphqlRequest(qryCount, 'customerCounts', { + only: 'byIntegrationType', + }); + }); + + test('Customer filtered by submitted form', async () => { + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + + const form = await formFactory({}); + + await formSubmissionFactory({ customerId: customer1._id, formId: form._id }); + await formSubmissionFactory({ customerId: customer2._id, formId: form._id }); + + await graphqlRequest(qryCustomersMain, 'customersMain', { + form: form._id, + }); + }); + + test('Customer filtered by submitted form with startDate and endDate', async () => { + const customer = await customerFactory({}, true); + const customer1 = await customerFactory({}, true); + const customer2 = await customerFactory({}, true); + + const startDate = moment().format('YYYY-MM-DD HH:mm'); + const endDate = moment(startDate) + .add(25, 'days') + .format('YYYY-MM-DD HH:mm'); + + const form = await formFactory(); + + // Creating 3 submissions for form + await formSubmissionFactory({ customerId: customer._id, formId: form._id }); + await formSubmissionFactory({ customerId: customer1._id, formId: form._id }); + await formSubmissionFactory({ customerId: customer2._id, formId: form._id }); + + const args = { + startDate, + endDate, + form: form._id, + }; + + await graphqlRequest(qryCustomersMain, 'customersMain', args); + }); + + test('Customer filtered by default selector', async () => { + await integrationFactory({}); + + await graphqlRequest(qryCustomersMain, 'customersMain', {}); + }); + + test('Customer detail', async () => { + const customer = await customerFactory({ trackedData: { t1: 'v1' } }, true); + + const mock = sinon.stub(elk, 'fetchElk').callsFake(() => { + return Promise.resolve({ + hits: { hits: [{ _source: { count: 1, attributes: [{ url: '/test' }] } }] }, + }); + }); + + const qry = ` + query customerDetail($_id: String!) { + customerDetail(_id: $_id) { + _id + createdAt + modifiedAt + integrationId + firstName + lastName + primaryEmail + emails + primaryPhone + phones + tagIds + remoteAddress + internalNotes + location + visitorContactInfo + customFieldsData + trackedData + ownerId + position + department + leadStatus + hasAuthority + description + doNotDisturb + links + urlVisits + conversations { _id } + getTags { _id } + owner { _id } + integration { _id } + companies { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'customerDetail', { + _id: customer._id, + }); + + mock.restore(); + + expect(response._id).toBe(customer._id); + }); +}); diff --git a/src/__tests__/dealDb.test.ts b/src/__tests__/dealDb.test.ts index 615563d7e..0e27ccc4b 100644 --- a/src/__tests__/dealDb.test.ts +++ b/src/__tests__/dealDb.test.ts @@ -1,15 +1,17 @@ import { boardFactory, - companyFactory, - customerFactory, + conversationFactory, dealFactory, pipelineFactory, + pipelineLabelFactory, stageFactory, userFactory, } from '../db/factories'; import { Boards, Deals, Pipelines, Stages } from '../db/models'; +import { getItem } from '../db/models/boardUtils'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; import { IDealDocument } from '../db/models/definitions/deals'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; import { IUserDocument } from '../db/models/definitions/users'; import './setup.ts'; @@ -20,14 +22,26 @@ describe('Test deals model', () => { let stage: IStageDocument; let deal: IDealDocument; let user: IUserDocument; + let label: IPipelineLabelDocument; + let secondUser: IUserDocument; beforeEach(async () => { // Creating test data board = await boardFactory(); pipeline = await pipelineFactory({ boardId: board._id }); stage = await stageFactory({ pipelineId: pipeline._id }); - deal = await dealFactory({ stageId: stage._id }); user = await userFactory({}); + secondUser = await userFactory({}); + label = await pipelineLabelFactory({}); + deal = await dealFactory({ + initialStageId: stage._id, + stageId: stage._id, + userId: user._id, + modifiedBy: user._id, + labelIds: [label._id], + assignedUserIds: [user._id], + watchedUserIds: [secondUser._id], + }); }); afterEach(async () => { @@ -38,102 +52,95 @@ describe('Test deals model', () => { await Deals.deleteMany({}); }); - // Test deal - test('Create deal', async () => { - const createdDeal = await Deals.createDeal({ - stageId: deal.stageId, - userId: user._id, - }); + test('Get deal', async () => { + try { + await Deals.getDeal('fakeId'); + } catch (e) { + expect(e.message).toBe('Deal not found'); + } - expect(createdDeal).toBeDefined(); - expect(createdDeal.stageId).toEqual(stage._id); - expect(createdDeal.createdAt).toEqual(deal.createdAt); - expect(createdDeal.userId).toEqual(user._id); + const response = await Deals.getDeal(deal._id); + + expect(response).toBeDefined(); }); - test('Update deal', async () => { - const dealStageId = 'fakeId'; - const updatedDeal = await Deals.updateDeal(deal._id, { - stageId: dealStageId, - }); + test('Get item on deal', async () => { + try { + await getItem('deal', 'fakeId'); + } catch (e) { + expect(e.message).toBe('deal not found'); + } - expect(updatedDeal).toBeDefined(); - expect(updatedDeal.stageId).toEqual(dealStageId); - expect(updatedDeal.closeDate).toEqual(deal.closeDate); + const response = await getItem('deal', deal._id); + + expect(response).toBeDefined(); }); - test('Update deal orders', async () => { - const dealToOrder = await dealFactory({}); + test('Create deal', async () => { + const args = { + stageId: deal.stageId, + userId: user._id, + }; - const [updatedDeal, updatedDealToOrder] = await Deals.updateOrder(stage._id, [ - { _id: deal._id, order: 9 }, - { _id: dealToOrder._id, order: 3 }, - ]); + const createdDeal = await Deals.createDeal(args); - expect(updatedDeal.stageId).toBe(stage._id); - expect(updatedDeal.order).toBe(3); - expect(updatedDealToOrder.order).toBe(9); + expect(createdDeal).toBeDefined(); + expect(createdDeal.stageId).toEqual(stage._id); + expect(createdDeal.userId).toEqual(user._id); }); - test('Remove deal', async () => { - const isDeleted = await Deals.removeDeal(deal.id); + test('Create deal Error(`Already converted a deal`)', async () => { + const conversation = await conversationFactory(); - expect(isDeleted).toBeTruthy(); - }); + const args = { + stageId: deal.stageId, + userId: user._id, + sourceConversationId: conversation._id, + }; - test('Remove deal not found', async () => { - expect.assertions(1); + const createdDeal = await Deals.createDeal(args); - const fakeDealId = 'fakeDealId'; + expect(createdDeal).toBeDefined(); + // Already converted a deal try { - await Deals.removeDeal(fakeDealId); + await Deals.createDeal(args); } catch (e) { - expect(e.message).toEqual('Deal not found'); + expect(e.message).toBe('Already converted a deal'); } }); - test('Deal change customer', async () => { - const newCustomer = await customerFactory({}); - - const customer1 = await customerFactory({}); - const customer2 = await customerFactory({}); - const dealObj = await dealFactory({ - customerIds: [customer2._id, customer1._id], + test('Update deal', async () => { + const dealStageId = 'fakeId'; + const updatedDeal = await Deals.updateDeal(deal._id, { + stageId: dealStageId, }); - await Deals.changeCustomer(newCustomer._id, [customer2._id, customer1._id]); + expect(updatedDeal).toBeDefined(); + expect(updatedDeal.stageId).toEqual(dealStageId); + expect(updatedDeal.closeDate).toEqual(deal.closeDate); + }); - const result = await Deals.findOne({ _id: dealObj._id }); + test('Watch deal', async () => { + await Deals.watchDeal(deal._id, true, user._id); - if (!result) { - throw new Error('Deal not found'); - } + const watchedDeal = await Deals.getDeal(deal._id); - expect(result.customerIds).toContain(newCustomer._id); - expect(result.customerIds).not.toContain(customer1._id); - expect(result.customerIds).not.toContain(customer2._id); - }); + expect(watchedDeal.watchedUserIds).toContain(user._id); - test('Deal change company', async () => { - const newCompany = await companyFactory({}); + // testing unwatch + await Deals.watchDeal(deal._id, false, user._id); - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - const dealObj = await dealFactory({ - companyIds: [company1._id, company2._id], - }); + const unwatchedDeal = await Deals.getDeal(deal._id); - await Deals.changeCompany(newCompany._id, [company1._id, company2._id]); + expect(unwatchedDeal.watchedUserIds).not.toContain(user._id); + }); - const result = await Deals.findOne({ _id: dealObj._id }); + test('Test removeDeals()', async () => { + await Deals.removeDeals([deal._id]); - if (!result) { - throw new Error('Deal not found'); - } + const removed = await Deals.findOne({ _id: deal._id }); - expect(result.companyIds).toContain(newCompany._id); - expect(result.companyIds).not.toContain(company1._id); - expect(result.companyIds).not.toContain(company2._id); + expect(removed).toBe(null); }); }); diff --git a/src/__tests__/dealMutations.test.ts b/src/__tests__/dealMutations.test.ts index 64e71d588..83b4c4e43 100644 --- a/src/__tests__/dealMutations.test.ts +++ b/src/__tests__/dealMutations.test.ts @@ -1,9 +1,34 @@ import { graphqlRequest } from '../db/connection'; -import { boardFactory, dealFactory, pipelineFactory, stageFactory, userFactory } from '../db/factories'; -import { Boards, Deals, Pipelines, Stages } from '../db/models'; +import { + boardFactory, + checklistFactory, + checklistItemFactory, + companyFactory, + conformityFactory, + customerFactory, + dealFactory, + pipelineFactory, + pipelineLabelFactory, + productFactory, + stageFactory, + userFactory, +} from '../db/factories'; +import { + Boards, + ChecklistItems, + Checklists, + Conformities, + Deals, + PipelineLabels, + Pipelines, + Products, + Stages, +} from '../db/models'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; -import { BOARD_TYPES } from '../db/models/definitions/constants'; -import { IDealDocument } from '../db/models/definitions/deals'; +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; +import { IDealDocument, IProductDocument } from '../db/models/definitions/deals'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; +import { IUserDocument } from '../db/models/definitions/users'; import './setup.ts'; @@ -12,25 +37,57 @@ describe('Test deals mutations', () => { let pipeline: IPipelineDocument; let stage: IStageDocument; let deal: IDealDocument; - let context; + let label: IPipelineLabelDocument; + let product: IProductDocument; + let user: IUserDocument; const commonDealParamDefs = ` - $name: String!, + $name: String! $stageId: String! + $assignedUserIds: [String] + $productsData: JSON + $status: String `; const commonDealParams = ` name: $name stageId: $stageId + assignedUserIds: $assignedUserIds + productsData: $productsData + status: $status + `; + + const commonDragParamDefs = ` + $itemId: String!, + $aboveItemId: String, + $destinationStageId: String!, + $sourceStageId: String, + $proccessId: String + `; + + const commonDragParams = ` + itemId: $itemId, + aboveItemId: $aboveItemId, + destinationStageId: $destinationStageId, + sourceStageId: $sourceStageId, + proccessId: $proccessId `; beforeEach(async () => { // Creating test data + user = await userFactory(); + board = await boardFactory({ type: BOARD_TYPES.DEAL }); - pipeline = await pipelineFactory({ boardId: board._id }); + pipeline = await pipelineFactory({ boardId: board._id, watchedUserIds: [user._id] }); stage = await stageFactory({ pipelineId: pipeline._id }); - deal = await dealFactory({ stageId: stage._id }); - context = { user: await userFactory({}) }; + label = await pipelineLabelFactory({ pipelineId: pipeline._id }); + product = await productFactory(); + deal = await dealFactory({ + initialStageId: stage._id, + stageId: stage._id, + labelIds: [label._id], + productsData: [{ productId: product._id }], + }); }); afterEach(async () => { @@ -39,17 +96,21 @@ describe('Test deals mutations', () => { await Pipelines.deleteMany({}); await Stages.deleteMany({}); await Deals.deleteMany({}); + await PipelineLabels.deleteMany({}); + await Products.deleteMany({}); }); test('Create deal', async () => { const args = { name: deal.name, stageId: stage._id, + customerIds: ['fakeCustomerId'], + companyIds: ['fakeCompanyId'], }; const mutation = ` - mutation dealsAdd(${commonDealParamDefs}) { - dealsAdd(${commonDealParams}) { + mutation dealsAdd(${commonDealParamDefs} $customerIds: [String] $companyIds: [String]) { + dealsAdd(${commonDealParams} customerIds: $customerIds companyIds: $companyIds) { _id name stageId @@ -57,76 +118,147 @@ describe('Test deals mutations', () => { } `; - const createdDeal = await graphqlRequest(mutation, 'dealsAdd', args, context); + const createdDeal = await graphqlRequest(mutation, 'dealsAdd', args); expect(createdDeal.stageId).toEqual(stage._id); }); test('Update deal', async () => { - const args = { - _id: deal._id, - name: deal.name, - stageId: stage._id, - }; - const mutation = ` mutation dealsEdit($_id: String!, ${commonDealParamDefs}) { dealsEdit(_id: $_id, ${commonDealParams}) { _id name stageId + assignedUserIds } } `; - const updatedDeal = await graphqlRequest(mutation, 'dealsEdit', args, context); + const product2 = await productFactory(); + + const args: any = { + _id: deal._id, + name: deal.name, + stageId: stage._id, + productsData: [{ productId: product2._id }, { productId: product._id }], + }; + + let response = await graphqlRequest(mutation, 'dealsEdit', args); + + expect(response.stageId).toEqual(stage._id); + + // if assignedUserIds is not empty + const user1 = await userFactory(); + args.assignedUserIds = [user1._id]; + + response = await graphqlRequest(mutation, 'dealsEdit', args); - expect(updatedDeal.stageId).toEqual(stage._id); + expect(response.assignedUserIds).toContain(user1._id); + + // if assigned productsData + const user2 = await userFactory(); + args.productsData.push({ productId: product2._id, assignUserId: user2._id }); + + response = await graphqlRequest(mutation, 'dealsEdit', args); + + expect(response.assignedUserIds).toContain(user2._id); + + // if assigned productsData unassign assignedUserIds + delete args.productsData; + try { + response = await graphqlRequest(mutation, 'dealsEdit', args); + } catch (e) { + expect(e).toBeDefined(); + } + + // not products data and assigneduserIDs + args.productsData = []; + args.status = 'archived'; + + delete args.assignedUserIds; + response = await graphqlRequest(mutation, 'dealsEdit', args); + + expect(response.assignedUserIds).toEqual([user1._id]); }); test('Change deal', async () => { const args = { - _id: deal._id, - destinationStageId: deal.stageId || '', + proccessId: Math.random().toString(), + itemId: deal._id, + aboveItemId: '', + destinationStageId: deal.stageId, + sourceStageId: deal.stageId, }; const mutation = ` - mutation dealsChange($_id: String!, $destinationStageId: String) { - dealsChange(_id: $_id, destinationStageId: $destinationStageId) { - _id, + mutation dealsChange(${commonDragParamDefs}) { + dealsChange(${commonDragParams}) { + _id + name stageId + order } } `; - const updatedDeal = await graphqlRequest(mutation, 'dealsChange', args, context); + const updatedDeal = await graphqlRequest(mutation, 'dealsChange', args); - expect(updatedDeal._id).toEqual(args._id); + expect(updatedDeal._id).toEqual(args.itemId); }); - test('Deal update orders', async () => { - const dealToStage = await dealFactory({}); + test('Change deal if move to another stage', async () => { + const anotherStage = await stageFactory({ pipelineId: pipeline._id }); const args = { - orders: [{ _id: deal._id, order: 9 }, { _id: dealToStage._id, order: 3 }], - stageId: stage._id, + proccessId: Math.random().toString(), + itemId: deal._id, + aboveItemId: '', + destinationStageId: anotherStage._id, + sourceStageId: deal.stageId, }; const mutation = ` - mutation dealsUpdateOrder($stageId: String!, $orders: [OrderItem]) { - dealsUpdateOrder(stageId: $stageId, orders: $orders) { + mutation dealsChange(${commonDragParamDefs}) { + dealsChange(${commonDragParams}) { _id + name stageId order } } `; - const [updatedDeal, updatedDealToOrder] = await graphqlRequest(mutation, 'dealsUpdateOrder', args, context); + const updatedDeal = await graphqlRequest(mutation, 'dealsChange', args); + + expect(updatedDeal._id).toEqual(args.itemId); + }); + + test('Update deal move to pipeline stage', async () => { + const mutation = ` + mutation dealsEdit($_id: String!, ${commonDealParamDefs}) { + dealsEdit(_id: $_id, ${commonDealParams}) { + _id + name + stageId + assignedUserIds + } + } + `; + + const anotherPipeline = await pipelineFactory({ boardId: board._id }); + const anotherStage = await stageFactory({ pipelineId: anotherPipeline._id }); + + const args = { + _id: deal._id, + stageId: anotherStage._id, + name: deal.name || '', + }; + + const updatedDeal = await graphqlRequest(mutation, 'dealsEdit', args); - expect(updatedDeal.order).toBe(3); - expect(updatedDealToOrder.order).toBe(9); - expect(updatedDeal.stageId).toBe(stage._id); + expect(updatedDeal._id).toEqual(args._id); + expect(updatedDeal.stageId).toEqual(args.stageId); }); test('Remove deal', async () => { @@ -138,7 +270,7 @@ describe('Test deals mutations', () => { } `; - await graphqlRequest(mutation, 'dealsRemove', { _id: deal._id }, context); + await graphqlRequest(mutation, 'dealsRemove', { _id: deal._id }); expect(await Deals.findOne({ _id: deal._id })).toBe(null); }); @@ -153,12 +285,94 @@ describe('Test deals mutations', () => { } `; - const watchAddDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: true }, context); + const watchAddDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: true }); expect(watchAddDeal.isWatched).toBe(true); - const watchRemoveDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: false }, context); + const watchRemoveDeal = await graphqlRequest(mutation, 'dealsWatch', { _id: deal._id, isAdd: false }); expect(watchRemoveDeal.isWatched).toBe(false); }); + + test('Test dealsCopy()', async () => { + const mutation = ` + mutation dealsCopy($_id: String!) { + dealsCopy(_id: $_id) { + _id + userId + name + stageId + } + } + `; + + const checklist = await checklistFactory({ + contentType: 'deal', + contentTypeId: deal._id, + title: 'deal-checklist', + }); + + await checklistItemFactory({ + checklistId: checklist._id, + content: 'Improve deal mutation test coverage', + isChecked: true, + }); + + const company = await companyFactory(); + const customer = await customerFactory(); + + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal._id, + relType: 'company', + relTypeId: company._id, + }); + + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal._id, + relType: 'customer', + relTypeId: customer._id, + }); + + const result = await graphqlRequest(mutation, 'dealsCopy', { _id: deal._id }, { user }); + + const clonedDealCompanies = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedDealCustomers = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedDealChecklist = await Checklists.findOne({ contentTypeId: result._id }); + + if (clonedDealChecklist) { + const clonedDealChecklistItems = await ChecklistItems.find({ checklistId: clonedDealChecklist._id }); + + expect(clonedDealChecklist.contentTypeId).toBe(result._id); + expect(clonedDealChecklistItems.length).toBe(1); + } + + expect(result.userId).toBe(user._id); + expect(result.name).toBe(`${deal.name}-copied`); + expect(result.stageId).toBe(deal.stageId); + + expect(clonedDealCompanies.length).toBe(1); + expect(clonedDealCustomers.length).toBe(1); + }); + + test('Test archive', async () => { + const mutation = ` + mutation dealsArchive($stageId: String!) { + dealsArchive(stageId: $stageId) + } + `; + + const dealStage = await stageFactory({ type: BOARD_TYPES.DEAL }); + + await dealFactory({ stageId: dealStage._id }); + await dealFactory({ stageId: dealStage._id }); + await dealFactory({ stageId: dealStage._id }); + + await graphqlRequest(mutation, 'dealsArchive', { stageId: dealStage._id }); + + const deals = await Deals.find({ stageId: dealStage._id, status: BOARD_STATUSES.ARCHIVED }); + + expect(deals.length).toBe(3); + }); }); diff --git a/src/__tests__/dealQueries.test.ts b/src/__tests__/dealQueries.test.ts index 19a2f2b1f..09ca0c643 100644 --- a/src/__tests__/dealQueries.test.ts +++ b/src/__tests__/dealQueries.test.ts @@ -1,76 +1,113 @@ import * as moment from 'moment'; import { graphqlRequest } from '../db/connection'; import { + boardFactory, companyFactory, + conformityFactory, customerFactory, dealFactory, + fieldFactory, + pipelineFactory, + pipelineLabelFactory, productFactory, stageFactory, userFactory, } from '../db/factories'; -import { Deals } from '../db/models'; +import { Boards, Deals, Pipelines, Stages } from '../db/models'; +import { BOARD_STATUSES } from '../db/models/definitions/constants'; import './setup.ts'; describe('dealQueries', () => { - const commonDealTypes = ` + const commonDealFields = ` _id name stageId - companyIds - customerIds assignedUserIds amount closeDate description - companies { - _id - } - customers { - _id - } + companies { _id } + customers { _id } products productsData - assignedUsers { - _id - } + assignedUsers { _id } + labels { _id } + hasNotified + isWatched + stage { _id } + boardId + pipeline { _id } + userId + createdUser { _id } `; const qryDealFilter = ` query deals( - $stageId: String + $search: String + $stageId: String + $pipelineId: String $assignedUserIds: [String] $customerIds: [String] $companyIds: [String] $productIds: [String] - $nextDay: String - $nextWeek: String - $nextMonth: String - $noCloseDate: String - $overdue: String + $closeDateType: String + $mainType: String + $mainTypeId: String + $isRelated: Boolean + $isSaved: Boolean + $date: ItemDate + $labelIds: [String] + $initialStageId: String + $userIds: [String] ) { deals( - stageId: $stageId + search: $search + stageId: $stageId + pipelineId: $pipelineId customerIds: $customerIds assignedUserIds: $assignedUserIds companyIds: $companyIds productIds: $productIds - nextDay: $nextDay - nextWeek: $nextWeek - nextMonth: $nextMonth - noCloseDate: $noCloseDate - overdue: $overdue + closeDateType: $closeDateType + conformityMainType: $mainType + conformityMainTypeId: $mainTypeId + conformityIsRelated: $isRelated + conformityIsSaved: $isSaved + date: $date + labelIds: $labelIds + initialStageId: $initialStageId + userIds: $userIds ) { - ${commonDealTypes} + ${commonDealFields} } } `; afterEach(async () => { // Clearing test data + await Boards.deleteMany({}); + await Pipelines.deleteMany({}); + await Stages.deleteMany({}); await Deals.deleteMany({}); }); + test('Filter by initialStageId', async () => { + const deal = await dealFactory(); + + const response = await graphqlRequest(qryDealFilter, 'deals', { initialStageId: deal.stageId }); + + expect(response.length).toBe(1); + }); + + test('Filter by search', async () => { + await dealFactory({ searchText: 'name' }); + + const response = await graphqlRequest(qryDealFilter, 'deals', { search: 'name' }); + + expect(response.length).toBe(1); + }); + test('Filter by next day', async () => { const tomorrow = moment() .add(1, 'day') @@ -79,7 +116,7 @@ describe('dealQueries', () => { await dealFactory({ closeDate: new Date(tomorrow) }); - const response = await graphqlRequest(qryDealFilter, 'deals', { nextDay: 'true' }); + const response = await graphqlRequest(qryDealFilter, 'deals', { closeDateType: 'nextDay' }); expect(response.length).toBe(1); }); @@ -91,7 +128,7 @@ describe('dealQueries', () => { await dealFactory({ closeDate: new Date(nextWeek) }); - const response = await graphqlRequest(qryDealFilter, 'deals', { nextWeek: 'true' }); + const response = await graphqlRequest(qryDealFilter, 'deals', { closeDateType: 'nextWeek' }); expect(response.length).toBe(1); }); @@ -103,7 +140,7 @@ describe('dealQueries', () => { await dealFactory({ closeDate: new Date(nextMonth) }); - const response = await graphqlRequest(qryDealFilter, 'deals', { nextMonth: 'true' }); + const response = await graphqlRequest(qryDealFilter, 'deals', { closeDateType: 'nextMonth' }); expect(response.length).toBe(1); }); @@ -111,7 +148,7 @@ describe('dealQueries', () => { test('Deal filter by has no close date', async () => { await dealFactory({ noCloseDate: true }); - const response = await graphqlRequest(qryDealFilter, 'deals', { noCloseDate: 'true' }); + const response = await graphqlRequest(qryDealFilter, 'deals', { closeDateType: 'noCloseDate' }); expect(response.length).toBe(1); }); @@ -124,15 +161,44 @@ describe('dealQueries', () => { await dealFactory({ closeDate: yesterday }); - const response = await graphqlRequest(qryDealFilter, 'deals', { overdue: 'true' }); + const response = await graphqlRequest(qryDealFilter, 'deals', { closeDateType: 'overdue' }); expect(response.length).toBe(1); }); test('Deal filter by products', async () => { - const { productId } = await productFactory(); + const field1 = await fieldFactory({ contentType: 'product' }); - await dealFactory({ productsData: { productId } }); + if (!field1) { + throw new Error('Field not found'); + } + + const customFieldsData = [{ field: field1._id, value: 'text' }]; + + const product = await productFactory({ customFieldsData }); + const productNoCustomData = await productFactory(); + const productId = product._id; + + const productsData = [ + { + productId: product._id, + currency: 'USD', + amount: 200, + tickUsed: true, + }, + { + productId: product._id, + currency: 'USD', + }, + { + productId: productNoCustomData._id, + }, + { + productId: undefined, + }, + ]; + + await dealFactory({ productsData }); const response = await graphqlRequest(qryDealFilter, 'deals', { productIds: [productId] }); @@ -144,68 +210,466 @@ describe('dealQueries', () => { await dealFactory({ assignedUserIds: [_id] }); - const response = await graphqlRequest(qryDealFilter, 'deals', { assignedUserIds: [_id] }); + let response = await graphqlRequest(qryDealFilter, 'deals', { assignedUserIds: [_id] }); expect(response.length).toBe(1); + + await dealFactory(); + + // Filter by assigned to no one + response = await graphqlRequest(qryDealFilter, 'deals', { assignedUserIds: [''] }); + + expect(response.length).toBe(0); }); test('Deal filter by customers', async () => { const { _id } = await customerFactory(); + const deal = await dealFactory(); - await dealFactory({ customerIds: [_id] }); + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal._id, + relType: 'customer', + relTypeId: _id, + }); - const response = await graphqlRequest(qryDealFilter, 'deals', { customerIds: [_id] }); + let response = await graphqlRequest(qryDealFilter, 'deals', { customerIds: [_id] }); expect(response.length).toBe(1); + + const customer1 = await customerFactory(); + + response = await graphqlRequest(qryDealFilter, 'deals', { customerIds: [customer1._id] }); + + expect(response.length).toBe(0); }); test('Deal filter by companies', async () => { const { _id } = await companyFactory(); - await dealFactory({ companyIds: [_id] }); + const deal = await dealFactory(); + + await conformityFactory({ + mainType: 'company', + mainTypeId: _id, + relType: 'deal', + relTypeId: deal._id, + }); - const response = await graphqlRequest(qryDealFilter, 'deals', { companyIds: [_id] }); + let response = await graphqlRequest(qryDealFilter, 'deals', { companyIds: [_id] }); expect(response.length).toBe(1); + + const company1 = await companyFactory(); + + response = await graphqlRequest(qryDealFilter, 'deals', { companyIds: [company1._id] }); + + expect(response.length).toBe(0); + }); + + test('Deal filter by label', async () => { + const { _id } = await pipelineLabelFactory(); + + await dealFactory({ labelIds: [_id] }); + + let response = await graphqlRequest(qryDealFilter, 'deals', { labelIds: [_id] }); + + expect(response.length).toBe(1); + + // filtering nolabelled deals + await dealFactory(); + + response = await graphqlRequest(qryDealFilter, 'deals', { labelIds: [''] }); + + expect(response.length).toBe(1); + }); + + test('Deal filter by date', async () => { + const board = await boardFactory(); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + const date = new Date(); + await dealFactory({ closeDate: date, stageId: stage._id }); + + const args = { + date: { year: date.getUTCFullYear(), month: date.getUTCMonth() }, + pipelineId: pipeline._id, + }; + + const response = await graphqlRequest(qryDealFilter, 'deals', args); + + expect(response.length).toBe(1); + }); + + test('Deals filtered by created user', async () => { + const board = await boardFactory(); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + const user = await userFactory(); + + const dealParams = { userId: user._id, stageId: stage._id }; + + await dealFactory(dealParams); + await dealFactory(dealParams); + await dealFactory(dealParams); + + const response = await graphqlRequest(qryDealFilter, 'deals', { userIds: [user._id] }); + + expect(response.length).toBe(3); }); test('Deals', async () => { - const stage = await stageFactory(); + const board = await boardFactory(); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const currentUser = await userFactory({}); const args = { stageId: stage._id }; + const deal = await dealFactory({ ...args, name: 'b' }); + await dealFactory({ ...args, name: 'c' }); + await dealFactory({ ...args, name: 'a' }); - await dealFactory(args); - await dealFactory(args); - await dealFactory(args); - + Object.assign(args, { pipelineId: stage.pipelineId }); const qry = ` - query deals($stageId: String!) { - deals(stageId: $stageId) { - ${commonDealTypes} + query deals($stageId: String!, $pipelineId: String, $sortField: String, $sortDirection: Int) { + deals(stageId: $stageId, pipelineId: $pipelineId, sortField: $sortField, sortDirection: $sortDirection) { + ${commonDealFields} } } `; - const response = await graphqlRequest(qry, 'deals', args); + let response = await graphqlRequest(qry, 'deals', args, { user: currentUser }); expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'deals', { ...args, sortField: 'name', sortDirection: 1 }); + + expect(response[0].name).toBe('a'); + expect(response[1].name).toBe('b'); + expect(response[2].name).toBe('c'); + + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { isCheckUser: true } }); + + response = await graphqlRequest(qry, 'deals', args, { user: currentUser }); + + expect(response.length).toBe(0); + + await Deals.updateOne({ _id: deal._id }, { $set: { assignedUserIds: [currentUser._id] } }); + + response = await graphqlRequest(qry, 'deals', args, { user: currentUser }); + + expect(response.length).toBe(1); }); test('Deal detail', async () => { - const deal = await dealFactory(); + const currentUser = await userFactory({}); + const board = await boardFactory(); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const deal = await dealFactory({ stageId: stage._id, watchedUserIds: [currentUser._id] }); const args = { _id: deal._id }; const qry = ` query dealDetail($_id: String!) { dealDetail(_id: $_id) { - ${commonDealTypes} + ${commonDealFields} } } `; - const response = await graphqlRequest(qry, 'dealDetail', args); + let response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); + expect(response._id).toBe(deal._id); + + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { visibility: 'private' } }); + try { + response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); + } catch (e) { + expect(e[0].message).toEqual('You do not have permission to view.'); + } + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { memberIds: [currentUser._id] } }); + response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); expect(response._id).toBe(deal._id); + + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { visibility: 'public', isCheckUser: true } }); + try { + response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); + } catch (e) { + expect(e[0].message).toEqual('You do not have permission to view.'); + } + + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { excludeCheckUserIds: [currentUser._id] } }); + response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); + expect(response._id).toBe(deal._id); + + await Pipelines.updateOne({ _id: pipeline._id }, { $set: { excludeCheckUserIds: [], isCheckUser: true } }); + await Deals.updateOne({ _id: deal._id }, { $set: { assignedUserIds: [currentUser._id] } }); + response = await graphqlRequest(qry, 'dealDetail', args, { user: currentUser }); + expect(response._id).toBe(deal._id); + expect(response.isWatched).toBe(true); + }); + + test('Deal total amount', async () => { + const board = await boardFactory(); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + const product = await productFactory(); + const productsData = [ + { + productId: product._id, + currency: 'USD', + amount: 200, + tickUsed: true, + }, + ]; + + const args = { + stageId: stage._id, + productsData, + }; + + await dealFactory(args); + await dealFactory(args); + await dealFactory(args); + + const filter = { pipelineId: pipeline._id }; + + const qry = ` + query dealsTotalAmounts($pipelineId: String) { + dealsTotalAmounts(pipelineId: $pipelineId) { + _id + dealCount + totalForType { + _id + name + currencies { + name + amount + } + } + } + } + `; + + const response = await graphqlRequest(qry, 'dealsTotalAmounts', filter); + + expect(response.dealCount).toBe(3); + expect(response.totalForType[0].currencies[0].name).toBe('USD'); + expect(response.totalForType[0].currencies[0].amount).toBe(600); + }); + + test('Deal (=ticket, task) filter by conformity saved and related', async () => { + const { _id } = await companyFactory(); + + const deal = await dealFactory(); + await dealFactory(); + + await customerFactory({}); + await companyFactory({}); + + let response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'company', + mainTypeId: _id, + isSaved: true, + }); + + expect(response.length).toBe(0); + + response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'company', + mainTypeId: _id, + isRelated: true, + }); + + expect(response.length).toBe(0); + + await conformityFactory({ + mainType: 'company', + mainTypeId: _id, + relType: 'deal', + relTypeId: deal._id, + }); + + const customer = await customerFactory({}); + await conformityFactory({ + mainType: 'company', + mainTypeId: _id, + relType: 'customer', + relTypeId: customer._id, + }); + + response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'company', + mainTypeId: _id, + isSaved: true, + }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'company', + mainTypeId: _id, + isRelated: true, + }); + + expect(response.length).toBe(0); + + response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'customer', + mainTypeId: customer._id, + isSaved: true, + }); + + expect(response.length).toBe(0); + + response = await graphqlRequest(qryDealFilter, 'deals', { + mainType: 'customer', + mainTypeId: customer._id, + isRelated: true, + }); + + expect(response.length).toBe(1); + }); + + test('Deal filter by customers and companies', async () => { + const customer = await customerFactory(); + const company = await companyFactory(); + + const deal = await dealFactory(); + const deal1 = await dealFactory(); + const deal2 = await dealFactory(); + + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal._id, + relType: 'customer', + relTypeId: customer._id, + }); + + await conformityFactory({ + mainType: 'company', + mainTypeId: company._id, + relType: 'deal', + relTypeId: deal._id, + }); + + await conformityFactory({ + mainType: 'deal', + mainTypeId: deal1._id, + relType: 'customer', + relTypeId: customer._id, + }); + + await conformityFactory({ + mainType: 'company', + mainTypeId: company._id, + relType: 'deal', + relTypeId: deal2._id, + }); + + const response = await graphqlRequest(qryDealFilter, 'deals', { + customerIds: [customer._id], + companyIds: [company._id], + }); + + expect(response.length).toBe(1); + }); + + test('Get archived deals', async () => { + const pipeline = await pipelineFactory(); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await dealFactory({ ...args, name: 'james' }); + await dealFactory({ ...args, name: 'jone' }); + await dealFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedDeals( + $pipelineId: String!, + $search: String, + $page: Int, + $perPage: Int + ) { + archivedDeals( + pipelineId: $pipelineId + search: $search + page: $page + perPage: $perPage + ) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'archivedDeals', { + pipelineId: pipeline._id, + }); + + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'archivedDeals', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'archivedDeals', { + pipelineId: 'fakeId', + }); + + expect(response.length).toBe(0); + }); + + test('Get archived deals count', async () => { + const pipeline = await pipelineFactory(); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await dealFactory({ ...args, name: 'james' }); + await dealFactory({ ...args, name: 'jone' }); + await dealFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedDealsCount( + $pipelineId: String!, + $search: String + ) { + archivedDealsCount( + pipelineId: $pipelineId + search: $search + ) + } + `; + + let response = await graphqlRequest(qry, 'archivedDealsCount', { + pipelineId: pipeline._id, + }); + + expect(response).toBe(3); + + response = await graphqlRequest(qry, 'archivedDealsCount', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response).toBe(1); + + response = await graphqlRequest(qry, 'archivedDealsCount', { + pipelineId: 'fakeId', + }); + + expect(response).toBe(0); }); }); diff --git a/src/__tests__/emailDeliveryDb.test.ts b/src/__tests__/emailDeliveryDb.test.ts new file mode 100644 index 000000000..5832d7ccd --- /dev/null +++ b/src/__tests__/emailDeliveryDb.test.ts @@ -0,0 +1,39 @@ +import { emailDeliveryFactory } from '../db/factories'; +import { EmailDeliveries } from '../db/models'; + +import './setup.ts'; + +describe('Test email delivery model', () => { + test('EmailDelivery create email delivery', async () => { + const doc = { + attachments: [], + subject: 'subject', + body: 'body', + to: ['test@gmail.com'], + cc: [''], + bcc: [''], + from: 'AuGpury89dguuzMWK', + kind: 'nylas-gmail', + userId: 'WQ3tsgnDdDu3jhbQj', + customerId: 'oqpF46JorrLRkmpKw', + __v: 0, + }; + + const emailDelivery = await EmailDeliveries.createEmailDelivery(doc); + expect(emailDelivery).toBeDefined(); + expect(emailDelivery.subject).toEqual(doc.subject); + }); + + test('Transaction email delivery update status', async () => { + const emailDelivery = await emailDeliveryFactory({ + kind: 'transaction', + status: 'pending', + }); + + await EmailDeliveries.updateEmailDeliveryStatus(emailDelivery._id, 'received'); + + const newEmailDelivery = await EmailDeliveries.findOne({ _id: emailDelivery._id }).lean(); + + expect(newEmailDelivery.status).toBe('received'); + }); +}); diff --git a/src/__tests__/emailDeliveryQueries.test.ts b/src/__tests__/emailDeliveryQueries.test.ts new file mode 100644 index 000000000..82c4a2737 --- /dev/null +++ b/src/__tests__/emailDeliveryQueries.test.ts @@ -0,0 +1,66 @@ +import { graphqlRequest } from '../db/connection'; +import { emailDeliveryFactory } from '../db/factories'; + +import './setup.ts'; + +describe('Email delivery queries', () => { + test('Transaction email deliveries', async () => { + await emailDeliveryFactory({ + subject: 'subject', + kind: 'transaction', + }); + + const query = ` + query transactionEmailDeliveries($searchValue: String, $page: Int, $perPage: Int) { + transactionEmailDeliveries(searchValue: $searchValue, page: $page, perPage: $perPage) { + list { + _id + } + } + } + `; + + const response = await graphqlRequest(query, 'transactionEmailDeliveries', { searchValue: 'subject' }); + + expect(response.list.length).toBe(1); + }); + + test('Email delivery detail', async () => { + const emailDelivery = await emailDeliveryFactory({}); + + const args = { _id: emailDelivery._id }; + + const qry = ` + query emailDeliveryDetail($_id: String! ) { + emailDeliveryDetail(_id: $_id ) { + _id + subject + body + to + cc + bcc + attachments + from + kind + userId + customerId + createdAt + + fromUser { + _id + details { + avatar + fullName + position + } + } + fromEmail + } + } + `; + + const response = await graphqlRequest(qry, 'emailDeliveryDetail', args); + + expect(response._id).toBe(emailDelivery._id); + }); +}); diff --git a/src/__tests__/emailTemplateDb.test.ts b/src/__tests__/emailTemplateDb.test.ts index f19701936..7a5b6297d 100644 --- a/src/__tests__/emailTemplateDb.test.ts +++ b/src/__tests__/emailTemplateDb.test.ts @@ -16,6 +16,18 @@ describe('Email template db', () => { await EmailTemplates.deleteMany({}); }); + test('Get email template', async () => { + try { + await EmailTemplates.getEmailTemplate('fakeId'); + } catch (e) { + expect(e.message).toBe('Email template not found'); + } + + const response = await EmailTemplates.getEmailTemplate(_emailTemplate._id); + + expect(response).toBeDefined(); + }); + test('Create email template', async () => { const emailTemplateObj = await EmailTemplates.create({ name: _emailTemplate.name, diff --git a/src/__tests__/engageMessageDb.test.ts b/src/__tests__/engageMessageDb.test.ts index abe5a74c6..dcac39ef2 100644 --- a/src/__tests__/engageMessageDb.test.ts +++ b/src/__tests__/engageMessageDb.test.ts @@ -1,14 +1,23 @@ -import * as Random from 'meteor-random'; +import * as sinon from 'sinon'; import { brandFactory, + conversationMessageFactory, customerFactory, + engageDataFactory, engageMessageFactory, + integrationFactory, segmentFactory, tagsFactory, userFactory, } from '../db/factories'; -import { Brands, Customers, EngageMessages, Segments, Tags, Users } from '../db/models'; - +import { Brands, Conversations, Customers, EngageMessages, Integrations, Segments, Tags, Users } from '../db/models'; + +import Messages from '../db/models/ConversationMessages'; +import { IBrandDocument } from '../db/models/definitions/brands'; +import { ICustomerDocument } from '../db/models/definitions/customers'; +import { IIntegrationDocument } from '../db/models/definitions/integrations'; +import { IUserDocument } from '../db/models/definitions/users'; +import * as events from '../events'; import './setup.ts'; describe('engage messages model tests', () => { @@ -17,8 +26,6 @@ describe('engage messages model tests', () => { let _brand; let _tag; let _message; - let _customer; - let _customer2; beforeEach(async () => { _user = await userFactory({}); @@ -26,8 +33,6 @@ describe('engage messages model tests', () => { _brand = await brandFactory({}); _tag = await tagsFactory({}); _message = await engageMessageFactory({ kind: 'auto' }); - _customer = await customerFactory({}); - _customer2 = await customerFactory({}); }); afterEach(async () => { @@ -39,6 +44,18 @@ describe('engage messages model tests', () => { await Customers.deleteMany({}); }); + test('Get engage message', async () => { + try { + await EngageMessages.getEngageMessage('fakeId'); + } catch (e) { + expect(e.message).toBe('Engage message not found'); + } + + const response = await EngageMessages.getEngageMessage(_message._id); + + expect(response).toBeDefined(); + }); + test('create messages', async () => { const doc = { kind: 'manual', @@ -148,61 +165,9 @@ describe('engage messages model tests', () => { }); test('save matched customer ids', async () => { - const message = await EngageMessages.setCustomerIds(_message._id, [_customer, _customer2]); - - if (!message || !message.customerIds) { - throw new Error('Engage message not found'); - } - - expect(message.customerIds).toContain(_customer._id); - expect(message.customerIds).toContain(_customer2._id); - expect(message.customerIds.length).toEqual(2); - }); - - test('add new delivery report', async () => { - const mailMessageId = Random.id(); - const message = await EngageMessages.addNewDeliveryReport(_message._id, mailMessageId, _customer._id); - - expect(message.deliveryReports[`${mailMessageId}`].status).toEqual('pending'); - expect(message.deliveryReports[`${mailMessageId}`].customerId).toEqual(_customer._id); - }); - - test('change delivery report status', async () => { - const customer = await customerFactory({}); - const mailId = Random.id(); - - const headers = { - mailId, - customerId: customer._id, - engageMessageId: _message._id, - }; - - const message = await EngageMessages.changeDeliveryReportStatus(headers, 'sent'); - - expect(message.deliveryReports[`${mailId}`].status).toEqual('sent'); - }); - - test('Set customer to do not disturb when complaint or bounce', async () => { - const customer = await customerFactory({ - doNotDisturb: 'No', - }); - const mailId = Random.id(); - - const headers = { - mailId, - customerId: customer._id, - engageMessageId: _message._id, - }; - - await EngageMessages.changeDeliveryReportStatus(headers, 'bounce'); - - const customerObj = await Customers.findOne({ _id: customer._id }); + const message = await EngageMessages.setCustomersCount(_message._id, 'totalCustomersCount', 2); - if (!customerObj) { - throw new Error('Customer not found'); - } - - expect(customerObj.doNotDisturb).toBe('Yes'); + expect(message.totalCustomersCount).toBe(2); }); test('changeCustomer', async () => { @@ -231,7 +196,11 @@ describe('engage messages model tests', () => { customerIds: [customer._id], }); - await EngageMessages.removeCustomerEngages(customer._id); + await engageMessageFactory({ + customerIds: [customer._id], + }); + + await EngageMessages.removeCustomersEngages([customer._id]); const engageMessages = await EngageMessages.find({ customerIds: { $in: [customer._id] }, @@ -241,34 +210,540 @@ describe('engage messages model tests', () => { messengerReceivedCustomerIds: { $in: [customer._id] }, }); - expect(engageMessages).toHaveLength(0); + expect(engageMessages).toHaveLength(2); expect(messengerReceivedCustomerIds).toHaveLength(0); }); +}); + +describe('createConversation', () => { + let _customer: ICustomerDocument; + let _integration: IIntegrationDocument; + + beforeEach(async () => { + // Creating test data + _customer = await customerFactory(); + _integration = await integrationFactory({}); + }); + + afterEach(async () => { + // Clearing test data + await Customers.deleteMany({}); + await Integrations.deleteMany({}); + await Conversations.deleteMany({}); + await Messages.deleteMany({}); + }); - test('increaseStat', async () => { - const engageMessage = await engageMessageFactory({}); + test('createOrUpdateConversationAndMessages', async () => { + const user = await userFactory({ fullName: 'Full name' }); - await EngageMessages.updateStats(engageMessage._id, 'open'); + const replacedContent = 'hi Full name'; - let engageMessageObj = await EngageMessages.findOne({ - _id: engageMessage._id, + const kwargs = { + customer: _customer, + integration: _integration, + user, + replacedContent, + engageData: engageDataFactory({ + content: replacedContent, + messageId: '_id', + }), + }; + + // create ========================== + const message = await EngageMessages.createOrUpdateConversationAndMessages(kwargs); + + if (!message) { + throw new Error('message is null'); + } + + const conversation = await Conversations.findOne({ + _id: message.conversationId, }); - if (!engageMessageObj || !engageMessageObj.stats) { - throw new Error('Engage message not found'); + if (!conversation) { + throw new Error('conversation not found'); } - expect(engageMessageObj.stats).toBeDefined(); - expect(engageMessageObj.stats.toJSON()).toEqual({ open: 1 }); + expect(await Conversations.find().countDocuments()).toBe(1); + expect(await Messages.find().countDocuments()).toBe(1); - await EngageMessages.updateStats(engageMessage._id, 'open'); + // check message fields + expect(message._id).toBeDefined(); + expect(message.content).toBe(replacedContent); + expect(message.engageData && message.engageData.content).toBe(replacedContent); + expect(message.userId).toBe(user._id); + expect(message.customerId).toBe(_customer._id); - engageMessageObj = await EngageMessages.findOne({ _id: engageMessage._id }); + // check conversation fields + expect(conversation._id).toBeDefined(); + expect(conversation.content).toBe(replacedContent); + expect(conversation.integrationId).toBe(_integration._id); - if (!engageMessageObj || !engageMessageObj.stats) { - throw new Error('Engage message not found'); + // second time ========================== + // must not create new conversation & messages update + await Messages.updateMany({ conversationId: conversation._id }, { $set: { isCustomerRead: true } }); + + let response = await EngageMessages.createOrUpdateConversationAndMessages(kwargs); + + expect(response).toBe(null); + + expect(await Conversations.find().countDocuments()).toBe(1); + expect(await Messages.find().countDocuments()).toBe(1); + + const updatedMessage = await Messages.findOne({ + conversationId: conversation._id, + }); + + if (!updatedMessage) { + throw new Error('message not found'); } - expect(engageMessageObj.stats.toJSON()).toEqual({ open: 2 }); + expect(updatedMessage.isCustomerRead).toBe(false); + + // do not mark as unread for conversations that + // have more than one messages ===================== + await Messages.updateMany({ conversationId: conversation._id }, { $set: { isCustomerRead: true } }); + + await conversationMessageFactory({ + conversationId: conversation._id, + isCustomerRead: true, + }); + + response = await EngageMessages.createOrUpdateConversationAndMessages(kwargs); + + expect(response).toBe(null); + + expect(await Conversations.find().countDocuments()).toBe(1); + expect(await Messages.find().countDocuments()).toBe(2); + + const [message1, message2] = await Messages.find({ + conversationId: conversation._id, + }); + + expect(message1.isCustomerRead).toBe(true); + expect(message2.isCustomerRead).toBe(true); + }); +}); + +describe('createVisitorMessages', () => { + let _user: IUserDocument; + let _brand: IBrandDocument; + let _customer: ICustomerDocument; + let _integration: IIntegrationDocument; + let mock; + + beforeEach(async () => { + // Creating test data + _customer = await customerFactory({ firstName: 'firstName', lastName: 'lastName' }); + + mock = sinon.stub(events, 'getNumberOfVisits').callsFake(() => { + return Promise.resolve(11); + }); + + _brand = await brandFactory({}); + _integration = await integrationFactory({ brandId: _brand._id }); + _user = await userFactory({}); + + const message = new EngageMessages({ + title: 'Visitor', + fromUserId: _user._id, + kind: 'visitorAuto', + method: 'messenger', + isLive: true, + messenger: { + brandId: _brand._id, + rules: [ + { + kind: 'currentPageUrl', + condition: 'is', + value: '/page', + }, + { + kind: 'numberOfVisits', + condition: 'greaterThan', + value: 10, + }, + ], + content: 'hi,{{ customer.name }}', + }, + }); + + // invalid from user id + await engageMessageFactory({ + kind: 'visitorAuto', + userId: 'invalid', + isLive: true, + messenger: { + brandId: _brand._id, + content: 'hi,{{ customer.firstName }} {{ customer.lastName }}', + }, + }); + + return message.save(); + }); + + afterEach(async () => { + // Clearing test data + await Customers.deleteMany({}); + await Integrations.deleteMany({}); + await Conversations.deleteMany({}); + await EngageMessages.deleteMany({}); + await Messages.deleteMany({}); + await Brands.deleteMany({}); + + mock.restore(); + }); + + test('must create conversation & message object', async () => { + // previous unread conversation messages created by engage + await conversationMessageFactory({ + customerId: _customer._id, + isCustomerRead: false, + engageData: engageDataFactory({ + messageId: '_id2', + }), + }); + + await conversationMessageFactory({ + customerId: _customer._id, + isCustomerRead: false, + engageData: engageDataFactory({ + messageId: '_id2', + }), + }); + + // main call + const msgs = await EngageMessages.createVisitorMessages({ + brand: _brand, + customer: _customer, + integration: _integration, + browserInfo: { + url: '/page', + }, + }); + + const conversation = await Conversations.findOne({ _id: { $in: msgs.map(m => m.conversationId) } }); + + if (!conversation) { + throw new Error('conversation not found'); + } + + const content = `hi,${_customer.firstName || ''} ${_customer.lastName || ''}`; + + expect(conversation._id).toBeDefined(); + expect(conversation.content).toBe(content); + expect(conversation.customerId).toBe(_customer._id); + expect(conversation.integrationId).toBe(_integration._id); + + const message = await Messages.findOne({ + conversationId: conversation._id, + }); + + if (!message) { + throw new Error('message not found'); + } + + expect(message._id).toBeDefined(); + expect(message.content).toBe(content); + + // count of unread conversation messages created by engage must be zero + const convEngageMessages = await Messages.find({ + customerId: _customer._id, + isCustomerRead: false, + engageData: { $exists: true }, + }); + + expect(convEngageMessages.length).toBe(0); + }); + + const browserLanguageRule = { + kind: 'browserLanguage', + condition: 'is', + value: 'en', + }; + + describe('checkRules', () => { + test('browserLanguage: not matched', async () => { + const response = await EngageMessages.checkRules({ + rules: [browserLanguageRule], + browserInfo: { language: 'mn' }, + }); + + expect(response).toBe(false); + }); + + test('browserLanguage: not all rules matched', async () => { + const response = await EngageMessages.checkRules({ + rules: [ + browserLanguageRule, + { + kind: 'browserLanguage', + condition: 'is', + value: 'mn', + }, + ], + + browserInfo: { language: 'en' }, + }); + + expect(response).toBe(false); + }); + + test('browserLanguage: all rules matched', async () => { + const response = await EngageMessages.checkRules({ + rules: [browserLanguageRule, browserLanguageRule], + browserInfo: { language: 'en' }, + }); + + expect(response).toBe(true); + }); + }); + + describe('checkIndividualRule', () => { + // is + test('is: not matching', () => { + const response = EngageMessages.checkRule({ + rule: browserLanguageRule, + browserInfo: { language: 'mn' }, + }); + + expect(response).toBe(false); + }); + + test('is: matching', () => { + const response = EngageMessages.checkRule({ + rule: browserLanguageRule, + browserInfo: { language: 'en' }, + }); + + expect(response).toBe(true); + }); + + // isNot + const isNotRule = { + kind: 'currentPageUrl', + condition: 'isNot', + value: '/page', + }; + + test('isNot: not matching', () => { + const response = EngageMessages.checkRule({ + rule: isNotRule, + browserInfo: { url: '/page' }, + }); + + expect(response).toBe(false); + }); + + test('isNot: matching', () => { + const response = EngageMessages.checkRule({ + rule: isNotRule, + browserInfo: { url: '/category' }, + }); + + expect(response).toBe(true); + }); + + // isUnknown + const isUnknownRule = { + kind: 'city', + condition: 'isUnknown', + }; + + test('isUnknown: not matching', () => { + const response = EngageMessages.checkRule({ + rule: isUnknownRule, + browserInfo: { city: 'Ulaanbaatar' }, + }); + + expect(response).toBe(false); + }); + + test('isUnknown: matching', () => { + const response = EngageMessages.checkRule({ + rule: isUnknownRule, + browserInfo: {}, + }); + + expect(response).toBe(true); + }); + + // hasAnyValue + const hasAnyValueRule = { + kind: 'country', + condition: 'hasAnyValue', + }; + + test('hasAnyValue: not matching', () => { + const response = EngageMessages.checkRule({ + rule: hasAnyValueRule, + browserInfo: {}, + }); + + expect(response).toBe(false); + }); + + test('hasAnyValue: matching', () => { + const response = EngageMessages.checkRule({ + rule: hasAnyValueRule, + browserInfo: { countryCode: 'MN' }, + }); + + expect(response).toBe(true); + }); + + // startsWith + const startsWithRule = { + kind: 'browserLanguage', + condition: 'startsWith', + value: 'en', + }; + + test('startsWith: not matching', () => { + const response = EngageMessages.checkRule({ + rule: startsWithRule, + browserInfo: { language: 'mongolian' }, + }); + + expect(response).toBe(false); + }); + + test('startsWith: matching', () => { + const response = EngageMessages.checkRule({ + rule: startsWithRule, + browserInfo: { language: 'english' }, + }); + + expect(response).toBe(true); + }); + + // endsWith + const endsWithRule = { + kind: 'browserLanguage', + condition: 'endsWith', + value: 'ian', + }; + + test('endsWith: not matching', () => { + const response = EngageMessages.checkRule({ + rule: endsWithRule, + browserInfo: { language: 'english' }, + }); + + expect(response).toBe(false); + }); + + test('endsWith: matching', () => { + const response = EngageMessages.checkRule({ + rule: endsWithRule, + browserInfo: { language: 'mongolian' }, + }); + + expect(response).toBe(true); + }); + + // greaterThan + const greaterThanRule = { + kind: 'numberOfVisits', + condition: 'greaterThan', + value: '1', + }; + + test('greaterThan: not matching', () => { + const response = EngageMessages.checkRule({ + rule: greaterThanRule, + browserInfo: {}, + numberOfVisits: 0, + }); + + expect(response).toBe(false); + }); + + test('greaterThan: matching', () => { + const response = EngageMessages.checkRule({ + rule: greaterThanRule, + browserInfo: {}, + numberOfVisits: 2, + }); + + expect(response).toBe(true); + }); + + // lessThan + const lessThanRule = { + kind: 'numberOfVisits', + condition: 'lessThan', + value: '1', + }; + + test('lessThan: not matching', () => { + const response = EngageMessages.checkRule({ + rule: lessThanRule, + browserInfo: {}, + numberOfVisits: 2, + }); + + expect(response).toBe(false); + }); + + test('lessThan: matching', () => { + const response = EngageMessages.checkRule({ + rule: lessThanRule, + browserInfo: {}, + numberOfVisits: 0, + }); + + expect(response).toBe(true); + }); + + // contains ====== + const containsRule = { + kind: 'currentPageUrl', + condition: 'contains', + value: 'page', + }; + + test('contains: not matching', () => { + const response = EngageMessages.checkRule({ + rule: containsRule, + browserInfo: { url: '/test' }, + }); + + expect(response).toBe(false); + }); + + test('contains: matching', () => { + const response = EngageMessages.checkRule({ + rule: containsRule, + browserInfo: { url: '/page' }, + }); + + expect(response).toBe(true); + }); + + // does not contain ====== + const doesNotContainsRule = { + kind: 'currentPageUrl', + condition: 'doesNotContain', + value: 'page', + }; + + test('does not contains: not matching', () => { + const response = EngageMessages.checkRule({ + rule: doesNotContainsRule, + browserInfo: { url: '/page' }, + }); + + expect(response).toBe(false); + }); + + test('does not contains: matching', () => { + const response = EngageMessages.checkRule({ + rule: doesNotContainsRule, + browserInfo: { url: '/test' }, + }); + + expect(response).toBe(true); + }); }); }); diff --git a/src/__tests__/engageMessageMutations.test.ts b/src/__tests__/engageMessageMutations.test.ts index 5fa29315d..a63289c08 100644 --- a/src/__tests__/engageMessageMutations.test.ts +++ b/src/__tests__/engageMessageMutations.test.ts @@ -1,7 +1,6 @@ import * as faker from 'faker'; -import * as moment from 'moment'; import * as sinon from 'sinon'; -import { INTEGRATION_KIND_CHOICES } from '../data/constants'; +import { MESSAGE_KINDS } from '../data/constants'; import * as engageUtils from '../data/resolvers/mutations/engageUtils'; import { graphqlRequest } from '../db/connection'; import { @@ -27,21 +26,35 @@ import { Tags, Users, } from '../db/models'; +import messageBroker from '../messageBroker'; -import { awsRequests } from '../trackers/engageTracker'; - +import { EngagesAPI } from '../data/dataSources'; +import { handleUnsubscription } from '../data/utils'; +import { KIND_CHOICES, METHODS } from '../db/models/definitions/constants'; import './setup.ts'; +// to prevent duplicate expect checks +const checkEngageMessage = (src, result) => { + expect(result.kind).toBe(src.kind); + expect(new Date(result.stopDate)).toEqual(src.stopDate); + expect(result.tagIds).toEqual(src.tagIds); + expect(result.brandIds).toEqual(src.brandIds); + expect(result.customerIds).toEqual(src.customerIds); + expect(result.title).toBe(src.title); + expect(result.fromUserId).toBe(src.fromUserId); + expect(result.method).toBe(src.method); + expect(result.isDraft).toBe(src.isDraft); + expect(result.isLive).toBe(src.isLive); +}; + describe('engage message mutation tests', () => { let _message; let _user; let _tag; let _brand; - let _segment; let _customer; let _integration; let _doc; - let context; let spy; const commonParamDefs = ` @@ -59,56 +72,104 @@ describe('engage message mutation tests', () => { $email: EngageMessageEmail, $scheduleDate: EngageScheduleDateInput, $messenger: EngageMessageMessenger, + $shortMessage: EngageMessageSmsInput `; const commonParams = ` - title: $title - kind: $kind - method: $method - fromUserId: $fromUserId - isDraft: $isDraft - isLive: $isLive - stopDate: $stopDate - segmentIds: $segmentIds - brandIds: $brandIds - tagIds: $tagIds - customerIds: $customerIds - email: $email - scheduleDate: $scheduleDate - messenger: $messenger + title: $title, + kind: $kind, + method: $method, + fromUserId: $fromUserId, + isDraft: $isDraft, + isLive: $isLive, + stopDate: $stopDate, + segmentIds: $segmentIds, + brandIds: $brandIds, + tagIds: $tagIds, + customerIds: $customerIds, + email: $email, + scheduleDate: $scheduleDate, + messenger: $messenger, + shortMessage: $shortMessage + `; + + const commonFields = ` + kind + segmentIds + brandIds + tagIds + customerIds + title + fromUserId + method + isDraft + stopDate + isLive + messengerReceivedCustomerIds + email + messenger + shortMessage { + from + fromIntegrationId + content + } + + segments { + _id + } + fromUser { + _id + } + getTags { + _id + } + fromIntegration { + _id + } `; + let dataSources; + beforeEach(async () => { + dataSources = { EngagesAPI: new EngagesAPI() }; + // Creating test data _user = await userFactory({}); _tag = await tagsFactory({}); _brand = await brandFactory({}); - _segment = await segmentFactory({ - connector: 'any', - conditions: [{ field: 'primaryEmail', operator: 'c', value: '@', type: 'string' }], + _integration = await integrationFactory({ brandId: _brand._id }); + + _customer = await customerFactory({ + integrationId: _integration._id, + emailValidationStatus: 'valid', + phoneValidationStatus: 'valid', + status: 'Active', + profileScore: 1, + primaryEmail: faker.internet.email(), + firstName: faker.random.word(), + lastName: faker.random.word(), }); + _message = await engageMessageFactory({ - kind: 'auto', + kind: MESSAGE_KINDS.AUTO, userId: _user._id, messenger: { content: 'content', brandId: _brand.id, }, + customerIds: [_customer._id], + brandIds: [_brand._id], + tagIds: [_tag._id], }); - _customer = await customerFactory({ - hasValidEmail: true, - }); - _integration = await integrationFactory({ brandId: 'brandId' }); _doc = { title: 'Message test', - kind: 'manual', - method: 'email', + kind: MESSAGE_KINDS.AUTO, + method: METHODS.EMAIL, fromUserId: _user._id, isDraft: true, isLive: true, stopDate: new Date(), - segmentIds: [_segment._id], brandIds: [_brand._id], tagIds: [_tag._id], customerIds: [_customer._id], @@ -120,7 +181,6 @@ describe('engage message mutation tests', () => { type: 'year', month: '2', day: '14', - time: moment('2018-08-24T12:45:00'), }, messenger: { brandId: _brand._id, @@ -130,7 +190,7 @@ describe('engage message mutation tests', () => { rules: [ { _id: _message._id, - kind: 'manual', + kind: MESSAGE_KINDS.MANUAL, text: faker.random.word(), condition: faker.random.word(), value: faker.random.word(), @@ -138,14 +198,12 @@ describe('engage message mutation tests', () => { ], }, }; - - context = { user: _user }; - spy = jest.spyOn(engageUtils, 'send'); }); afterEach(async () => { spy.mockRestore(); + // Clearing test data _doc = null; await Users.deleteMany({}); @@ -160,13 +218,20 @@ describe('engage message mutation tests', () => { await ConversationMessages.deleteMany({}); }); + test('generateCustomerSelector', async () => { + const segment = await segmentFactory({}); + const brand = await brandFactory({}); + await integrationFactory({ brandId: brand._id }); + + await engageUtils.generateCustomerSelector({ segmentIds: [segment._id], brandIds: [brand._id] }); + }); + test('Engage utils send via messenger', async () => { const brand = await brandFactory(); const emessage = await engageMessageFactory({ - method: 'messenger', + method: METHODS.MESSENGER, title: 'Send via messenger', userId: _user._id, - segmentIds: [_segment._id], customerIds: [_customer._id], isLive: true, messenger: { @@ -175,11 +240,17 @@ describe('engage message mutation tests', () => { }, }); + try { + await engageUtils.send(emessage); + } catch (e) { + expect(e.message).toEqual('Integration not found'); + } + const emessageWithoutUser = await engageMessageFactory({ - method: 'messenger', + method: METHODS.MESSENGER, title: 'Send via messenger', userId: 'fromUserId', - segmentIds: [_segment._id], + customerIds: [_customer._id], isLive: true, messenger: { brandId: brand._id, @@ -187,12 +258,6 @@ describe('engage message mutation tests', () => { }, }); - try { - await engageUtils.send(emessage); - } catch (e) { - expect(e.message).toEqual('Integration not found'); - } - try { await engageUtils.send(emessageWithoutUser); } catch (e) { @@ -201,14 +266,13 @@ describe('engage message mutation tests', () => { const integration = await integrationFactory({ brandId: brand._id, - kind: INTEGRATION_KIND_CHOICES.MESSENGER, + kind: KIND_CHOICES.MESSENGER, }); const emessageWithBrand = await engageMessageFactory({ - method: 'messenger', + method: METHODS.MESSENGER, title: 'Send via messenger', userId: _user._id, - segmentIds: [_segment._id], isLive: true, customerIds: [_customer._id], messenger: { @@ -253,23 +317,68 @@ describe('engage message mutation tests', () => { expect(newMessage.engageData.messageId).toBe(emessageWithBrand._id); expect(newMessage.engageData.fromUserId).toBe(_user._id); expect(newMessage.engageData.brandId).toBe(brand._id); + + const emessageNoMessenger = await engageMessageFactory({ + isLive: true, + userId: _user._id, + method: 'messenger', + }); + + await engageUtils.send(emessageNoMessenger); + }); // end engage utils send via messenger + + test('Engage utils send via messenger without initial values', async () => { + _customer.firstName = undefined; + _customer.lastName = undefined; + _customer.primaryEmail = undefined; + + _customer.save(); + + const user = await userFactory(); + + user.email = undefined; + user.details = undefined; + user.save(); + + const brand = await brandFactory(); + + await integrationFactory({ + brandId: brand._id, + kind: KIND_CHOICES.MESSENGER, + }); + + const emessage = await engageMessageFactory({ + method: METHODS.MESSENGER, + customerIds: [_customer._id], + userId: user._id, + isLive: true, + messenger: { + brandId: brand._id, + content: 'content', + }, + }); + + await engageUtils.send(emessage); }); - test('Engage utils send via email', async () => { + test('Engage utils send via email & sms', async () => { + const mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + return Promise.resolve('success'); + }); + process.env.AWS_SES_ACCESS_KEY_ID = '123'; process.env.AWS_SES_SECRET_ACCESS_KEY = '123'; process.env.AWS_SES_CONFIG_SET = 'aws-ses'; process.env.AWS_ENDPOINT = '123'; process.env.MAIL_PORT = '123'; process.env.AWS_REGION = 'us-west-2'; - - sinon.stub(engageUtils.utils, 'executeSendViaEmail').callsFake(); + process.env.ENGAGE_ADMINS = '[{"_id":"WkjhEfjJ4QW9EEW9F","name":"engageAdmin","email":"mrbatamar@gmail.com"}]'; const emessage = await engageMessageFactory({ - method: 'email', + method: METHODS.EMAIL, title: 'Send via email', userId: 'fromUserId', - segmentIds: [_segment._id], + customerIds: [_customer], email: { subject: 'subject', content: 'content', @@ -283,13 +392,12 @@ describe('engage message mutation tests', () => { expect(e.message).toBe('User not found'); } - const executeSendViaEmail = jest.spyOn(engageUtils.utils, 'executeSendViaEmail'); - const emessageWithUser = await engageMessageFactory({ - method: 'email', + method: METHODS.EMAIL, title: 'Send via email', userId: _user._id, - segmentIds: [_segment._id], + customerIds: [_customer._id], + isLive: true, email: { subject: 'subject', content: 'content', @@ -299,48 +407,66 @@ describe('engage message mutation tests', () => { await engageUtils.send(emessageWithUser); - expect(executeSendViaEmail.mock.calls.length).toBe(1); + _customer.firstName = undefined; + _customer.lastName = undefined; + + _customer.save(); + + const emessageNoInitial = await engageMessageFactory({ + method: METHODS.EMAIL, + title: 'Send via email', + userId: _user._id, + isLive: true, + customerIds: [_customer._id], + email: { + subject: 'subject', + content: 'content', + attachments: [], + }, + }); + + await engageUtils.send(emessageNoInitial); + + const emessageNotLive = await engageMessageFactory({ + isLive: false, + userId: _user._id, + }); + + await engageUtils.send(emessageNotLive); + + // sms engage msg + const msg = await engageMessageFactory({ + method: METHODS.SMS, + title: 'Send via sms', + userId: _user._id, + customerIds: [_customer._id], + isLive: true, + }); + + await engageUtils.send(msg); + + mock.restore(); }); const engageMessageAddMutation = ` mutation engageMessageAdd(${commonParamDefs}) { engageMessageAdd(${commonParams}) { - kind - segmentIds - brandIds - tagIds - customerIds - title - fromUserId - method - isDraft - stopDate - isLive - stopDate - messengerReceivedCustomerIds - email - messenger - deliveryReports + ${commonFields} + scheduleDate { type day month - time - } - segments { - _id - } - fromUser { - _id - } - getTags { - _id } } } `; test('Add engage message', async () => { + const mock = sinon.stub(messageBroker(), 'sendMessage').callsFake(() => { + return Promise.resolve('success'); + }); + process.env.AWS_SES_ACCESS_KEY_ID = '123'; process.env.AWS_SES_SECRET_ACCESS_KEY = '123'; process.env.AWS_SES_CONFIG_SET = 'aws-ses'; @@ -352,72 +478,29 @@ describe('engage message mutation tests', () => { throw new Error('User not found'); } - const sandbox = sinon.createSandbox(); - - sandbox.stub(awsRequests, 'getVerifiedEmails').callsFake(() => { - return new Promise(resolve => { - return resolve({ VerifiedEmailAddresses: [user.email] }); + try { + await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', { + ..._doc, + kind: MESSAGE_KINDS.MANUAL, + brandIds: ['_id'], }); - }); - - const awsSpy = jest.spyOn(awsRequests, 'getVerifiedEmails'); - const sendSpy = jest.spyOn(engageUtils, 'send'); + } catch (e) { + expect(e[0].message).toBe('No customers found'); + } - const engageMessage = await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', _doc, context); + const engageMessage = await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', _doc); const tags = engageMessage.getTags.map(tag => tag._id); - expect(spy.mock.calls.length).toBe(1); - expect(sendSpy.mock.calls.length).toBe(1); - expect(engageMessage.kind).toBe(_doc.kind); - expect(new Date(engageMessage.stopDate)).toEqual(_doc.stopDate); - expect(engageMessage.segmentIds).toEqual(_doc.segmentIds); - expect(engageMessage.segments[0]._id).toContain(_doc.segmentIds); - expect(engageMessage.tagIds).toEqual(_doc.tagIds); - expect(engageMessage.brandIds).toEqual(_doc.brandIds); - expect(engageMessage.customerIds).toEqual(_doc.customerIds); - expect(engageMessage.title).toBe(_doc.title); - expect(engageMessage.fromUserId).toBe(_doc.fromUserId); - expect(engageMessage.method).toBe(_doc.method); - expect(engageMessage.isDraft).toBe(_doc.isDraft); - expect(engageMessage.isLive).toBe(_doc.isLive); expect(engageMessage.messengerReceivedCustomerIds).toEqual([]); expect(tags).toEqual(_doc.tagIds); - expect(engageMessage.email.toJSON()).toEqual(_doc.email); - expect(engageMessage.messenger.toJSON()).toMatchObject(_doc.messenger); - expect(engageMessage.deliveryReports).toEqual({}); expect(engageMessage.scheduleDate.type).toEqual('year'); expect(engageMessage.scheduleDate.month).toEqual('2'); expect(engageMessage.scheduleDate.day).toEqual('14'); - expect(engageMessage.fromUser._id).toBe(_doc.fromUserId); - awsSpy.mockRestore(); - sendSpy.mockRestore(); - }); - test('Engage add with unverified email', async () => { - expect.assertions(1); + checkEngageMessage(engageMessage, _doc); - process.env.AWS_SES_CONFIG_SET = 'aws-ses'; - process.env.AWS_ENDPOINT = '123'; - - const sandbox = sinon.createSandbox(); - const awsSpy = jest.spyOn(awsRequests, 'getVerifiedEmails'); - const mock = sinon.stub(engageUtils.utils, 'executeSendViaEmail').callsFake(); - - sandbox.stub(awsRequests, 'getVerifiedEmails').callsFake(() => { - return new Promise(resolve => { - return resolve({ VerifiedEmailAddresses: [] }); - }); - }); - - try { - await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', _doc, context); - } catch (e) { - expect(e.toString()).toContain('Email not verified'); - } - - awsSpy.mockRestore(); - mock.mockRestore(); + mock.restore(); }); test('Edit engage message', async () => { @@ -425,45 +508,18 @@ describe('engage message mutation tests', () => { mutation engageMessageEdit($_id: String! ${commonParamDefs}) { engageMessageEdit(_id: $_id ${commonParams}) { _id - kind - segmentIds - brandIds - tagIds - customerIds - title - fromUserId - method - isDraft - isLive - stopDate - messengerReceivedCustomerIds - email - messenger - segments { - _id - } - fromUser { - _id - } - getTags { - _id - } + ${commonFields} } } `; - const engageMessage = await graphqlRequest(mutation, 'engageMessageEdit', { ..._doc, _id: _message._id }, context); + const editedUser = await userFactory(); + const args = { ..._doc, _id: _message._id, fromUserId: editedUser._id }; + + const engageMessage = await graphqlRequest(mutation, 'engageMessageEdit', args); const tags = engageMessage.getTags.map(tag => tag._id); - expect(engageMessage.kind).toBe(_doc.kind); - expect(engageMessage.segmentIds).toEqual(_doc.segmentIds); - expect(engageMessage.segments[0]._id).toContain(_doc.segmentIds); - expect(engageMessage.brandIds).toEqual(_doc.brandIds); - expect(engageMessage.tagIds).toEqual(_doc.tagIds); - expect(engageMessage.customerIds).toEqual(_doc.customerIds); - expect(engageMessage.title).toBe(_doc.title); - expect(engageMessage.fromUserId).toBe(_doc.fromUserId); expect(engageMessage.messenger.brandId).toBe(_doc.messenger.brandId); expect(engageMessage.messenger.kind).toBe(_doc.messenger.kind); expect(engageMessage.messenger.sentAs).toBe(_doc.messenger.sentAs); @@ -472,13 +528,10 @@ describe('engage message mutation tests', () => { expect(engageMessage.messenger.rules.text).toBe(_doc.messenger.rules.text); expect(engageMessage.messenger.rules.condition).toBe(_doc.messenger.rules.condition); expect(engageMessage.messenger.rules.value).toBe(_doc.messenger.rules.value); - expect(engageMessage.method).toBe(_doc.method); - expect(engageMessage.isDraft).toBe(_doc.isDraft); - expect(engageMessage.isLive).toBe(_doc.isLive); expect(engageMessage.messengerReceivedCustomerIds).toEqual([]); expect(tags).toEqual(_doc.tagIds); - expect(engageMessage.email.toJSON()).toEqual(_doc.email); - expect(engageMessage.fromUser._id).toBe(_doc.fromUserId); + + checkEngageMessage(engageMessage, args); }); test('Remove engage message', async () => { @@ -490,7 +543,9 @@ describe('engage message mutation tests', () => { } `; - await graphqlRequest(mutation, 'engageMessageRemove', { _id: _message._id }, context); + _message = await engageMessageFactory({ kind: MESSAGE_KINDS.AUTO }); + + await graphqlRequest(mutation, 'engageMessageRemove', { _id: _message._id }); expect(await EngageMessages.findOne({ _id: _message._id })).toBe(null); }); @@ -503,9 +558,18 @@ describe('engage message mutation tests', () => { } } `; - const engageMessage = await graphqlRequest(mutation, 'engageMessageSetLive', { _id: _message._id }, context); - expect(engageMessage.isLive).toBe(true); + let response = await graphqlRequest(mutation, 'engageMessageSetLive', { _id: _message._id }); + + expect(response.isLive).toBe(true); + + const manualMessage = await engageMessageFactory({ kind: MESSAGE_KINDS.MANUAL }); + + response = await graphqlRequest(mutation, 'engageMessageSetLive', { _id: manualMessage._id }); + + expect(response.isLive).toBe(true); + + await graphqlRequest(mutation, 'engageMessageSetLive', { _id: _message._id }); }); test('Set pause engage message', async () => { @@ -517,7 +581,7 @@ describe('engage message mutation tests', () => { } `; - const engageMessage = await graphqlRequest(mutation, 'engageMessageSetPause', { _id: _message._id }, context); + const engageMessage = await graphqlRequest(mutation, 'engageMessageSetPause', { _id: _message._id }); expect(engageMessage.isLive).toBe(false); }); @@ -547,7 +611,7 @@ describe('engage message mutation tests', () => { const conversation = await conversationFactory(conversationObj); const conversationMessage = await conversationMessageFactory(conversationMessageObj); - _message.segmentIds = [_segment._id]; + _message.customerIds = [_customer._id]; _message.messenger.brandId = _integration.brandId; await _message.save(); @@ -560,15 +624,12 @@ describe('engage message mutation tests', () => { } `; - const sendSpy = jest.spyOn(engageUtils, 'send'); - - const engageMessage = await graphqlRequest(mutation, 'engageMessageSetLiveManual', { _id: _message._id }, context); + const engageMessage = await graphqlRequest(mutation, 'engageMessageSetLiveManual', { _id: _message._id }); if (!conversationMessage.engageData) { throw new Error('Conversation engageData not found'); } - expect(engageUtils.send).toHaveBeenCalled(); expect(engageMessage.isLive).toBe(true); expect(conversation.userId).toBe(conversationObj.userId); expect(conversation.customerId).toBe(conversationObj.customerId); @@ -579,6 +640,124 @@ describe('engage message mutation tests', () => { expect(conversationMessage.userId).toBe(conversationMessageObj.userId); expect(conversationMessage.customerId).toBe(conversationMessageObj.customerId); expect(conversationMessage.content).toBe(conversationMessageObj.content); - sendSpy.mockRestore(); + }); + + test('Handle engage unsubscribe', async () => { + const customer = await customerFactory({ doNotDisturb: 'No' }); + const user = await userFactory({ doNotDisturb: 'No' }); + + await handleUnsubscription({ cid: customer._id, uid: user._id }); + + const updatedCustomer = await Customers.getCustomer(customer._id); + + expect(updatedCustomer.doNotDisturb).toBe('Yes'); + + const updatedUser = await Users.getUser(user._id); + + expect(updatedUser.doNotDisturb).toBe('Yes'); + }); + + test('configSave', async () => { + const mutation = ` + mutation engagesUpdateConfigs($configsMap: JSON!) { + engagesUpdateConfigs(configsMap: $configsMap) + } + `; + + const mock = sinon.stub(dataSources.EngagesAPI, 'engagesUpdateConfigs').callsFake(() => { + return Promise.resolve([]); + }); + + await graphqlRequest( + mutation, + 'engagesUpdateConfigs', + { configsMap: { accessKeyId: 'accessKeyId' } }, + { dataSources }, + ); + + mock.restore(); + }); + + test('dataSources', async () => { + const check = async (mutation, name, args) => { + await graphqlRequest(mutation, name, args, { dataSources }); + }; + + const api = dataSources.EngagesAPI; + + let mock = sinon.stub(api, 'engagesVerifyEmail').callsFake(() => { + return Promise.resolve('true'); + }); + + await check( + ` + mutation engageMessageVerifyEmail($email: String!) { + engageMessageVerifyEmail(email: $email) + } + `, + 'engageMessageVerifyEmail', + { email: 'email@yahoo.com' }, + ); + + mock = sinon.stub(api, 'engagesRemoveVerifiedEmail').callsFake(() => { + return Promise.resolve('true'); + }); + + await check( + ` + mutation engageMessageRemoveVerifiedEmail($email: String!) { + engageMessageRemoveVerifiedEmail(email: $email) + } + `, + 'engageMessageRemoveVerifiedEmail', + { email: 'email@yahoo.com' }, + ); + + mock = sinon.stub(api, 'engagesSendTestEmail').callsFake(() => { + return Promise.resolve('true'); + }); + + await check( + ` + mutation engageMessageSendTestEmail($from: String!, $to: String!, $content: String!) { + engageMessageSendTestEmail(from: $from, to: $to, content: $content) + } + `, + 'engageMessageSendTestEmail', + { from: 'from@yahoo.com', to: 'to@yahoo.com', content: 'content' }, + ); + + mock.restore(); + }); + + test('Test auto engage with type SMS', async () => { + try { + await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', { + ..._doc, + kind: MESSAGE_KINDS.AUTO, + method: METHODS.SMS, + brandIds: ['_id'], + }); + } catch (e) { + expect(e[0].message).toBe(`SMS engage message of kind ${MESSAGE_KINDS.AUTO} is not supported`); + } + }); + + test('Test sms engage message with integration chosen', async () => { + const integration = await integrationFactory({ kind: 'telnyx' }); + + const response = await graphqlRequest(engageMessageAddMutation, 'engageMessageAdd', { + ..._doc, + fromUserId: '', + kind: MESSAGE_KINDS.MANUAL, + method: METHODS.SMS, + shortMessage: { + content: 'sms test', + fromIntegrationId: integration._id, + }, + title: 'Message test', + }); + + expect(response.fromIntegration._id).toBe(integration._id); }); }); diff --git a/src/__tests__/engageMessageQueries.test.ts b/src/__tests__/engageMessageQueries.test.ts index baa4f295b..9347f25a4 100644 --- a/src/__tests__/engageMessageQueries.test.ts +++ b/src/__tests__/engageMessageQueries.test.ts @@ -1,7 +1,9 @@ +import * as sinon from 'sinon'; import { graphqlRequest } from '../db/connection'; import { brandFactory, engageMessageFactory, segmentFactory, tagsFactory, userFactory } from '../db/factories'; import { Brands, EngageMessages, Segments, Tags, Users } from '../db/models'; +import { EngagesAPI } from '../data/dataSources'; import './setup.ts'; describe('engageQueries', () => { @@ -25,27 +27,6 @@ describe('engageQueries', () => { ids: $ids ) { _id - kind - segmentIds - brandIds - tagIds - customerIds - title - fromUserId - method - isDraft - isLive - stopDate - createdDate - messengerReceivedCustomerIds - - email - messenger - deliveryReports - - segments { _id } - fromUser { _id } - getTags { _id } } } `; @@ -56,6 +37,12 @@ describe('engageQueries', () => { } `; + let dataSources; + + beforeEach(async () => { + dataSources = { EngagesAPI: new EngagesAPI() }; + }); + afterEach(async () => { // Clearing test data await EngageMessages.deleteMany({}); @@ -65,6 +52,16 @@ describe('engageQueries', () => { await Segments.deleteMany({}); }); + test('Engage messages', async () => { + await engageMessageFactory({}); + await engageMessageFactory({}); + await engageMessageFactory({}); + + const responses = await graphqlRequest(qryEngageMessages, 'engageMessages'); + + expect(responses.length).toBe(3); + }); + test('Engage messages filtered by ids', async () => { const engageMessage1 = await engageMessageFactory({}); const engageMessage2 = await engageMessageFactory({}); @@ -168,6 +165,43 @@ describe('engageQueries', () => { expect(response.length).toBe(2); }); + test('Enage email delivery report list', async () => { + const dataSourceMock = sinon.stub(dataSources.EngagesAPI, 'engageReportsList').callsFake(() => { + return Promise.resolve({ + list: [ + { + _id: '123', + status: 'pending', + }, + ], + totalCount: 1, + }); + }); + + const query = ` + query engageReportsList($page: Int, $perPage: Int) { + engageReportsList(page: $page, perPage: $perPage) { + totalCount + list { + _id + status + createdAt + customerId + engage { + title + } + } + } + } + `; + + const response = await graphqlRequest(query, 'engageReportsList', {}, { dataSources }); + + expect(response.list.length).toBe(1); + + dataSourceMock.restore(); + }); + test('Engage message detail', async () => { const engageMessage = await engageMessageFactory(); @@ -175,13 +209,49 @@ describe('engageQueries', () => { query engageMessageDetail($_id: String) { engageMessageDetail(_id: $_id) { _id + kind + segmentIds + brandIds + tagIds + customerIds + title + fromUserId + method + isDraft + isLive + stopDate + createdAt + messengerReceivedCustomerIds + + email + messenger + + brands { _id } + segments { _id } + brand { _id } + tags { _id } + fromUser { _id } + getTags { _id } + + stats + logs + smsStats } } `; - const response = await graphqlRequest(qry, 'engageMessageDetail', { _id: engageMessage._id }); + let response = await graphqlRequest(qry, 'engageMessageDetail', { _id: engageMessage._id }, { dataSources }); expect(response._id).toBe(engageMessage._id); + + const brand = await brandFactory(); + const messenger = { brandId: brand._id, content: 'Content' }; + const engageMessageWithBrand = await engageMessageFactory({ messenger }); + + response = await graphqlRequest(qry, 'engageMessageDetail', { _id: engageMessageWithBrand._id }, { dataSources }); + + expect(response._id).toBe(engageMessageWithBrand._id); + expect(response.brand._id).toBe(brand._id); }); test('Count engage messsage by kind', async () => { @@ -220,34 +290,46 @@ describe('engageQueries', () => { expect(response.draft).toBe(1); expect(response.paused).toBe(1); - response = await graphqlRequest(qryCount, 'engageMessageCounts', { name: 'status', kind: 'manual' }, { user }); + response = await graphqlRequest(qryCount, 'engageMessageCounts', { name: 'status' }, { user }); - expect(response.paused).toBe(3); + expect(response.paused).toBe(4); expect(response.yours).toBe(1); }); test('Count engage message by tag', async () => { const tag = await tagsFactory(); + const user = await userFactory(); // default value of isLive, isDraft are 'false' await engageMessageFactory({ kind: 'auto' }); await engageMessageFactory({ kind: 'auto' }); + await engageMessageFactory({ kind: 'auto', tagIds: [tag._id], isLive: true }); + await engageMessageFactory({ kind: 'auto', tagIds: [tag._id], isDraft: true, isLive: true }); await engageMessageFactory({ kind: 'auto', tagIds: [tag._id] }); - await engageMessageFactory({ kind: 'auto', tagIds: [tag._id] }); + await engageMessageFactory({ kind: 'auto', tagIds: [tag._id], userId: user._id }); - let response = await graphqlRequest(qryCount, 'engageMessageCounts', { - name: 'tag', - kind: 'auto', - }); + const args: any = { name: 'tag', kind: 'auto' }; + args.status = 'live'; + let response = await graphqlRequest(qryCount, 'engageMessageCounts', args); expect(response[tag._id]).toBe(2); - response = await graphqlRequest(qryCount, 'engageMessageCounts', { - name: 'tag', - kind: 'manual', - }); + args.status = 'draft'; + response = await graphqlRequest(qryCount, 'engageMessageCounts', args); + expect(response[tag._id]).toBe(1); + + args.status = 'paused'; + response = await graphqlRequest(qryCount, 'engageMessageCounts', args); + expect(response[tag._id]).toBe(2); + + args.status = 'yours'; + response = await graphqlRequest(qryCount, 'engageMessageCounts', args, { user }); + expect(response[tag._id]).toBe(1); - expect(response[tag._id]).toBe(0); + args.kind = ''; + args.status = ''; + response = await graphqlRequest(qryCount, 'engageMessageCounts', args, { user }); + expect(response[tag._id]).toBe(4); }); test('Get total count of engage message', async () => { @@ -265,4 +347,34 @@ describe('engageQueries', () => { expect(response).toBe(3); }); + + test('Get verified emails', async () => { + const qry = ` + query engageVerifiedEmails { + engageVerifiedEmails + } + `; + + const mock = sinon.stub(dataSources.EngagesAPI, 'engagesGetVerifiedEmails').callsFake(() => { + return Promise.resolve([]); + }); + + await graphqlRequest(qry, 'engageVerifiedEmails', {}, { dataSources }); + + mock.restore(); + }); + + test('configDetail', async () => { + const qry = ` + query engagesConfigDetail { + engagesConfigDetail + } + `; + + try { + await graphqlRequest(qry, 'engagesConfigDetail', {}, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Engages api is not running'); + } + }); }); diff --git a/src/__tests__/engageTracker.test.ts b/src/__tests__/engageTracker.test.ts deleted file mode 100644 index 47af1641f..000000000 --- a/src/__tests__/engageTracker.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as moment from 'moment'; -import { createScheduleRule } from '../cronJobs/engages'; - -import './setup.ts'; - -describe('Engage tracker tests', async () => { - test('Create schedule cron job by year', () => { - const doc = { - type: 'year', - month: 2, - day: 14, - time: moment('2018-08-22T12:25:00').toString(), - }; - - const rule = createScheduleRule(doc); - - expect(rule).toBe('25 12 14 2 *'); - }); - - test('Create schedule cron job by month', () => { - const doc = { - type: 'month', - day: 14, - time: moment('2018-08-22T12:25:00').toString(), - }; - - const rule = createScheduleRule(doc); - - expect(rule).toBe('25 12 14 * *'); - }); - - test('Create schedule cron job by day', () => { - const doc = { - type: 'day', - time: moment('2018-08-22T12:25:00').toString(), - }; - - const rule = createScheduleRule(doc); - - expect(rule).toBe('25 12 * * *'); - }); - - test('Create schedule cron job by week day', () => { - const doc = { - type: '5', - time: moment('2018-08-22T12:25:00').toString(), - }; - - const rule = createScheduleRule(doc); - - expect(rule).toBe('25 12 * * 5'); - }); - - test('Create default schedule cron job', () => { - const doc = { - type: '', - month: '', - day: '', - }; - - const rule = createScheduleRule(doc); - - expect(rule).toBe('0 45 23 * * *'); - }); -}); diff --git a/src/__tests__/fieldDb.test.ts b/src/__tests__/fieldDb.test.ts index 3631a786b..4f31c28ff 100644 --- a/src/__tests__/fieldDb.test.ts +++ b/src/__tests__/fieldDb.test.ts @@ -30,19 +30,20 @@ describe('Fields', () => { } // first attempt - let field = await Fields.createField({ contentType: 'customer' }); + let field = await Fields.createField({ contentType: 'customer', text: 'text' }); expect(field.order).toBe(0); // second attempt - field = await Fields.createField({ contentType: 'customer' }); + field = await Fields.createField({ contentType: 'customer', text: 'text' }); expect(field.order).toBe(1); // third attempt - field = await Fields.createField({ contentType: 'customer' }); + field = await Fields.createField({ contentType: 'customer', text: 'text' }); expect(field.order).toBe(2); field = await Fields.createField({ contentType: 'customer', + text: 'text', groupId: group._id, }); expect(field.order).toBe(0); @@ -56,16 +57,17 @@ describe('Fields', () => { // first attempt let field = await Fields.createField({ contentType, + text: 'text', contentTypeId: form1._id, }); expect(field.order).toBe(0); // second attempt - field = await Fields.createField({ contentType, contentTypeId: form1._id }); + field = await Fields.createField({ contentType, contentTypeId: form1._id, text: 'text' }); expect(field.order).toBe(1); // must create new order - field = await Fields.createField({ contentType, contentTypeId: form2._id }); + field = await Fields.createField({ contentType, contentTypeId: form2._id, text: 'text' }); expect(field.order).toBe(0); }); @@ -73,7 +75,7 @@ describe('Fields', () => { expect.assertions(1); try { - await Fields.createField({ contentType: 'form' }); + await Fields.createField({ contentType: 'form', text: 'text' }); } catch (e) { expect(e.message).toEqual('Content type id is required'); } @@ -86,6 +88,7 @@ describe('Fields', () => { await Fields.createField({ contentType: 'form', contentTypeId: 'DFAFDFADS', + text: 'text', }); } catch (e) { expect(e.message).toEqual('Form not found with _id of DFAFDFADS'); @@ -126,7 +129,7 @@ describe('Fields', () => { const fieldObj = await Fields.updateField(_field._id, fieldDoc); try { - await Fields.updateField(testField._id, {}); + await Fields.updateField(testField._id, { text: 'text' }); } catch (e) { expect(e.message).toBe('Cant update this field'); } @@ -153,10 +156,6 @@ describe('Fields', () => { await customerFactory({ customFieldsData: { [_field._id]: '1231' } }); const testField = await fieldFactory({ isDefinedByErxes: true }); - if (!testField) { - throw new Error('Couldnt create field'); - } - try { await Fields.removeField('DFFFDSFD'); } catch (e) { @@ -164,7 +163,7 @@ describe('Fields', () => { } try { - await Fields.updateField(testField._id, {}); + await Fields.updateField(testField._id, { text: 'text' }); } catch (e) { expect(e.message).toBe('Cant update this field'); } @@ -258,8 +257,8 @@ describe('Fields', () => { expect(res).toEqual(expect.any(Date)); }); - test('Validate fields: invalid values', async () => { - expect.assertions(1); + test('Validate fields', async () => { + expect.assertions(4); // required ===== _field.isRequired = true; @@ -270,6 +269,24 @@ describe('Fields', () => { } catch (e) { expect(e.message).toBe(`${_field.text}: required`); } + + // if empty object pass + let response = await Fields.cleanMulti({}); + + expect(response).toEqual({}); + + // if field is empty + _field.isRequired = false; + await _field.save(); + + response = await Fields.cleanMulti({ [_field._id]: '' }); + + expect(response[_field._id]).toBe(''); + + // if value is not empty + response = await Fields.cleanMulti({ [_field._id]: 10 }); + + expect(response[_field._id]).toBe(10); }); test('Update field visible', async () => { @@ -317,7 +334,7 @@ describe('Fields groups', () => { }); test('Create group', async () => { - expect.assertions(5); + expect.assertions(6); const user = await userFactory({}); const doc = { @@ -332,11 +349,17 @@ describe('Fields groups', () => { expect(groupObj.description).toBe(doc.description); expect(groupObj.contentType).toBe(doc.contentType); // we already created fieldGroup on beforeEach of every test + expect(groupObj.order).toBe(1); + + groupObj = await FieldsGroups.createGroup(doc); + expect(groupObj.order).toBe(2); + // create first group whose contentType is company + doc.contentType = FIELDS_GROUPS_CONTENT_TYPES.COMPANY; groupObj = await FieldsGroups.createGroup(doc); - expect(groupObj.order).toBe(3); + expect(groupObj.contentType).toBe(FIELDS_GROUPS_CONTENT_TYPES.COMPANY); }); test('Update group', async () => { diff --git a/src/__tests__/fieldMutations.test.ts b/src/__tests__/fieldMutations.test.ts index 587e62929..6936ce480 100644 --- a/src/__tests__/fieldMutations.test.ts +++ b/src/__tests__/fieldMutations.test.ts @@ -241,12 +241,12 @@ describe('Fields mutations', () => { } `; - const fieldGroup = await graphqlRequest(mutation, 'fieldsGroupsAdd', fieldGroupArgs, context); + const fieldGroup = await graphqlRequest(mutation, 'fieldsGroupsAdd', fieldGroupArgs); expect(fieldGroup.contentType).toBe(fieldGroupArgs.contentType); expect(fieldGroup.name).toBe(fieldGroupArgs.name); expect(fieldGroup.description).toBe(fieldGroupArgs.description); - expect(fieldGroup.order).toBe(fieldGroupArgs.order); + expect(fieldGroup.order).toBe(1); expect(fieldGroup.isVisible).toBe(fieldGroupArgs.isVisible); }); diff --git a/src/__tests__/fieldQueries.test.ts b/src/__tests__/fieldQueries.test.ts index 94f49530c..f8f40a556 100644 --- a/src/__tests__/fieldQueries.test.ts +++ b/src/__tests__/fieldQueries.test.ts @@ -1,8 +1,18 @@ import * as faker from 'faker'; +import * as sinon from 'sinon'; import { graphqlRequest } from '../db/connection'; -import { fieldFactory, fieldGroupFactory } from '../db/factories'; +import { + brandFactory, + customerFactory, + fieldFactory, + fieldGroupFactory, + integrationFactory, + usersGroupFactory, +} from '../db/factories'; import { Companies, Customers, Fields, FieldsGroups } from '../db/models'; +import * as elk from '../elasticsearch'; +import { KIND_CHOICES } from '../db/models/definitions/constants'; import './setup.ts'; describe('fieldQueries', () => { @@ -24,21 +34,11 @@ describe('fieldQueries', () => { const qry = ` query fields($contentType: String! $contentTypeId: String) { fields(contentType: $contentType contentTypeId: $contentTypeId) { + name _id - contentType - contentTypeId - type - validation - text - description - options - isRequired - order - isVisible - isDefinedByErxes - groupId - lastUpdatedUser { _id } - lastUpdatedUserId + lastUpdatedUser { + _id + } } } `; @@ -75,13 +75,51 @@ describe('fieldQueries', () => { }); test('Fields combined by content type', async () => { + const mock = sinon.stub(elk, 'fetchElk').callsFake(() => { + return Promise.resolve({ + aggregations: { + trackedDataKeys: { + fieldKeys: { + buckets: [ + { + key: 'pageView', + hits: { + hits: { + hits: [ + { + _source: { + name: 'pageView', + attributes: [ + { + field: 'url', + value: '/test', + }, + ], + }, + }, + ], + }, + }, + }, + ], + }, + }, + }, + }); + }); + // Creating test data + const visibleGroup = await usersGroupFactory({ isVisible: true }); + const invisibleGroup = await usersGroupFactory({ isVisible: false }); + await fieldFactory({ contentType: 'company' }); - await fieldFactory({ contentType: 'customer' }); + await fieldFactory({ contentType: 'company' }); + await fieldFactory({ contentType: 'customer', groupId: visibleGroup._id }); + await fieldFactory({ contentType: 'customer', groupId: invisibleGroup._id }); const qry = ` - query fieldsCombinedByContentType($contentType: String!) { - fieldsCombinedByContentType(contentType: $contentType) + query fieldsCombinedByContentType($contentType: String!, $usageType: String) { + fieldsCombinedByContentType(contentType: $contentType, usageType: $usageType) } `; @@ -101,20 +139,42 @@ describe('fieldQueries', () => { expect(responseFields.name).toBe(companyFields.name); // customer ======================= + const brand = await brandFactory({}); + const integration = await integrationFactory({ brandId: brand._id, kind: KIND_CHOICES.MESSENGER }); + const integration1 = await integrationFactory({ brandId: brand._id, kind: KIND_CHOICES.MESSENGER }); + const integration2 = await integrationFactory({ brandId: brand._id, kind: KIND_CHOICES.MESSENGER }); + const integration3 = await integrationFactory({ brandId: brand._id, kind: KIND_CHOICES.MESSENGER }); + + await customerFactory({ integrationId: integration._id }); + await customerFactory({ integrationId: integration1._id }); + await customerFactory({ + integrationId: integration2._id, + }); + await customerFactory({ + integrationId: integration3._id, + }); + responses = await graphqlRequest(qry, 'fieldsCombinedByContentType', { contentType: 'customer', + usageType: 'import', }); + responses = await graphqlRequest(qry, 'fieldsCombinedByContentType', { contentType: 'customer' }); + + await graphqlRequest(qry, 'fieldsCombinedByContentType', { contentType: 'product' }); + // getting fields of customers schema - const customerFields: any = []; responseFields = responses.map(response => response.name); + const customerFields: any = []; Customers.schema.eachPath(path => { customerFields.push(path); }); expect(responseFields.firstName).toBe(customerFields.firstName); expect(responseFields.lastName).toBe(customerFields.lastName); + + mock.restore(); }); test('Fields default columns config', async () => { @@ -131,11 +191,11 @@ describe('fieldQueries', () => { contentType: 'customer', }); - expect(responses.length).toBe(4); - expect(responses[0].name).toBe('firstName'); - expect(responses[1].name).toBe('lastName'); - expect(responses[2].name).toBe('primaryEmail'); - expect(responses[3].name).toBe('primaryPhone'); + expect(responses.length).toBe(7); + expect(responses[0].name).toBe('location.country'); + expect(responses[1].name).toBe('firstName'); + expect(responses[2].name).toBe('lastName'); + expect(responses[3].name).toBe('primaryEmail'); // get company default config responses = await graphqlRequest(qry, 'fieldsDefaultColumnsConfig', { @@ -149,6 +209,15 @@ describe('fieldQueries', () => { expect(responses[4].name).toBe('plan'); expect(responses[5].name).toBe('lastSeenAt'); expect(responses[6].name).toBe('sessionCount'); + + // get product default config + responses = await graphqlRequest(qry, 'fieldsDefaultColumnsConfig', { + contentType: 'product', + }); + + expect(responses[0].name).toBe('categoryCode'); + expect(responses[1].name).toBe('code'); + expect(responses[2].name).toBe('name'); }); test('Field groups', async () => { @@ -160,14 +229,18 @@ describe('fieldQueries', () => { query fieldsGroups($contentType: String) { fieldsGroups(contentType: $contentType) { _id + lastUpdatedUser { + _id + } + fields { + _id + } } } `; // customer content type ============ - let responses = await graphqlRequest(qry, 'fieldsGroups', { - contentType: 'customer', - }); + let responses = await graphqlRequest(qry, 'fieldsGroups'); expect(responses.length).toBe(1); diff --git a/src/__tests__/formDb.test.ts b/src/__tests__/formDb.test.ts index 3047def5a..325597e34 100644 --- a/src/__tests__/formDb.test.ts +++ b/src/__tests__/formDb.test.ts @@ -1,7 +1,8 @@ import * as toBeType from 'jest-tobetype'; import { customerFactory, fieldFactory, formFactory, userFactory } from '../db/factories'; -import { Customers, Fields, Forms, Users } from '../db/models'; +import { Customers, Fields, Forms, FormSubmissions, Users } from '../db/models'; +import { FORM_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; expect.extend(toBeType); @@ -18,43 +19,35 @@ describe('form creation', () => { await Forms.deleteMany({}); }); - test(`testing if Error('createdUser must be supplied') is throwing as intended`, async () => { - expect.assertions(1); - - try { - await Forms.createForm( - { - title: 'Test form', - description: 'Test form description', - }, - undefined, - ); - } catch (e) { - expect(e.message).toEqual('createdUser must be supplied'); - } - }); - test('check if form creation method is working successfully', async () => { const form = await Forms.createForm( { title: 'Test form', description: 'Test form description', + type: FORM_TYPES.GROWTH_HACK, }, _user._id, ); - const formObj = await Forms.findOne({ _id: form._id }); + expect(form.title).toBe('Test form'); + expect(form.description).toBe('Test form description'); + expect(form.createdDate).toBeDefined(); + expect(form.createdUserId).toBe(_user._id); + }); +}); - if (!formObj || !formObj.code) { - throw new Error('Form not found'); - } +test('Get form', async () => { + const form = await formFactory(); - expect(formObj.title).toBe('Test form'); - expect(formObj.description).toBe('Test form description'); - expect(formObj.code.length).toBe(6); - expect(formObj.createdDate).toBeDefined(); - expect(formObj.createdUserId).toBe(_user._id); - }); + try { + await Forms.getForm('fakeId'); + } catch (e) { + expect(e.message).toBe('Form not found'); + } + + const response = await Forms.getForm(form._id); + + expect(response).toBeDefined(); }); describe('form update', () => { @@ -75,6 +68,7 @@ describe('form update', () => { const doc = { title: 'Test form 2', description: 'Test form description 2', + type: FORM_TYPES.GROWTH_HACK, }; const formAfterUpdate = await Forms.updateForm(_form._id, doc); @@ -87,7 +81,7 @@ describe('form update', () => { }); }); -describe('form remove', async () => { +describe('form remove', () => { let _form; beforeEach(async () => { @@ -98,6 +92,7 @@ describe('form remove', async () => { await Forms.deleteMany({}); await Fields.deleteMany({}); await Customers.deleteMany({}); + await FormSubmissions.deleteMany({}); }); test('check if form removal is working successfully', async () => { @@ -161,4 +156,111 @@ describe('form duplication', () => { expect(fieldsCount).toEqual(6); expect(duplicatedFieldsCount).toEqual(3); }); + + test('check if formSubmission creation method is working successfully', async () => { + const customer = await customerFactory({}); + + const formSubmission = await FormSubmissions.createFormSubmission({ + customerId: customer._id, + formId: _form._id, + }); + + const formSubmissionObj = await FormSubmissions.findOne({ _id: formSubmission._id }); + + if (!formSubmissionObj) { + throw new Error('Form submission not found'); + } + + expect(formSubmissionObj.customerId).toBe(customer._id); + expect(formSubmissionObj.formId).toBe(_form._id); + }); + + const formId = 'DFDFDAFD'; + const contentTypeId = formId; + + test('validate', async () => { + // not submitted field + await fieldFactory({ + contentTypeId, + }); + + const requiredField = await fieldFactory({ + contentTypeId, + isRequired: true, + }); + + const emailField = await fieldFactory({ + contentTypeId, + validation: 'email', + }); + + const phoneField = await fieldFactory({ + contentTypeId, + validation: 'phone', + }); + + const validPhoneField = await fieldFactory({ + contentTypeId, + validation: 'phone', + }); + + const numberField = await fieldFactory({ + contentTypeId, + validation: 'number', + }); + + const validNumberField = await fieldFactory({ + contentTypeId, + validation: 'number', + }); + + const validDateField = await fieldFactory({ + contentTypeId, + validation: 'date', + }); + + const dateField = await fieldFactory({ + contentTypeId, + validation: 'date', + }); + + const submissions = [ + { _id: requiredField._id, value: null }, + { _id: emailField._id, value: 'email', validation: 'email' }, + { _id: phoneField._id, value: 'phone', validation: 'phone' }, + { _id: validPhoneField._id, value: '88183943', validation: 'phone' }, + { _id: numberField._id, value: 'number', validation: 'number' }, + { _id: validNumberField._id, value: 10, validation: 'number' }, + { _id: dateField._id, value: 'date', validation: 'date' }, + { _id: validDateField._id, value: '2012-09-01', validation: 'date' }, + ]; + + // call function + const errors = await Forms.validate(formId, submissions); + + // must be 4 error + expect(errors.length).toEqual(5); + + const [requiredError, emailError, phoneError, numberError, dateError] = errors; + + // required + expect(requiredError.fieldId).toEqual(requiredField._id); + expect(requiredError.code).toEqual('required'); + + // email + expect(emailError.fieldId).toEqual(emailField._id); + expect(emailError.code).toEqual('invalidEmail'); + + // phone + expect(phoneError.fieldId).toEqual(phoneField._id); + expect(phoneError.code).toEqual('invalidPhone'); + + // number + expect(numberError.fieldId).toEqual(numberField._id); + expect(numberError.code).toEqual('invalidNumber'); + + // date + expect(dateError.fieldId).toEqual(dateField._id); + expect(dateError.code).toEqual('invalidDate'); + }); }); diff --git a/src/__tests__/formMutations.test.ts b/src/__tests__/formMutations.test.ts index 4cf2f6b92..b2cb85e51 100644 --- a/src/__tests__/formMutations.test.ts +++ b/src/__tests__/formMutations.test.ts @@ -1,8 +1,9 @@ import * as faker from 'faker'; import { graphqlRequest } from '../db/connection'; -import { formFactory, userFactory } from '../db/factories'; -import { Forms, Users } from '../db/models'; +import { fieldFactory, formFactory, growthHackFactory } from '../db/factories'; +import { Forms, FormSubmissions, Users } from '../db/models'; +import { FORM_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; /* @@ -11,35 +12,34 @@ import './setup.ts'; const args = { title: faker.random.word(), description: faker.random.word(), + type: FORM_TYPES.GROWTH_HACK, }; describe('form and formField mutations', () => { - let _user; let _form; - let context; const commonParamDefs = ` $title: String! + $type: String! $description: String `; const commonParams = ` title: $title + type: $type description: $description `; beforeEach(async () => { // Creating test data - _user = await userFactory({}); _form = await formFactory({}); - - context = { user: _user }; }); afterEach(async () => { // Clearing test data await Users.deleteMany({}); await Forms.deleteMany({}); + await FormSubmissions.deleteMany({}); }); test('Add form', async () => { @@ -52,7 +52,7 @@ describe('form and formField mutations', () => { } `; - const form = await graphqlRequest(mutation, 'formsAdd', args, context); + const form = await graphqlRequest(mutation, 'formsAdd', args); expect(form.title).toBe(args.title); expect(form.description).toBe(args.description); @@ -69,10 +69,48 @@ describe('form and formField mutations', () => { } `; - const form = await graphqlRequest(mutation, 'formsEdit', { _id: _form._id, ...args }, context); + const form = await graphqlRequest(mutation, 'formsEdit', { _id: _form._id, ...args }); expect(form._id).toBe(_form._id); expect(form.title).toBe(args.title); expect(form.description).toBe(args.description); }); + + test('Form submission save', async () => { + const mutation = ` + mutation formSubmissionsSave($formId: String $contentTypeId: String $contentType: String $formSubmissions: JSON) { + formSubmissionsSave(formId: $formId contentTypeId: $contentTypeId contentType: $contentType formSubmissions: $formSubmissions) + } + `; + + const growthHack = await growthHackFactory(); + const form = await formFactory(); + const formField = await fieldFactory({ text: 'age', contentType: 'form', contentTypeId: form._id }); + + const formSubmissionArgs: any = { + formId: form._id, + contentTypeId: growthHack._id, + contentType: 'growthHack', + formSubmissions: { + [formField._id]: 10, + }, + }; + + let response = await graphqlRequest(mutation, 'formSubmissionsSave', formSubmissionArgs); + + expect(response).toBe(true); + + formSubmissionArgs.formSubmissions = null; + response = await graphqlRequest(mutation, 'formSubmissionsSave', formSubmissionArgs); + + expect(response).toBe(true); + + formSubmissionArgs.formSubmissions = { + [formField._id]: 20, + }; + + response = await graphqlRequest(mutation, 'formSubmissionsSave', formSubmissionArgs); + + expect(response).toBe(true); + }); }); diff --git a/src/__tests__/formQueries.test.ts b/src/__tests__/formQueries.test.ts index f00f89e73..2f295a973 100644 --- a/src/__tests__/formQueries.test.ts +++ b/src/__tests__/formQueries.test.ts @@ -23,6 +23,14 @@ describe('formQueries', () => { _id title code + + createdUser { + _id + } + + fields { + _id + } } } `; @@ -47,6 +55,9 @@ describe('formQueries', () => { _id title code + createdUser { + _id + } } } `; diff --git a/src/__tests__/growthHackDb.test.ts b/src/__tests__/growthHackDb.test.ts new file mode 100644 index 000000000..c80fd3c88 --- /dev/null +++ b/src/__tests__/growthHackDb.test.ts @@ -0,0 +1,100 @@ +import { boardFactory, growthHackFactory, pipelineFactory, stageFactory, userFactory } from '../db/factories'; +import { Boards, GrowthHacks, Pipelines, Stages } from '../db/models'; +import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; +import { IGrowthHackDocument } from '../db/models/definitions/growthHacks'; +import { IUserDocument } from '../db/models/definitions/users'; + +import { BOARD_TYPES } from '../db/models/definitions/constants'; +import './setup.ts'; + +describe('Test growthHacks model', () => { + let board: IBoardDocument; + let pipeline: IPipelineDocument; + let stage: IStageDocument; + let growthHack: IGrowthHackDocument; + let user: IUserDocument; + + beforeEach(async () => { + // Creating test data + board = await boardFactory({ type: BOARD_TYPES.GROWTH_HACK }); + pipeline = await pipelineFactory({ boardId: board._id }); + stage = await stageFactory({ pipelineId: pipeline._id }); + growthHack = await growthHackFactory({ stageId: stage._id }); + user = await userFactory({}); + }); + + afterEach(async () => { + // Clearing test data + await Boards.deleteMany({}); + await Pipelines.deleteMany({}); + await Stages.deleteMany({}); + await GrowthHacks.deleteMany({}); + }); + + test('Get growth hack', async () => { + try { + await GrowthHacks.getGrowthHack('fakeId'); + } catch (e) { + expect(e.message).toBe('Growth hack not found'); + } + + const response = await GrowthHacks.getGrowthHack(growthHack._id); + + expect(response).toBeDefined(); + }); + + // Test growthHack + test('Create growthHack', async () => { + const createdGrowthHack = await GrowthHacks.createGrowthHack({ + stageId: growthHack.stageId, + userId: user._id, + }); + + expect(createdGrowthHack).toBeDefined(); + expect(createdGrowthHack.stageId).toEqual(stage._id); + expect(createdGrowthHack.userId).toEqual(user._id); + }); + + test('Update growthHack', async () => { + const growthHackStageId = 'fakeId'; + const updatedGrowthHack = await GrowthHacks.updateGrowthHack(growthHack._id, { + stageId: growthHackStageId, + }); + + expect(updatedGrowthHack).toBeDefined(); + expect(updatedGrowthHack.stageId).toEqual(growthHackStageId); + expect(updatedGrowthHack.closeDate).toEqual(growthHack.closeDate); + }); + + test('Watch growthHack', async () => { + await GrowthHacks.watchGrowthHack(growthHack._id, true, user._id); + + const watchedGH = await GrowthHacks.getGrowthHack(growthHack._id); + + expect(watchedGH.watchedUserIds).toContain(user._id); + + // testing unwatch + await GrowthHacks.watchGrowthHack(growthHack._id, false, user._id); + + const unwatchedGH = await GrowthHacks.getGrowthHack(growthHack._id); + + expect(unwatchedGH.watchedUserIds).not.toContain(user._id); + }); + + test('Vote growthHack', async () => { + await GrowthHacks.voteGrowthHack(growthHack._id, true, user._id); + + const voteGH = await GrowthHacks.getGrowthHack(growthHack._id); + + expect(voteGH.votedUserIds).toContain(user._id); + expect(voteGH.voteCount).toBe(1); + + // testing unwatch + await GrowthHacks.voteGrowthHack(growthHack._id, false, user._id); + + const unvoteGH = await GrowthHacks.getGrowthHack(growthHack._id); + + expect(unvoteGH.watchedUserIds).not.toContain(user._id); + expect(unvoteGH.voteCount).toBe(0); + }); +}); diff --git a/src/__tests__/growthHackMutations.test.ts b/src/__tests__/growthHackMutations.test.ts new file mode 100644 index 000000000..5595ad38d --- /dev/null +++ b/src/__tests__/growthHackMutations.test.ts @@ -0,0 +1,367 @@ +import { graphqlRequest } from '../db/connection'; +import { + boardFactory, + checklistFactory, + checklistItemFactory, + growthHackFactory, + pipelineFactory, + pipelineLabelFactory, + stageFactory, + userFactory, +} from '../db/factories'; +import { Boards, ChecklistItems, Checklists, GrowthHacks, PipelineLabels, Pipelines, Stages } from '../db/models'; +import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; +import { IGrowthHackDocument } from '../db/models/definitions/growthHacks'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; + +import './setup.ts'; + +describe('Test growthHacks mutations', () => { + let board: IBoardDocument; + let pipeline: IPipelineDocument; + let stage: IStageDocument; + let growthHack: IGrowthHackDocument; + let label: IPipelineLabelDocument; + let context; + + const commonGrowthHackParamDefs = ` + $name: String!, + $stageId: String! + $hackStages: [String] + $assignedUserIds: [String] + $status: String + `; + + const commonGrowthHackParams = ` + name: $name + stageId: $stageId + hackStages: $hackStages + assignedUserIds: $assignedUserIds + status: $status + `; + + const commonDragParamDefs = ` + $itemId: String!, + $aboveItemId: String, + $destinationStageId: String!, + $sourceStageId: String, + $proccessId: String + `; + + const commonDragParams = ` + itemId: $itemId, + aboveItemId: $aboveItemId, + destinationStageId: $destinationStageId, + sourceStageId: $sourceStageId, + proccessId: $proccessId + `; + + beforeEach(async () => { + // Creating test data + board = await boardFactory({ type: BOARD_TYPES.GROWTH_HACK }); + pipeline = await pipelineFactory({ boardId: board._id }); + stage = await stageFactory({ pipelineId: pipeline._id }); + label = await pipelineLabelFactory(); + growthHack = await growthHackFactory({ + initialStageId: stage._id, + stageId: stage._id, + labelIds: [label._id], + }); + context = { user: await userFactory({}) }; + }); + + afterEach(async () => { + // Clearing test data + await Boards.deleteMany({}); + await Pipelines.deleteMany({}); + await Stages.deleteMany({}); + await GrowthHacks.deleteMany({}); + await PipelineLabels.deleteMany({}); + }); + + test('Create growthHack', async () => { + const args = { + name: growthHack.name, + stageId: stage._id, + hackStages: ['one', 'two'], + }; + + const mutation = ` + mutation growthHacksAdd(${commonGrowthHackParamDefs}) { + growthHacksAdd(${commonGrowthHackParams}) { + _id + name + stageId + hackStages + } + } + `; + + const createdGrowthHack = await graphqlRequest(mutation, 'growthHacksAdd', args, context); + + expect(createdGrowthHack.stageId).toEqual(stage._id); + expect(createdGrowthHack.hackStages[0]).toEqual(args.hackStages[0]); + expect(createdGrowthHack.hackStages[1]).toEqual(args.hackStages[1]); + }); + + test('Update growthHack', async () => { + const stage2 = await stageFactory(); + + let args: any = { + _id: growthHack._id, + name: 'changed-name', + stageId: stage2._id, + status: 'archived', + }; + + const mutation = ` + mutation growthHacksEdit($_id: String!, ${commonGrowthHackParamDefs}) { + growthHacksEdit(_id: $_id, ${commonGrowthHackParams}) { + _id + name + stageId + assignedUserIds + } + } + `; + + let updatedGrowthHack = await graphqlRequest(mutation, 'growthHacksEdit', args, context); + + expect(updatedGrowthHack.name).toEqual(args.name); + expect(updatedGrowthHack.stageId).toEqual(args.stageId); + + args.assignedUserIds = [(await userFactory())._id]; + updatedGrowthHack = await graphqlRequest(mutation, 'growthHacksEdit', args, context); + + expect(updatedGrowthHack.assignedUserIds.length).toBe(args.assignedUserIds.length); + + args = { + _id: growthHack._id, + name: 'changed-name', + stageId: stage2._id, + status: 'active', + }; + + updatedGrowthHack = await graphqlRequest(mutation, 'growthHacksEdit', args, context); + expect(updatedGrowthHack.stageId).toBe(args.stageId); + }); + + test('Change growthHack', async () => { + const args = { + proccessId: Math.random().toString(), + itemId: growthHack._id, + aboveItemId: '', + destinationStageId: growthHack.stageId, + sourceStageId: growthHack.stageId, + }; + + const mutation = ` + mutation growthHacksChange(${commonDragParamDefs}) { + growthHacksChange(${commonDragParams}) { + _id + name + stageId + order + } + } + `; + + const updatedGrowthHack = await graphqlRequest(mutation, 'growthHacksChange', args, context); + + expect(updatedGrowthHack._id).toEqual(args.itemId); + }); + + test('Change growthHack if move to another stage', async () => { + const anotherStage = await stageFactory({ pipelineId: pipeline._id }); + + const args = { + proccessId: Math.random().toString(), + itemId: growthHack._id, + aboveItemId: '', + destinationStageId: anotherStage._id, + sourceStageId: growthHack.stageId, + }; + + const mutation = ` + mutation growthHacksChange(${commonDragParamDefs}) { + growthHacksChange(${commonDragParams}) { + _id + name + stageId + order + } + } + `; + + const updatedGH = await graphqlRequest(mutation, 'growthHacksChange', args); + + expect(updatedGH._id).toEqual(args.itemId); + }); + + test('Update growthHack move to pipeline stage', async () => { + const mutation = ` + mutation growthHacksEdit($_id: String!, ${commonGrowthHackParamDefs}) { + growthHacksEdit(_id: $_id, ${commonGrowthHackParams}) { + _id + name + stageId + assignedUserIds + } + } + `; + + const anotherPipeline = await pipelineFactory({ boardId: board._id }); + const anotherStage = await stageFactory({ pipelineId: anotherPipeline._id }); + + const args = { + _id: growthHack._id, + stageId: anotherStage._id, + name: growthHack.name || '', + }; + + const updatedGrowthHack = await graphqlRequest(mutation, 'growthHacksEdit', args); + + expect(updatedGrowthHack._id).toEqual(args._id); + expect(updatedGrowthHack.stageId).toEqual(args.stageId); + }); + + test('Remove growthHack', async () => { + const mutation = ` + mutation growthHacksRemove($_id: String!) { + growthHacksRemove(_id: $_id) { + _id + } + } + `; + + await graphqlRequest(mutation, 'growthHacksRemove', { _id: growthHack._id }, context); + + expect(await GrowthHacks.findOne({ _id: growthHack._id })).toBe(null); + }); + + test('Watch growthHack', async () => { + const mutation = ` + mutation growthHacksWatch($_id: String!, $isAdd: Boolean!) { + growthHacksWatch(_id: $_id, isAdd: $isAdd) { + _id + isWatched + } + } + `; + + const watchAddGrowthHack = await graphqlRequest( + mutation, + 'growthHacksWatch', + { _id: growthHack._id, isAdd: true }, + context, + ); + + expect(watchAddGrowthHack.isWatched).toBe(true); + + const watchRemoveGrowthHack = await graphqlRequest( + mutation, + 'growthHacksWatch', + { _id: growthHack._id, isAdd: false }, + context, + ); + + expect(watchRemoveGrowthHack.isWatched).toBe(false); + }); + + test('Vote growthHack', async () => { + const mutation = ` + mutation growthHacksVote($_id: String!, $isVote: Boolean!) { + growthHacksVote(_id: $_id, isVote: $isVote) { + _id + voteCount + isVoted + votedUsers { + _id + } + } + } + `; + + const votedGrowthHack = await graphqlRequest( + mutation, + 'growthHacksVote', + { _id: growthHack._id, isVote: true }, + context, + ); + + expect(votedGrowthHack.voteCount).toBe(1); + expect(votedGrowthHack.votedUsers[0]._id).toBe(context.user._id); + expect(votedGrowthHack.isVoted).toBe(true); + + const unvotedGrowthHack = await graphqlRequest( + mutation, + 'growthHacksVote', + { _id: growthHack._id, isVote: false }, + context, + ); + + expect(unvotedGrowthHack.voteCount).toBe(0); + expect(unvotedGrowthHack.votedUsers.length).toBe(0); + expect(unvotedGrowthHack.isVoted).toBe(false); + }); + + test('Test growthHacksCopy()', async () => { + const mutation = ` + mutation growthHacksCopy($_id: String!) { + growthHacksCopy(_id: $_id) { + _id + userId + name + stageId + } + } + `; + + const checklist = await checklistFactory({ + contentType: 'growthHack', + contentTypeId: growthHack._id, + title: 'gh-checklist', + }); + + await checklistItemFactory({ + checklistId: checklist._id, + content: 'Improve growthHack mutation test coverage', + isChecked: true, + }); + + const result = await graphqlRequest(mutation, 'growthHacksCopy', { _id: growthHack._id }, context); + + const clonedGhChecklist = await Checklists.findOne({ contentTypeId: result._id }); + + if (clonedGhChecklist) { + const clonedGhChecklistItems = await ChecklistItems.find({ checklistId: clonedGhChecklist._id }); + + expect(clonedGhChecklist.contentTypeId).toBe(result._id); + expect(clonedGhChecklistItems.length).toBe(1); + } + + expect(result.name).toBe(`${growthHack.name}-copied`); + expect(result.stageId).toBe(growthHack.stageId); + }); + + test('Growth hack archive', async () => { + const mutation = ` + mutation growthHacksArchive($stageId: String!) { + growthHacksArchive(stageId: $stageId) + } + `; + + const ghStage = await stageFactory({ type: BOARD_TYPES.GROWTH_HACK }); + + await growthHackFactory({ stageId: ghStage._id }); + await growthHackFactory({ stageId: ghStage._id }); + await growthHackFactory({ stageId: ghStage._id }); + + await graphqlRequest(mutation, 'growthHacksArchive', { stageId: ghStage._id }); + + const ghs = await GrowthHacks.find({ stageId: ghStage._id, status: BOARD_STATUSES.ARCHIVED }); + + expect(ghs.length).toBe(3); + }); +}); diff --git a/src/__tests__/growthHackQueries.test.ts b/src/__tests__/growthHackQueries.test.ts new file mode 100644 index 000000000..30877c0e0 --- /dev/null +++ b/src/__tests__/growthHackQueries.test.ts @@ -0,0 +1,307 @@ +import { graphqlRequest } from '../db/connection'; +import { + boardFactory, + fieldFactory, + formFactory, + formSubmissionFactory, + growthHackFactory, + pipelineFactory, + stageFactory, + userFactory, +} from '../db/factories'; +import { GrowthHacks } from '../db/models'; + +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; +import './setup.ts'; + +describe('growthHackQueries', () => { + const commonGrowthHackTypes = ` + _id + name + stageId + assignedUserIds + closeDate + description + pipeline { _id } + assignedUsers { _id } + impact + labels { _id } + createdUser { _id } + votedUsers { _id } + stage { _id } + isVoted + boardId + formId + scoringType + isWatched + formSubmissions + formFields { _id } + `; + + const qryGrowthHackFilter = ` + query growthHacks( + $stageId: String + $assignedUserIds: [String] + $priority: [String] + $hackStage: [String] + $closeDateType: String + ) { + growthHacks( + stageId: $stageId + assignedUserIds: $assignedUserIds + priority: $priority + hackStage: $hackStage + closeDateType: $closeDateType + ) { + ${commonGrowthHackTypes} + } + } + `; + + afterEach(async () => { + // Clearing test data + await GrowthHacks.deleteMany({}); + }); + + test('Filter by team members', async () => { + const { _id } = await userFactory(); + + await growthHackFactory({ assignedUserIds: [_id] }); + + const response = await graphqlRequest(qryGrowthHackFilter, 'growthHacks', { assignedUserIds: [_id] }); + + expect(response.length).toBe(1); + }); + + test('Filter by priority', async () => { + await growthHackFactory({ priority: 'critical' }); + + const response = await graphqlRequest(qryGrowthHackFilter, 'growthHacks', { priority: ['critical'] }); + + expect(response.length).toBe(1); + }); + + test('Filter by hack stage', async () => { + await growthHackFactory({ hackStages: ['Awareness'] }); + + const response = await graphqlRequest(qryGrowthHackFilter, 'growthHacks', { hackStage: ['Awareness'] }); + + expect(response.length).toBe(1); + }); + + test('Growth hacks', async () => { + const board = await boardFactory({ type: BOARD_TYPES.GROWTH_HACK }); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + await growthHackFactory({ impact: 5, stageId: stage._id }); + await growthHackFactory({ impact: 10, stageId: stage._id }); + await growthHackFactory({ impact: 2, stageId: stage._id }); + + const qry = ` + query growthHacks($stageId: String, $sortField: String, $sortDirection: Int) { + growthHacks(stageId: $stageId, sortField: $sortField, sortDirection: $sortDirection) { + ${commonGrowthHackTypes} + } + } + `; + + let response = await graphqlRequest(qry, 'growthHacks', { stageId: stage._id }); + + expect(response.length).toBe(3); + + // sort descending by impact + response = await graphqlRequest(qry, 'growthHacks', { stageId: stage._id, sortField: 'impact', sortDirection: -1 }); + + expect(response[0].impact).toBe(10); + }); + + test('Growth hacks total count', async () => { + await growthHackFactory({ hackStages: ['Awareness'] }); + await growthHackFactory({ hackStages: ['Awareness'] }); + await growthHackFactory(); + await growthHackFactory(); + + const qry = ` + query growthHacksTotalCount($hackStage: [String]) { + growthHacksTotalCount(hackStage: $hackStage) + } + `; + + const filter = { hackStage: ['Awareness'] }; + + const totalCount = await graphqlRequest(qry, 'growthHacksTotalCount', filter); + + expect(totalCount).toBe(2); + }); + + test('Growth hacks priority matrix', async () => { + const pipeline = await pipelineFactory(); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + await growthHackFactory({ impact: 5, ease: 4, stageId: stage._id }); + await growthHackFactory({ impact: 7, ease: 2, stageId: stage._id }); + + await growthHackFactory({ impact: 5, stageId: stage._id }); + await growthHackFactory({ impact: 5, ease: 0, stageId: stage._id }); + await growthHackFactory({ stageId: stage._id }); + + const qry = ` + query growthHacksPriorityMatrix($pipelineId: String) { + growthHacksPriorityMatrix(pipelineId: $pipelineId) + } + `; + + const priorityMatrix = await graphqlRequest(qry, 'growthHacksPriorityMatrix', { pipelineId: pipeline._id }); + + expect(priorityMatrix.length).toBe(2); + }); + + test('GrowthHack detail', async () => { + const form = await formFactory(); + const field = await fieldFactory({ + contentType: 'form', + contentTypeId: form._id, + }); + + const boardWithForm = await boardFactory({ type: BOARD_TYPES.GROWTH_HACK }); + const pipelineWithForm = await pipelineFactory({ boardId: boardWithForm._id }); + const stageWithForm = await stageFactory({ pipelineId: pipelineWithForm._id, formId: form._id }); + + const user = await userFactory(); + const growthHackWithForm = await growthHackFactory({ + stageId: stageWithForm._id, + watchedUserIds: [user._id], + votedUserIds: [user._id], + }); + + await formSubmissionFactory({ + formId: form._id, + contentTypeId: growthHackWithForm._id, + contentType: 'growthHack', + formFieldId: field._id, + value: 'Hey', + }); + + await formSubmissionFactory({ + formId: form._id, + contentTypeId: growthHackWithForm._id, + contentType: 'growthHack', + }); + + const qry = ` + query growthHackDetail($_id: String!) { + growthHackDetail(_id: $_id) { + ${commonGrowthHackTypes} + } + } + `; + + let response = await graphqlRequest(qry, 'growthHackDetail', { _id: growthHackWithForm._id }, { user }); + + expect(response._id).toBe(growthHackWithForm._id); + expect(response.isWatched).toBe(true); + expect(response.formSubmissions[field._id]).toBe('Hey'); + expect(response.isVoted).toBe(true); + + const growthHack = await growthHackFactory(); + response = await graphqlRequest(qry, 'growthHackDetail', { _id: growthHack._id }); + + expect(response._id).toBe(growthHack._id); + expect(response.isVoted).toBe(false); + }); + + test('Get archived growth hacks', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.GROWTH_HACK }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await growthHackFactory({ ...args, name: 'james' }); + await growthHackFactory({ ...args, name: 'jone' }); + await growthHackFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedGrowthHacks( + $pipelineId: String!, + $search: String, + $page: Int, + $perPage: Int + ) { + archivedGrowthHacks( + pipelineId: $pipelineId + search: $search + page: $page + perPage: $perPage + ) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'archivedGrowthHacks', { + pipelineId: pipeline._id, + }); + + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'archivedGrowthHacks', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'archivedGrowthHacks', { + pipelineId: 'fakeId', + }); + + expect(response.length).toBe(0); + }); + + test('Get archived growth hacks count', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.GROWTH_HACK }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await growthHackFactory({ ...args, name: 'james' }); + await growthHackFactory({ ...args, name: 'jone' }); + await growthHackFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedGrowthHacksCount( + $pipelineId: String!, + $search: String, + ) { + archivedGrowthHacksCount( + pipelineId: $pipelineId + search: $search + ) + } + `; + + let response = await graphqlRequest(qry, 'archivedGrowthHacksCount', { + pipelineId: pipeline._id, + }); + + expect(response).toBe(3); + + response = await graphqlRequest(qry, 'archivedGrowthHacksCount', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response).toBe(1); + + response = await graphqlRequest(qry, 'archivedGrowthHacksCount', { + pipelineId: 'fakeId', + }); + + expect(response).toBe(0); + }); +}); diff --git a/src/__tests__/httpEndpoints.test.ts b/src/__tests__/httpEndpoints.test.ts new file mode 100644 index 000000000..bc9755cf9 --- /dev/null +++ b/src/__tests__/httpEndpoints.test.ts @@ -0,0 +1,105 @@ +import * as faker from 'faker'; +import * as request from 'supertest'; + +import { customerFactory, integrationFactory, scriptFactory, userFactory } from '../db/factories'; +import { Customers, Integrations, Scripts, Users } from '../db/models'; +import { app } from '../index'; +import './setup.ts'; + +/** + * Run this test when erxes-api is stopped, because while erxes-api is running, it throws port being used error. + */ +describe('HTTP endpoint tests', () => { + afterEach(async () => { + await Customers.deleteMany({}); + await Integrations.deleteMany({}); + await Scripts.deleteMany({}); + await Users.deleteMany({}); + }); + + test('Test /initial-setup endpoint with no users', async () => { + const response = await request(app).get('/initial-setup'); + + expect(response.text).toBe('no owner'); + }); + + test('Test /initial-setup endpoint with users', async () => { + await userFactory(); + + const response = await request(app).get('/initial-setup'); + + expect(response.text).toBe('success'); + }); + + test('Test /health', async () => { + const response = await request(app).get('/health'); + + expect(response.text).toBe('ok'); + }); + + test('Test /script-manager without script id', async () => { + const response = await request(app).get('/script-manager'); + + expect(response.text).toBe('Not found'); + }); + + test('Test /script-manager with script id', async () => { + const script = await scriptFactory({ name: 'script' }); + const response = await request(app) + .get('/script-manager') + .query({ id: script._id }); + + expect(response.text).toContain('window.erxesSettings'); + }); + + test('Test /events-identify-customer', async () => { + const integration = await integrationFactory(); + const customer = await customerFactory({ + integrationId: integration._id, + primaryEmail: faker.internet.email(), + primaryPhone: faker.phone.phoneNumber(), + }); + + const response = await request(app) + .post('/events-identify-customer') + .send({ + args: { + email: customer.primaryEmail, + phone: customer.primaryPhone, + code: customer.code, + }, + }); + + expect(response.body).toBeDefined(); + expect(response.body.customerId).toBe(customer._id); + }); + + test('Test /telnyx/webhook', async () => { + const webhookParams = { + from: faker.phone.phoneNumber(), + to: faker.phone.phoneNumber(), + telnyxId: faker.random.uuid(), + }; + + const webhookData = { + data: { + payload: { + from: webhookParams.from || faker.phone.phoneNumber(), + id: webhookParams.telnyxId || faker.random.uuid(), + to: [{ phone_number: webhookParams.to || faker.phone.phoneNumber(), status: faker.random.word() }], + }, + }, + }; + + const response = await request(app) + .post('/telnyx/webhook') + .send(webhookData); + + const { data } = response.body; + + expect(data).toBeDefined(); + expect(data.payload).toBeDefined(); + expect(data.payload.from).toBe(webhookParams.from); + expect(data.payload.id).toBe(webhookParams.telnyxId); + }); +}); diff --git a/src/__tests__/importHistoryDb.test.ts b/src/__tests__/importHistoryDb.test.ts index 2033c3c51..18027271a 100644 --- a/src/__tests__/importHistoryDb.test.ts +++ b/src/__tests__/importHistoryDb.test.ts @@ -1,4 +1,4 @@ -import { customerFactory, userFactory } from '../db/factories'; +import { customerFactory, importHistoryFactory, userFactory } from '../db/factories'; import { Customers, ImportHistory, Users } from '../db/models'; import './setup.ts'; @@ -11,6 +11,20 @@ describe('Import history model test', () => { await Users.deleteMany({}); }); + test('Get customer', async () => { + const importHistory = await importHistoryFactory({}); + + try { + await ImportHistory.getImportHistory('fakeId'); + } catch (e) { + expect(e.message).toBe('Import history not found'); + } + + const response = await ImportHistory.getImportHistory(importHistory._id); + + expect(response).toBeDefined(); + }); + test('Create import history', async () => { const customer = await customerFactory({}); const user = await userFactory({}); @@ -38,6 +52,8 @@ describe('Import history model test', () => { }); test('Remove history', async () => { + expect.assertions(2); + const customer = await customerFactory({}); const customer1 = await customerFactory({}); const customer2 = await customerFactory({}); @@ -57,5 +73,11 @@ describe('Import history model test', () => { await ImportHistory.removeHistory(importHistory._id); expect(await ImportHistory.findOne({ _id: importHistory._id })).toBeNull(); + + try { + await ImportHistory.removeHistory('fakeId'); + } catch (e) { + expect(e.message).toBe('Import history not found'); + } }); }); diff --git a/src/__tests__/importHistoryMutations.test.ts b/src/__tests__/importHistoryMutations.test.ts index e3c094faa..538592dc8 100644 --- a/src/__tests__/importHistoryMutations.test.ts +++ b/src/__tests__/importHistoryMutations.test.ts @@ -1,26 +1,14 @@ -import * as sinon from 'sinon'; import { graphqlRequest } from '../db/connection'; -import { customerFactory, importHistoryFactory, userFactory } from '../db/factories'; -import { ImportHistory, Users } from '../db/models'; -import * as workerUtils from '../workers/utils'; +import { customerFactory, importHistoryFactory } from '../db/factories'; +import { ImportHistory } from '../db/models'; +import messageBroker from '../messageBroker'; import './setup.ts'; describe('Import history mutations', () => { - let _user; - let context; - - beforeEach(async () => { - // Creating test data - _user = await userFactory({}); - - context = { user: _user }; - }); - afterEach(async () => { // Clearing test data await ImportHistory.deleteMany({}); - await Users.deleteMany({}); }); test('Remove import histories', async () => { @@ -29,24 +17,55 @@ describe('Import history mutations', () => { importHistoriesRemove(_id: $_id) } `; + + const spy = jest.spyOn(messageBroker(), 'sendRPCMessage'); + spy.mockImplementation(() => Promise.resolve({ status: 'ok' })); + const customer = await customerFactory({}); - const importHistory = await importHistoryFactory({ - ids: [customer._id], - }); + const customerHistory = await importHistoryFactory({ ids: [customer._id], contentType: 'customer' }); - const mock = sinon.stub(workerUtils, 'createWorkers').callsFake(); + await graphqlRequest(mutation, 'importHistoriesRemove', { _id: customerHistory._id }); + const historyObj = await ImportHistory.getImportHistory(customerHistory._id); - await graphqlRequest(mutation, 'importHistoriesRemove', { _id: importHistory._id }, context); + expect(historyObj.status).toBe('Removing'); - const historyObj = await ImportHistory.findOne({ _id: importHistory._id }); + spy.mockRestore(); + }); - if (!historyObj) { - throw new Error('History not found'); + test('Remove import histories (Error)', async () => { + const mutation = ` + mutation importHistoriesRemove($_id: String!) { + importHistoriesRemove(_id: $_id) + } + `; + + const spy = jest.spyOn(messageBroker(), 'sendRPCMessage'); + spy.mockImplementation(() => Promise.resolve({ status: 'error', message: 'Workers are busy' })); + + const customer = await customerFactory({}); + const importHistory = await importHistoryFactory({ ids: [customer._id] }); + + try { + await graphqlRequest(mutation, 'importHistoriesRemove', { _id: importHistory._id }); + } catch (e) { + expect(e[0].message).toBe('Workers are busy'); } - expect(historyObj.status).toBe('Removing'); + spy.mockRestore(); + }); + + test('Cancel import history', async () => { + const mutation = ` + mutation importHistoriesCancel($_id: String!) { + importHistoriesCancel(_id: $_id) + } + `; + + const importHistory = await importHistoryFactory({}); + + const response = await graphqlRequest(mutation, 'importHistoriesCancel', { _id: importHistory._id }); - mock.restore(); + expect(response).toBe(true); }); }); diff --git a/src/__tests__/importHistoryQueries.test.ts b/src/__tests__/importHistoryQueries.test.ts index dbb013a1e..a16fb40e4 100644 --- a/src/__tests__/importHistoryQueries.test.ts +++ b/src/__tests__/importHistoryQueries.test.ts @@ -19,17 +19,17 @@ describe('Import history queries', () => { importHistories(type: $type) { list { _id - contentType - date - user { - details { - fullName + contentType + date + user { + details { + fullName + } } - } - success - failed - total - ids + success + failed + total + ids } count } @@ -42,6 +42,7 @@ describe('Import history queries', () => { }); expect(responses.list.length).toBe(1); + expect(responses.count).toBe(1); // company ============================ responses = await graphqlRequest(qry, 'importHistories', { @@ -49,5 +50,29 @@ describe('Import history queries', () => { }); expect(responses.list.length).toBe(1); + expect(responses.count).toBe(1); + }); + + test('Import history detail', async () => { + const qry = ` + query importHistoryDetail($_id: String!) { + importHistoryDetail(_id: $_id) { + _id + user { _id } + } + } + `; + + const importHistory = await importHistoryFactory({ errorMsgs: ['error messages'] }); + + let response = await graphqlRequest(qry, 'importHistoryDetail', { _id: importHistory._id }); + + expect(response._id).toBe(importHistory._id); + + const importHistoryNoError = await importHistoryFactory({}); + + response = await graphqlRequest(qry, 'importHistoryDetail', { _id: importHistoryNoError._id }); + + expect(response._id).toBe(importHistoryNoError._id); }); }); diff --git a/src/__tests__/insight/dealInsightQueries.test.ts b/src/__tests__/insight/dealInsightQueries.test.ts index eee03056c..3973c4212 100644 --- a/src/__tests__/insight/dealInsightQueries.test.ts +++ b/src/__tests__/insight/dealInsightQueries.test.ts @@ -10,14 +10,16 @@ const paramsDef = ` $pipelineIds: String, $boardId: String, $startDate: String, - $endDate: String + $endDate: String, + $status: String `; const paramsValue = ` pipelineIds: $pipelineIds, boardId: $boardId, startDate: $startDate, - endDate: $endDate + endDate: $endDate, + status: $status `; describe('dealInsightQueries', () => { @@ -85,21 +87,51 @@ describe('dealInsightQueries', () => { expect(response.length).toBe(1); }); + test('dealInsightsMain', async () => { + const qry = ` + query dealInsightsMain(${paramsDef}) { + dealInsightsMain(${paramsValue}) + } + `; + + let response = await graphqlRequest(qry, 'dealInsightsMain', doc); + + expect(response.trend.length).toBe(1); + + doc.status = 'won'; + response = await graphqlRequest(qry, 'dealInsightsMain', doc); + + expect(response.trend.length).toBe(0); + }); + test('dealInsightsByTeamMember', async () => { const user = await userFactory({}); - Deals.findByIdAndUpdate(deal._id, { + await Deals.findByIdAndUpdate(deal._id, { modifiedAt: new Date(), modifiedBy: user._id, }); + const deal2 = await dealFactory({ stageId: stage._id }); + const user2 = await userFactory({}); + + await Deals.findByIdAndUpdate(deal2._id, { + modifiedAt: new Date(), + modifiedBy: user2._id, + }); + const qry = ` query dealInsightsByTeamMember(${paramsDef}) { dealInsightsByTeamMember(${paramsValue}) } `; - const response = await graphqlRequest(qry, 'dealInsightsByTeamMember', doc); - expect(response.length).toBe(1); + let response = await graphqlRequest(qry, 'dealInsightsByTeamMember', doc); + expect(response.length).toBe(2); + + doc.boardId = 'fakeBoardId'; + + response = await graphqlRequest(qry, 'dealInsightsByTeamMember', { boardId: 'fakeBoardId' }); + expect(response.length).toBe(0); }); }); diff --git a/src/__tests__/insight/insightQueries.test.ts b/src/__tests__/insight/insightQueries.test.ts index bb94a5b11..a1f4c4ebf 100644 --- a/src/__tests__/insight/insightQueries.test.ts +++ b/src/__tests__/insight/insightQueries.test.ts @@ -155,19 +155,19 @@ export const beforeEachTest = async () => { const integration = await integrationFactory({ brandId: brand._id, - kind: 'facebook', + kind: 'facebook-messenger', }); const formIntegration = await integrationFactory({ brandId: brand._id, - kind: 'facebook', + kind: 'facebook-messenger', }); const user = await userFactory({}); const secondUser = await userFactory({}); const args = { - integrationIds: 'facebook', + integrationIds: 'facebook-messenger', brandIds: brand._id, startDate, endDate, diff --git a/src/__tests__/insight/utils.ts b/src/__tests__/insight/utils.ts index 005b30952..11adc90ef 100644 --- a/src/__tests__/insight/utils.ts +++ b/src/__tests__/insight/utils.ts @@ -45,7 +45,7 @@ const generateNoConversation = async (integrationId: string, userId: string) => await conversationFactory({ userId, messageCount: 1 }); }; -const generateFormConversation = async (integrationId: string, userId: string) => { +const generateLeadConversation = async (integrationId: string, userId: string) => { const formConversation = await conversationFactory({ integrationId }); // For request @@ -150,19 +150,19 @@ export const beforeEachTest = async () => { const integration = await integrationFactory({ brandId: brand._id, - kind: 'facebook', + kind: 'facebook-messenger', }); - const formIntegration = await integrationFactory({ + const leadIntegration = await integrationFactory({ brandId: brand._id, - kind: 'form', + kind: 'lead', }); const user = await userFactory({}); const secondUser = await userFactory({}); const args = { - integrationIds: 'facebook', + integrationIds: 'facebook-messenger', brandIds: brand._id, startDate, endDate, @@ -172,7 +172,7 @@ export const beforeEachTest = async () => { await generateNoConversation(integration._id, user._id); // 2 form conversation with two request and two response message respectively - await generateFormConversation(formIntegration._id, user._id); + await generateLeadConversation(leadIntegration._id, user._id); // 2 closed facebook conversation with tag, two request and two response message respectively await generateClosedConversation(integration._id, user._id, tag._id); diff --git a/src/__tests__/integrationDb.test.ts b/src/__tests__/integrationDb.test.ts index 99de99295..fb7b98ab3 100644 --- a/src/__tests__/integrationDb.test.ts +++ b/src/__tests__/integrationDb.test.ts @@ -1,23 +1,28 @@ import * as faker from 'faker'; +import * as momentTz from 'moment-timezone'; import { brandFactory, conversationFactory, conversationMessageFactory, + customerFactory, fieldFactory, formFactory, integrationFactory, userFactory, } from '../db/factories'; -import { Brands, ConversationMessages, Fields, Forms, Integrations, Users } from '../db/models'; -import { FORM_LOAD_TYPES, KIND_CHOICES, MESSENGER_DATA_AVAILABILITY } from '../db/models/definitions/constants'; +import { Brands, ConversationMessages, Forms, Integrations, Users } from '../db/models'; +import { KIND_CHOICES, LEAD_LOAD_TYPES, MESSENGER_DATA_AVAILABILITY } from '../db/models/definitions/constants'; +import { isTimeInBetween } from '../db/models/Integrations'; import './setup.ts'; describe('messenger integration model add method', () => { let _brand; + let _user; beforeEach(async () => { _brand = await brandFactory({}); + _user = await userFactory({}); }); afterEach(async () => { @@ -25,17 +30,75 @@ describe('messenger integration model add method', () => { await Integrations.deleteMany({}); }); + test('Get integration', async () => { + const integration = await integrationFactory({}); + + try { + await Integrations.getIntegration('fakeId'); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + + const response = await Integrations.getIntegration(integration._id); + + expect(response).toBeDefined(); + }); + + test('Find integration', async () => { + const integration = await integrationFactory({}); + + const response = await Integrations.findIntegrations({ _id: integration._id }); + + expect(response.length).toBe(1); + }); + + test('update basic info', async () => { + const integration = await integrationFactory(); + + const doc = { + name: 'updated', + brandId: 'brandId', + }; + + const response = await Integrations.updateBasicInfo(integration._id, doc); + + expect(response.name).toBe(doc.name); + expect(response.brandId).toBe(doc.brandId); + }); + + test('update basic info (Error: Integration not found)', async () => { + const doc = { + name: 'updated', + brandId: 'brandId', + }; + + try { + await Integrations.updateBasicInfo('fakeId', doc); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + }); + test('check if messenger integration create method is running successfully', async () => { + expect.assertions(4); + const doc = { name: 'Integration test', brandId: _brand._id, + kind: KIND_CHOICES.MESSENGER, }; - const integration = await Integrations.createMessengerIntegration(doc); + const integration = await Integrations.createMessengerIntegration(doc, _user._id); expect(integration.name).toBe(doc.name); expect(integration.brandId).toBe(doc.brandId); expect(integration.kind).toBe(KIND_CHOICES.MESSENGER); + + try { + await Integrations.createMessengerIntegration(doc, _user._id); + } catch (e) { + expect(e.message).toBe('Duplicated messenger for single brand'); + } }); }); @@ -73,15 +136,46 @@ describe('messenger integration model edit method', () => { }); }); -describe('form integration create model test without formData', () => { +describe('messenger integration model edit with duplicated brand method', () => { + test('check if messenger integration update method is throwing exception', async () => { + const _brand = await brandFactory({}); + const _brand2 = await brandFactory({}); + const _integration = await integrationFactory({ + kind: KIND_CHOICES.MESSENGER, + brandId: _brand._id, + }); + + await integrationFactory({ + kind: KIND_CHOICES.MESSENGER, + brandId: _brand2._id, + }); + + const doc = { + name: 'Integration test 2', + brandId: _brand2._id, + kind: KIND_CHOICES.MESSENGER, + }; + + try { + await Integrations.updateMessengerIntegration(_integration._id, doc); + } catch (e) { + expect(e.message).toBe('Duplicated messenger for single brand'); + } + + await Brands.deleteMany({}); + await Integrations.deleteMany({}); + }); +}); + +describe('lead integration create model test without leadData', () => { + let _user; let _brand; let _form; - let _user; beforeEach(async () => { - _brand = await brandFactory({}); _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); + _brand = await brandFactory({}); + _form = await formFactory({}); }); afterEach(async () => { @@ -91,32 +185,33 @@ describe('form integration create model test without formData', () => { await Forms.deleteMany({}); }); - test('check if create form integration test wihtout formData is throwing exception', async () => { + test('check if create lead integration test without leadData is throwing exception', async () => { expect.assertions(1); const mainDoc = { - name: 'form integration test', + name: 'lead integration test', brandId: _brand._id, formId: _form._id, + kind: KIND_CHOICES.LEAD, }; try { - await Integrations.createFormIntegration(mainDoc); + await Integrations.createLeadIntegration(mainDoc, _user._id); } catch (e) { - expect(e.message).toEqual('formData must be supplied'); + expect(e.message).toEqual('leadData must be supplied'); } }); }); -describe('create form integration', () => { +describe('create lead integration', () => { + let _user; let _brand; let _form; - let _user; beforeEach(async () => { - _brand = await brandFactory({}); _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); + _brand = await brandFactory({}); + _form = await formFactory({}); }); afterEach(async () => { @@ -126,55 +221,56 @@ describe('create form integration', () => { await Forms.deleteMany({}); }); - test('test if create form integration is working successfully', async () => { + test('test if create lead integration is working successfully', async () => { const mainDoc = { - name: 'form integration test', + name: 'lead integration test', brandId: _brand._id, formId: _form._id, + kind: KIND_CHOICES.LEAD, }; - const formData = { - loadType: FORM_LOAD_TYPES.EMBEDDED, + const leadData = { + loadType: LEAD_LOAD_TYPES.EMBEDDED, }; - const integration = await Integrations.createFormIntegration({ - ...mainDoc, - formData, - }); + const integration = await Integrations.createLeadIntegration( + { + ...mainDoc, + leadData, + }, + _user._id, + ); - if (!integration || !integration.formData) { + if (!integration || !integration.leadData) { throw new Error('Integration not found'); } expect(integration.formId).toEqual(_form._id); expect(integration.name).toEqual(mainDoc.name); expect(integration.brandId).toEqual(_brand._id); - expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.EMBEDDED); - expect(integration.kind).toEqual(KIND_CHOICES.FORM); + expect(integration.leadData.loadType).toEqual(LEAD_LOAD_TYPES.EMBEDDED); + expect(integration.kind).toEqual(KIND_CHOICES.LEAD); }); }); -describe('edit form integration', () => { +describe('edit lead integration', () => { let _brand; let _brand2; let _form; - let _form2; - let _user; - let _formIntegration; + let _leadIntegration; beforeEach(async () => { _brand = await brandFactory({}); _brand2 = await brandFactory({}); - _user = await userFactory({}); - _form = await formFactory({ createdUserId: _user._id }); - _form2 = await formFactory({ createdUserId: _user._id }); - _formIntegration = await integrationFactory({ - name: 'form integration test', + _form = await formFactory({}); + + _leadIntegration = await integrationFactory({ + name: 'lead integration test', brandId: _brand._id, formId: _form._id, - kind: KIND_CHOICES.FORM, - formData: { - loadType: FORM_LOAD_TYPES.EMBEDDED, + kind: KIND_CHOICES.LEAD, + leadData: { + loadType: LEAD_LOAD_TYPES.EMBEDDED, }, }); }); @@ -186,30 +282,56 @@ describe('edit form integration', () => { await Forms.deleteMany({}); }); - test('test if integration form update method is running successfully', async () => { + test('create external integration', async () => { + const brand = await brandFactory(); + const user = await userFactory(); + + const doc = { + name: 'external', + kind: KIND_CHOICES.FACEBOOK_MESSENGER, + brandId: brand._id, + accountId: 'accountId', + }; + + const integration = await Integrations.createExternalIntegration(doc, user._id); + + expect(integration.name).toBe(doc.name); + expect(integration.kind).toBe(doc.kind); + expect(integration.brandId).toBe(doc.brandId); + }); + + test('test if integration lead update method is running successfully', async () => { const mainDoc = { - name: 'form integration test 2', + name: 'lead integration test 2', brandId: _brand2._id, - formId: _form2._id, + formId: _form._id, + kind: KIND_CHOICES.LEAD, }; - const formData = { - loadType: FORM_LOAD_TYPES.SHOUTBOX, + const leadData = { + loadType: LEAD_LOAD_TYPES.SHOUTBOX, }; - const integration = await Integrations.updateFormIntegration(_formIntegration._id, { + const integration = await Integrations.updateLeadIntegration(_leadIntegration._id, { ...mainDoc, - formData, + leadData, }); - if (!integration || !integration.formData) { + if (!integration || !integration.leadData) { throw new Error('Integration not found'); } expect(integration.name).toEqual(mainDoc.name); - expect(integration.formId).toEqual(_form2._id); + expect(integration.formId).toEqual(_form._id); expect(integration.brandId).toEqual(_brand2._id); - expect(integration.formData.loadType).toEqual(FORM_LOAD_TYPES.SHOUTBOX); + expect(integration.leadData.loadType).toEqual(LEAD_LOAD_TYPES.SHOUTBOX); + + const integrationNoLeadData = await Integrations.updateLeadIntegration(_leadIntegration._id, { + ...mainDoc, + }); + + expect(integrationNoLeadData.leadData && integrationNoLeadData.leadData.adminEmails).toHaveLength(0); + expect(integrationNoLeadData.leadData && integrationNoLeadData.leadData.rules).toHaveLength(0); }); }); @@ -221,15 +343,15 @@ describe('remove integration model method test', () => { beforeEach(async () => { _brand = await brandFactory({}); + _form = await formFactory(); - _form = await formFactory({}); await fieldFactory({ contentType: 'form', contentTypeId: _form._id }); _integration = await integrationFactory({ - name: 'form integration test', + name: 'lead integration test', brandId: _brand._id, formId: _form._id, - kind: 'form', + kind: 'lead', }); _conversation = await conversationFactory({ @@ -238,24 +360,36 @@ describe('remove integration model method test', () => { await conversationMessageFactory({ conversationId: _conversation._id }); await conversationMessageFactory({ conversationId: _conversation._id }); + + await customerFactory({ integrationId: _integration._id }); }); afterEach(async () => { await Brands.deleteMany({}); await Integrations.deleteMany({}); await Users.deleteMany({}); - await ConversationMessages.deleteMany({}); await Forms.deleteMany({}); - await Fields.deleteMany({}); + await ConversationMessages.deleteMany({}); }); - test('test if remove form integration model method is working successfully', async () => { + test('test if remove lead integration model method is working successfully', async () => { + try { + await Integrations.removeIntegration('fakeId'); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + await Integrations.removeIntegration(_integration._id); expect(await Integrations.find({}).countDocuments()).toEqual(0); expect(await ConversationMessages.find({}).countDocuments()).toBe(0); expect(await Forms.find({}).countDocuments()).toBe(0); - expect(await Fields.find({}).countDocuments()).toBe(0); + + const integrationNoForm = await integrationFactory(); + + await Integrations.removeIntegration(integrationNoForm._id); + + expect(await Integrations.find({}).countDocuments()).toEqual(0); }); }); @@ -322,17 +456,17 @@ describe('save integration messenger configurations test', () => { isOnline: false, onlineHours: [ { - day: 'Monday', - from: '8am', - to: '12pm', + day: 'monday', + from: '8:00 AM', + to: '12:00 PM', }, { - day: 'Monday', - from: '2pm', - to: '6pm', + day: 'monday', + from: '2:00 PM', + to: '6:00 PM', }, ], - timezone: 'CET', + timezone: momentTz.tz.guess(true), messages: { en: { welcome: 'Welcome user', @@ -373,14 +507,14 @@ describe('save integration messenger configurations test', () => { isOnline: true, onlineHours: [ { - day: 'Tuesday', - from: '9am', - to: '1pm', + day: 'tuesday', + from: '9:00 AM', + to: '1:00 PM', }, { - day: 'Tuesday', - from: '3pm', - to: '7pm', + day: 'tuesday', + from: '3:00 PM', + to: '7:00 PM', }, ], timezone: 'EET', @@ -413,4 +547,217 @@ describe('save integration messenger configurations test', () => { expect(integration.messengerData.messages.en.away).toEqual(messengerData.messages.en.away); expect(integration.messengerData.messages.en.thank).toEqual(messengerData.messages.en.thank); }); + + test('Increase view count of lead', async () => { + expect.assertions(2); + + let updated = await Integrations.increaseViewCount(_integration.formId, true); + + expect(updated.leadData && updated.leadData.viewCount).toBe(1); + + updated = await Integrations.increaseViewCount(_integration.formId, true); + expect(updated.leadData && updated.leadData.viewCount).toBe(2); + }); + + test('Increase contacts gathered', async () => { + expect.assertions(2); + + let updated = await Integrations.increaseContactsGathered(_integration.formId, true); + + expect(updated.leadData && updated.leadData.contactsGathered).toBe(1); + + updated = await Integrations.increaseContactsGathered(_integration.formId, true); + expect(updated.leadData && updated.leadData.contactsGathered).toBe(2); + }); + + describe('Manual mode', () => { + test('empty', async () => { + const integration = await integrationFactory({}); + expect(Integrations.isOnline(integration)).toBeFalsy(); + }); + + test('isOnline() must return status as it is', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'manual', + }, + }); + + if (!integration.messengerData) { + throw new Error('Messenger data undefined'); + } + + // online + integration.messengerData.isOnline = true; + expect(Integrations.isOnline(integration)).toBeTruthy(); + + // offline + integration.messengerData.isOnline = false; + expect(Integrations.isOnline(integration)).toBeFalsy(); + }); + }); + + describe('Auto mode', () => { + test('isTimeInBetween()', () => { + const time1 = '9:00 AM'; + const time2 = '6:00 PM'; + const timezone = momentTz.tz.guess(true); + + expect(isTimeInBetween(timezone, new Date('2017/05/08 11:10 AM'), time1, time2)).toBeTruthy(); + expect(isTimeInBetween(timezone, new Date('2017/05/08 7:00 PM'), time1, time2)).toBeFalsy(); + }); + + test('isOnline() must return false if there is no config for current day', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'auto', + onlineHours: [ + { + day: 'tuesday', + from: '9:00 AM', + to: '5:00 PM', + }, + ], + }, + }); + + // 2017-05-08, monday + expect(Integrations.isOnline(integration, new Date('2017/05/08 11:10 AM'))).toBeFalsy(); + }); + + test('isOnline() for specific day', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'auto', + onlineHours: [ + { + day: 'tuesday', + from: '9:00 AM', + to: '5:00 PM', + }, + ], + }, + }); + + // 2017-05-09, tuesday + expect(Integrations.isOnline(integration, new Date('2017/05/09 06:10 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/09 09:01 AM'))).toBeTruthy(); + }); + + test('isOnline() for everyday', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'auto', + onlineHours: [ + { + day: 'everyday', + from: '9:00 AM', + to: '5:00 PM', + }, + ], + timezone: momentTz.tz.guess(true), + }, + }); + + // monday -> sunday + expect(Integrations.isOnline(integration, new Date('2017/05/08 10:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/09 11:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/10 12:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/11 1:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/12 2:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/13 3:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/14 3:30 PM'))).toBeTruthy(); + + // monday -> sunday + expect(Integrations.isOnline(integration, new Date('2017/05/08 1:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/09 4:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/10 5:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/11 6:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/12 6:00 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/13 7:00 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/14 8:00 PM'))).toBeFalsy(); + }); + + test('isOnline() for weekdays', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'auto', + onlineHours: [ + { + day: 'weekdays', + from: '9:00 AM', + to: '5:00 PM', + }, + ], + timezone: momentTz.tz.guess(true), + }, + }); + + // weekdays + expect(Integrations.isOnline(integration, new Date('2017/05/08 10:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/09 11:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/10 12:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/11 1:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/12 2:00 PM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/11 11:00 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/12 8:00 AM'))).toBeFalsy(); + + // weekend + expect(Integrations.isOnline(integration, new Date('2017/05/13 10:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/14 11:00 AM'))).toBeFalsy(); + }); + + test('isOnline() for weekend', async () => { + const integration = await integrationFactory({ + messengerData: { + availabilityMethod: 'auto', + onlineHours: [ + { + day: 'weekends', + from: '9:00 AM', + to: '5:00 PM', + }, + ], + timezone: momentTz.tz.guess(true), + }, + }); + + // weekdays + expect(Integrations.isOnline(integration, new Date('2017/05/08 10:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/09 11:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/10 12:00 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/11 1:00 PM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/12 2:00 PM'))).toBeFalsy(); + + // weekend + expect(Integrations.isOnline(integration, new Date('2017/05/13 10:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/14 11:00 AM'))).toBeTruthy(); + expect(Integrations.isOnline(integration, new Date('2017/05/13 07:00 AM'))).toBeFalsy(); + expect(Integrations.isOnline(integration, new Date('2017/05/14 11:00 PM'))).toBeFalsy(); + }); + }); + + test('getWidgetIntegration', async () => { + expect.assertions(4); + + try { + await Integrations.getWidgetIntegration('_id', 'messenger'); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + + const brand = await brandFactory({}); + const integration = await integrationFactory({ brandId: brand._id, kind: 'messenger' }); + + // brandObject false + let response = await Integrations.getWidgetIntegration(brand.code || '', 'messenger'); + + expect(response._id).toBe(integration._id); + + // brandObject true + response = await Integrations.getWidgetIntegration(brand.code || '', 'messenger', true); + + expect(response.integration._id).toBe(integration._id); + expect(response.brand._id).toBe(brand._id); + }); }); diff --git a/src/__tests__/integrationMutations.test.ts b/src/__tests__/integrationMutations.test.ts index bc345cb9c..fbee6aa26 100644 --- a/src/__tests__/integrationMutations.test.ts +++ b/src/__tests__/integrationMutations.test.ts @@ -1,15 +1,28 @@ +import './setup.ts'; + import * as faker from 'faker'; +import messageBroker from '../messageBroker'; + +import * as utils from '../data/utils'; +import { + brandFactory, + customerFactory, + formFactory, + integrationFactory, + tagsFactory, + userFactory, +} from '../db/factories'; +import { Brands, Customers, EmailDeliveries, Integrations, Users } from '../db/models'; + +import { IntegrationsAPI } from '../data/dataSources'; import { graphqlRequest } from '../db/connection'; -import { brandFactory, integrationFactory, userFactory } from '../db/factories'; -import { Brands, Integrations, Users } from '../db/models'; - -import './setup.ts'; +import { KIND_CHOICES } from '../db/models/definitions/constants'; describe('mutations', () => { let _integration; let _brand; - let _user; - let context; + let tag; + let form; const commonParamDefs = ` $name: String! @@ -23,7 +36,7 @@ describe('mutations', () => { languageCode: $languageCode `; - const commonFormProperties = { + const commonLeadProperties = { languageCode: 'en', loadType: faker.random.word(), fromEmail: faker.internet.email(), @@ -33,38 +46,46 @@ describe('mutations', () => { adminEmailContent: faker.random.word(), redirectUrl: faker.random.word(), successAction: faker.random.word(), - formData: { + leadData: { thankContent: faker.random.word(), adminEmails: [], }, }; + let dataSources; + beforeEach(async () => { + dataSources = { IntegrationsAPI: new IntegrationsAPI() }; + // Creating test data - _user = await userFactory({}); - _integration = await integrationFactory({}); _brand = await brandFactory({}); - - context = { user: _user }; + tag = await tagsFactory(); + form = await formFactory(); + _integration = await integrationFactory({ brandId: _brand._id, formId: form._id, tagIds: [tag._id] }); }); afterEach(async () => { // Clearing test data await Users.deleteMany({}); await Brands.deleteMany({}); + await Customers.deleteMany({}); + await EmailDeliveries.deleteMany({}); await Integrations.deleteMany({}); }); test('Create messenger integration', async () => { + await Integrations.remove({}); + const args = { - name: _integration.name, + name: 'Integration Name', brandId: _brand._id, languageCode: 'en', + channelIds: ['randomId'], }; const mutation = ` - mutation integrationsCreateMessengerIntegration(${commonParamDefs}) { - integrationsCreateMessengerIntegration(${commonParams}) { + mutation integrationsCreateMessengerIntegration($channelIds: [String] ${commonParamDefs}) { + integrationsCreateMessengerIntegration(channelIds: $channelIds ${commonParams}) { name brandId languageCode @@ -72,7 +93,7 @@ describe('mutations', () => { } `; - const integration = await graphqlRequest(mutation, 'integrationsCreateMessengerIntegration', args, context); + const integration = await graphqlRequest(mutation, 'integrationsCreateMessengerIntegration', args); expect(integration.name).toBe(args.name); expect(integration.brandId).toBe(args.brandId); @@ -80,20 +101,25 @@ describe('mutations', () => { }); test('Edit messenger integration', async () => { + const secondBrand = await brandFactory(); + const args = { _id: _integration._id, name: _integration.name, - brandId: _brand._id, + brandId: secondBrand._id, languageCode: 'en', + channelIds: ['randomId'], }; const mutation = ` mutation integrationsEditMessengerIntegration( $_id: String! + $channelIds: [String] ${commonParamDefs} ) { integrationsEditMessengerIntegration( _id: $_id + channelIds: $channelIds ${commonParams} ) { _id @@ -104,7 +130,7 @@ describe('mutations', () => { } `; - const integration = await graphqlRequest(mutation, 'integrationsEditMessengerIntegration', args, context); + const integration = await graphqlRequest(mutation, 'integrationsEditMessengerIntegration', args); expect(integration._id).toBe(args._id); expect(integration.name).toBe(args.name); @@ -136,26 +162,24 @@ describe('mutations', () => { } `; - const messengerAppearanceData = await graphqlRequest( - mutation, - 'integrationsSaveMessengerAppearanceData', - args, - context, - ); + const messengerAppearanceData = await graphqlRequest(mutation, 'integrationsSaveMessengerAppearanceData', args); expect(messengerAppearanceData._id).toBe(args._id); expect(messengerAppearanceData.uiOptions.toJSON()).toEqual(args.uiOptions); }); test('Save messenger integration config', async () => { + const user = await userFactory({}); + const messengerData = { - supporterIds: [_user.id], + supporterIds: [user.id], notifyCustomer: false, isOnline: false, availabilityMethod: 'auto', requireAuth: false, showChat: false, showLauncher: false, + showVideoCallRequest: false, forceLogoutWhenResolve: false, onlineHours: [ { @@ -194,69 +218,72 @@ describe('mutations', () => { } `; - const messengerConfig = await graphqlRequest(mutation, 'integrationsSaveMessengerConfigs', args, context); + const messengerConfig = await graphqlRequest(mutation, 'integrationsSaveMessengerConfigs', args); expect(messengerConfig._id).toBe(args._id); expect(messengerConfig.messengerData.toJSON()).toEqual(args.messengerData); }); - test('Create form integration', async () => { + test('Create lead integration', async () => { + const leadIntegration = await integrationFactory({ formId: 'formId', kind: 'lead' }); + const args = { - name: _integration.name, + name: leadIntegration.name, brandId: _brand._id, - formId: _integration.formId, - ...commonFormProperties, + formId: leadIntegration.formId, + ...commonLeadProperties, }; const mutation = ` - mutation integrationsCreateFormIntegration( + mutation integrationsCreateLeadIntegration( ${commonParamDefs} $formId: String! - $formData: IntegrationFormData! + $leadData: IntegrationLeadData! ) { - integrationsCreateFormIntegration( + integrationsCreateLeadIntegration( ${commonParams} formId: $formId - formData: $formData + leadData: $leadData ) { name brandId languageCode formId - formData + leadData } } `; - const formIntegration = await graphqlRequest(mutation, 'integrationsCreateFormIntegration', args, context); + const response = await graphqlRequest(mutation, 'integrationsCreateLeadIntegration', args); - expect(formIntegration.name).toBe(args.name); - expect(formIntegration.brandId).toBe(args.brandId); - expect(formIntegration.languageCode).toBe(args.languageCode); - expect(formIntegration.formId).toBe(args.formId); - expect(formIntegration.formData.toJSON()).toEqual(args.formData); + expect(response.name).toBe(args.name); + expect(response.brandId).toBe(args.brandId); + expect(response.languageCode).toBe(args.languageCode); + expect(response.formId).toBe(args.formId); }); - test('Edit form integration', async () => { + test('Edit lead integration', async () => { + const leadIntegration = await integrationFactory({ formId: 'formId', kind: 'lead' }); + const args = { - _id: _integration._id, - name: _integration.name, + _id: leadIntegration._id, + name: leadIntegration.name, brandId: _brand._id, - formId: _integration.formId, - ...commonFormProperties, + formId: leadIntegration.formId, + ...commonLeadProperties, }; const mutation = ` - mutation integrationsEditFormIntegration( + mutation integrationsEditLeadIntegration( $_id: String! $formId: String! - $formData: IntegrationFormData! + $leadData: IntegrationLeadData! ${commonParamDefs} ) { - integrationsEditFormIntegration( + integrationsEditLeadIntegration( _id: $_id formId: $formId - formData: $formData + leadData: $leadData ${commonParams} ) { _id @@ -264,18 +291,362 @@ describe('mutations', () => { brandId languageCode formId - formData + leadData } } `; - const formIntegration = await graphqlRequest(mutation, 'integrationsEditFormIntegration', args, context); + const response = await graphqlRequest(mutation, 'integrationsEditLeadIntegration', args); + + expect(response._id).toBe(args._id); + expect(response.name).toBe(args.name); + expect(response.brandId).toBe(args.brandId); + expect(response.languageCode).toBe(args.languageCode); + expect(response.formId).toBe(args.formId); + }); + + test('Create external integration', async () => { + const mutation = ` + mutation integrationsCreateExternalIntegration( + $kind: String! + $name: String! + $brandId: String! + $accountId: String, + $data: JSON + $channelIds: [String] + ) { + integrationsCreateExternalIntegration( + kind: $kind + name: $name + brandId: $brandId + accountId: $accountId + data: $data + channelIds: $channelIds + ) { + _id + name + kind + brandId + } + } + `; + + const brand = await brandFactory(); + + const args: any = { + kind: 'nylas-gmail', + name: 'Nyals gmail integration', + brandId: brand._id, + channelIds: ['randomId'], + }; + + try { + await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Error: Integrations api is not running'); + } + + args.kind = 'facebook-post'; + + const createIntegrationSpy = jest.spyOn(dataSources.IntegrationsAPI, 'createIntegration'); + createIntegrationSpy.mockImplementation(() => Promise.resolve()); + + await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + + args.kind = 'twitter-dm'; + args.data = { data: 'data' }; + + await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + + args.kind = 'smooch-viber'; + args.data = { data: 'data' }; + + await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + + const response = await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + + expect(response).toBeDefined(); + + args.kind = 'webhook'; + args.data = { data: 'data' }; + + await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { dataSources }); + + const webhookResponse = await graphqlRequest(mutation, 'integrationsCreateExternalIntegration', args, { + dataSources, + }); + + expect(webhookResponse).toBeDefined(); + + createIntegrationSpy.mockRestore(); + }); + + test('Update config', async () => { + const mutation = ` + mutation integrationsUpdateConfigs($configsMap: JSON!) { + integrationsUpdateConfigs(configsMap: $configsMap) + } + `; + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'updateConfigs'); + spy.mockImplementation(() => Promise.resolve()); + + await graphqlRequest( + mutation, + 'integrationsUpdateConfigs', + { configsMap: { FACEBOOK_TOKEN: 'token' } }, + { dataSources }, + ); + + spy.mockRestore(); + }); + + test('Remove account', async () => { + const mutation = ` + mutation integrationsRemoveAccount($_id: String!) { + integrationsRemoveAccount(_id: $_id) + } + `; + + const integration1 = await integrationFactory(); + + const spy = jest.spyOn(messageBroker(), 'sendRPCMessage'); + spy.mockImplementation(() => Promise.resolve({ erxesApiIds: [integration1._id] })); + + const response = await graphqlRequest(mutation, 'integrationsRemoveAccount', { _id: 'accountId' }); + + try { + await graphqlRequest(mutation, 'integrationsRemoveAccount', { _id: 'accountId' }); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + + expect(response).toBe('success'); + + spy.mockRestore(); + + const spy1 = jest.spyOn(messageBroker(), 'sendRPCMessage'); + + spy1.mockImplementation(() => Promise.resolve({ erxesApiIds: [] })); + + const secondResponse = await graphqlRequest(mutation, 'integrationsRemoveAccount', { _id: 'accountId' }); + + expect(secondResponse).toBe('success'); + + spy1.mockRestore(); + }); + + test('Send mail', async () => { + const mutation = ` + mutation integrationSendMail( + $erxesApiId: String! + $subject: String! + $to: [String]! + $cc: [String] + $bcc: [String] + $from: String! + $kind: String + $customerId: String + ) { + integrationSendMail( + erxesApiId: $erxesApiId + subject: $subject + to: $to + cc: $cc + bcc: $bcc + from: $from + kind: $kind + customerId: $customerId + ) + } + `; + + const customer = await customerFactory({ primaryEmail: 'user@mail.com' }); + + const args = { + erxesApiId: 'erxesApiId', + subject: 'Subject', + to: ['user@mail.com'], + cc: ['cc'], + bcc: ['bcc'], + from: 'from', + kind: 'nylas-gmail', + body: 'body', + }; + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'sendEmail'); + const mockReplaceEditorAttribute = jest.spyOn(utils, 'replaceEditorAttributes'); + + mockReplaceEditorAttribute.mockImplementation(() => + Promise.resolve({ + replacedContent: 'replacedContent', + replacers: [{ key: 'key', value: 'value' }], + }), + ); + + spy.mockImplementation(() => Promise.resolve()); + + await graphqlRequest(mutation, 'integrationSendMail', args, { dataSources }); + + const emailDelivery = await EmailDeliveries.findOne({ customerId: customer._id }); + + if (emailDelivery) { + expect(JSON.stringify(emailDelivery.to)).toEqual(JSON.stringify(args.to)); + expect(customer._id).toEqual(emailDelivery.customerId); + } + + spy.mockRestore(); + + try { + await graphqlRequest(mutation, 'integrationSendMail', args, { dataSources }); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + + mockReplaceEditorAttribute.mockRestore(); + }); + + test('Integrations remove', async () => { + const mutation = ` + mutation integrationsRemove($_id: String!) { + integrationsRemove(_id: $_id) + } + `; + + const messengerIntegration = await integrationFactory({ kind: 'messenger', formId: form._id, tagIds: [tag._id] }); + + const removeSpy = jest.spyOn(dataSources.IntegrationsAPI, 'removeIntegration'); + removeSpy.mockImplementation(() => Promise.resolve()); + + await graphqlRequest(mutation, 'integrationsRemove', { + _id: messengerIntegration._id, + }); + + expect(await Integrations.findOne({ _id: messengerIntegration._id })).toBe(null); + + const facebookPostIntegration = await integrationFactory({ kind: 'facebook-post' }); + + await graphqlRequest( + mutation, + 'integrationsRemove', + { + _id: facebookPostIntegration._id, + }, + { + dataSources, + }, + ); + + removeSpy.mockRestore(); + }); + + test('test integrationsRemove() to catch error', async () => { + const mutation = ` + mutation integrationsRemove($_id: String!) { + integrationsRemove(_id: $_id) + } + `; + + const fbPostIntegration = await integrationFactory({ kind: 'facebook-post' }); + + try { + await graphqlRequest(mutation, 'integrationsRemove', { _id: fbPostIntegration._id }); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + }); + + test('Integrations archive', async () => { + const mutation = ` + mutation integrationsArchive($_id: String!, $status: Boolean!) { + integrationsArchive(_id: $_id, status: $status) { + _id + isActive + } + } + `; + + const integration = await integrationFactory(); + let response = await graphqlRequest(mutation, 'integrationsArchive', { + _id: integration._id, + status: true, + }); + + expect(response.isActive).toBeFalsy(); + + response = await graphqlRequest(mutation, 'integrationsArchive', { _id: integration._id, status: false }); + + expect(response.isActive).toBeTruthy(); + }); + + test('Integrations edit common fields', async () => { + const mutation = ` + mutation integrationsEditCommonFields($_id: String!, $name: String!, $brandId: String!, $channelIds: [String], $data: JSON) { + integrationsEditCommonFields(_id: $_id name: $name brandId: $brandId channelIds: $channelIds data: $data) { + _id + name + brandId + webhookData + } + } + `; + + const integration = await integrationFactory(); + + const doc: any = { + _id: integration._id, + name: 'updated', + brandId: 'brandId', + channelIds: ['randomId'], + }; + + const response = await graphqlRequest(mutation, 'integrationsEditCommonFields', doc); + + expect(response._id).toBe(doc._id); + expect(response.name).toBe(doc.name); + expect(response.brandId).toBe(doc.brandId); + + const webhookIntegration = await integrationFactory({ kind: KIND_CHOICES.WEBHOOK }); + + doc._id = webhookIntegration._id; + doc.data = { + script: 'script', + }; + + const webhookResponse = await graphqlRequest(mutation, 'integrationsEditCommonFields', doc); + + expect(webhookResponse).toBeDefined(); + }); + + test('test integrationsSendSms()', async () => { + const mutation = ` + mutation integrationsSendSms( + $integrationId: String! + $content: String! + $to: String! + ) { + integrationsSendSms( + integrationId: $integrationId + content: $content + to: $to + ) + } + `; + + const args = { + integrationId: 'integrationId', + content: 'Hello', + to: '+976123456789', + }; + + const spy = jest.spyOn(dataSources.IntegrationsAPI, 'sendSms'); + + spy.mockImplementation(() => Promise.resolve({ status: 'ok' })); + + const response = await graphqlRequest(mutation, 'integrationsSendSms', args, { dataSources }); + + expect(response.status).toBe('ok'); - expect(formIntegration._id).toBe(args._id); - expect(formIntegration.name).toBe(args.name); - expect(formIntegration.brandId).toBe(args.brandId); - expect(formIntegration.languageCode).toBe(args.languageCode); - expect(formIntegration.formId).toBe(args.formId); - expect(formIntegration.formData.toJSON()).toEqual(args.formData); + spy.mockRestore(); }); }); diff --git a/src/__tests__/integrationQueries.test.ts b/src/__tests__/integrationQueries.test.ts index d683772d2..046ade85e 100644 --- a/src/__tests__/integrationQueries.test.ts +++ b/src/__tests__/integrationQueries.test.ts @@ -1,10 +1,14 @@ +import './setup.ts'; + import * as faker from 'faker'; -import { graphqlRequest } from '../db/connection'; +import messageBroker from '../messageBroker'; + import { brandFactory, channelFactory, integrationFactory, tagsFactory } from '../db/factories'; -import { Brands, Channels, Integrations } from '../db/models'; -import { TAG_TYPES } from '../db/models/definitions/constants'; +import { Brands, Channels, Integrations, Tags } from '../db/models'; -import './setup.ts'; +import { IntegrationsAPI } from '../data/dataSources'; +import { graphqlRequest } from '../db/connection'; +import { TAG_TYPES } from '../db/models/definitions/constants'; describe('integrationQueries', () => { const qryIntegrations = ` @@ -27,19 +31,6 @@ describe('integrationQueries', () => { tag: $tag ) { _id - kind - name - brandId - languageCode - code - formId - formData - messengerData - uiOptions - - brand { _id } - form { _id } - channels { _id } } } `; @@ -57,12 +48,14 @@ describe('integrationQueries', () => { `; const name = faker && faker.random ? faker.random.word() : 'anonymous'; + const dataSources = { IntegrationsAPI: new IntegrationsAPI() }; afterEach(async () => { // Clearing test data await Integrations.deleteMany({}); await Channels.deleteMany({}); await Brands.deleteMany({}); + await Tags.deleteMany({}); }); test('Integrations', async () => { @@ -98,7 +91,7 @@ describe('integrationQueries', () => { test('Integrations filtered by kind', async () => { await integrationFactory({ kind: 'messenger' }); - await integrationFactory({ kind: 'form' }); + await integrationFactory({ kind: 'lead' }); // messenger ======================== let responses = await graphqlRequest(qryIntegrations, 'integrations', { @@ -107,19 +100,31 @@ describe('integrationQueries', () => { expect(responses.length).toBe(1); - // form ========================= + // lead ========================= responses = await graphqlRequest(qryIntegrations, 'integrations', { - kind: 'form', + kind: 'lead', }); expect(responses.length).toBe(1); }); + test('Integrations filtered by mail', async () => { + await integrationFactory({ kind: 'gmail' }); + await integrationFactory({ kind: 'nylas-gmail' }); + + // mail ======================== + const responses = await graphqlRequest(qryIntegrations, 'integrations', { + kind: 'mail', + }); + + expect(responses.length).toBe(2); + }); + test('Integrations filtered by channel', async () => { - const integration1 = await integrationFactory({ kind: 'facebook' }); - const integration2 = await integrationFactory({ kind: 'facebook' }); + const integration1 = await integrationFactory({ kind: 'facebook-messenger' }); + const integration2 = await integrationFactory({ kind: 'facebook-messenger' }); - await integrationFactory({ kind: 'facebook' }); + await integrationFactory({ kind: 'facebook-messenger' }); const integrationIds = [integration1._id, integration2._id]; @@ -136,8 +141,8 @@ describe('integrationQueries', () => { const brand = await brandFactory(); await integrationFactory({ kind: 'messenger', brandId: brand._id }); - await integrationFactory({ kind: 'form', brandId: brand._id }); - await integrationFactory({ kind: 'form' }); + await integrationFactory({ kind: 'lead', brandId: brand._id }); + await integrationFactory({ kind: 'lead', brandId: 'fakeId' }); const responses = await graphqlRequest(qryIntegrations, 'integrations', { brandId: brand._id, @@ -159,36 +164,76 @@ describe('integrationQueries', () => { }); test('Integration detail', async () => { - const integration = await integrationFactory(); - const qry = ` query integrationDetail($_id: String!) { integrationDetail(_id: $_id) { _id + kind + name + brandId + languageCode + code + formId + leadData + messengerData + uiOptions + + brand { _id } + form { _id } + channels { _id } + tags { _id } + + websiteMessengerApps { _id } + knowledgeBaseMessengerApps { _id } + leadMessengerApps { _id } } } `; - const response = await graphqlRequest(qry, 'integrationDetail', { - _id: integration._id, + const tag = await tagsFactory(); + const messengerIntegration = await integrationFactory({ tagIds: [tag._id], brandId: 'fakeId' }); + + let response = await graphqlRequest( + qry, + 'integrationDetail', + { + _id: messengerIntegration._id, + }, + { dataSources }, + ); + + expect(response._id).toBe(messengerIntegration._id); + expect(response.tags.length).toBe(1); + expect(response.websiteMessengerApps.length).toBe(0); + expect(response.knowledgeBaseMessengerApps.length).toBe(0); + expect(response.leadMessengerApps.length).toBe(0); + + const leadIntegration = await integrationFactory({ kind: 'lead' }); + + response = await graphqlRequest(qry, 'integrationDetail', { + _id: leadIntegration._id, }); - expect(response._id).toBe(integration._id); + expect(response._id).toBe(leadIntegration._id); + expect(response.tags.length).toBe(0); + expect(response.websiteMessengerApps.length).toBe(0); + expect(response.knowledgeBaseMessengerApps.length).toBe(0); + expect(response.leadMessengerApps.length).toBe(0); }); test('Get total count of integrations by kind', async () => { await integrationFactory({ kind: 'messenger' }); - await integrationFactory({ kind: 'form' }); + await integrationFactory({ kind: 'lead' }); // messenger ========================= let response = await graphqlRequest(qryCount, 'integrationsTotalCount', {}); expect(response.byKind.messenger).toBe(1); - // form ============================= + // lead ============================= response = await graphqlRequest(qryCount, 'integrationsTotalCount', {}); - expect(response.byKind.form).toBe(1); + expect(response.byKind.lead).toBe(1); }); test('Get total count of integrations by channel', async () => { @@ -210,11 +255,101 @@ describe('integrationQueries', () => { const brand = await brandFactory(); await integrationFactory({ kind: 'messenger', brandId: brand._id }); - await integrationFactory({ kind: 'form', brandId: brand._id }); - await integrationFactory({ kind: 'form' }); + await integrationFactory({ kind: 'lead', brandId: brand._id }); + await integrationFactory({ kind: 'lead' }); const response = await graphqlRequest(qryCount, 'integrationsTotalCount', {}); expect(response.byBrand[brand._id]).toBe(2); }); + + test('Get total count of integrations by tag', async () => { + await integrationFactory({}); + await integrationFactory({}); + await integrationFactory({}); + + const tagObj = await tagsFactory({ type: TAG_TYPES.INTEGRATION }); + await integrationFactory({ tagIds: [tagObj._id] }); + + const responses = await graphqlRequest(qryCount, 'integrationsTotalCount'); + + expect(responses.byTag[tagObj._id]).toBe(1); + }); + + test('Fetch integration api', async () => { + const qry = ` + query integrationsFetchApi($path: String!, $params: JSON!) { + integrationsFetchApi(path: $path, params: $params) + } + `; + + try { + await graphqlRequest(qry, 'integrationsFetchApi', { path: '/', params: { type: 'facebook' } }, { dataSources }); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + + try { + await graphqlRequest( + qry, + 'integrationsFetchApi', + { path: '/integrations', params: { type: 'facebook' } }, + { dataSources }, + ); + } catch (e) { + expect(e[0].message).toBe('Integrations api is not running'); + } + }); + + test('Get used types', async () => { + const qry = ` + query integrationsGetUsedTypes { + integrationsGetUsedTypes { + _id + name + } + } + `; + + await integrationFactory({ kind: 'messenger' }); + + const usedTypes = await graphqlRequest(qry, 'integrationsGetUsedTypes'); + + expect(usedTypes[0]._id).toBe('messenger'); + expect(usedTypes[0].name).toBe('Web messenger'); + }); + + test('line webhook', async () => { + const query = ` + query integrationGetLineWebhookUrl($_id: String!) { + integrationGetLineWebhookUrl(_id: $_id) + } + `; + + const spy = jest.spyOn(messageBroker(), 'sendRPCMessage'); + + spy.mockImplementation(() => Promise.resolve('https://webhookurl')); + + const response = await graphqlRequest(query, 'integrationGetLineWebhookUrl', { _id: 'id' }); + + try { + await graphqlRequest(query, 'integrationGetLineWebhookUrl', { _id: 'id' }); + } catch (e) { + expect(e[0].message).toBeDefined(); + } + + expect(response).toBe('https://webhookurl'); + + spy.mockRestore(); + + const spy1 = jest.spyOn(messageBroker(), 'sendRPCMessage'); + + spy1.mockImplementation(() => Promise.resolve('https://webhookurl')); + + const secondResponse = await graphqlRequest(query, 'integrationGetLineWebhookUrl', { _id: 'id' }); + + expect(secondResponse).toBe('https://webhookurl'); + + spy1.mockRestore(); + }); }); diff --git a/src/__tests__/internalNoteDb.test.ts b/src/__tests__/internalNoteDb.test.ts index 60b0a26b2..9f35d378a 100644 --- a/src/__tests__/internalNoteDb.test.ts +++ b/src/__tests__/internalNoteDb.test.ts @@ -39,6 +39,18 @@ describe('InternalNotes model test', () => { await Users.deleteMany({}); }); + test('Get internal note', async () => { + try { + await InternalNotes.getInternalNote('fakeId'); + } catch (e) { + expect(e.message).toBe('Internal note not found'); + } + + const response = await InternalNotes.getInternalNote(_internalNote._id); + + expect(response).toBeDefined(); + }); + test('Create internalNote', async () => { // valid const doc = generateData(); @@ -113,7 +125,12 @@ describe('InternalNotes model test', () => { contentTypeId: customer._id, }); - await InternalNotes.removeCustomerInternalNotes(customer._id); + await internalNoteFactory({ + contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, + contentTypeId: customer._id, + }); + + await InternalNotes.removeCustomersInternalNotes([customer._id]); const internalNote = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, @@ -131,13 +148,18 @@ describe('InternalNotes model test', () => { contentTypeId: company._id, }); - await InternalNotes.removeCompanyInternalNotes(company._id); + await internalNoteFactory({ + contentType: ACTIVITY_CONTENT_TYPES.COMPANY, + contentTypeId: company._id, + }); - const internalNote = await InternalNotes.find({ + await InternalNotes.removeCompaniesInternalNotes([company._id]); + + const internalNotes = await InternalNotes.find({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, contentTypeId: company._id, }); - expect(internalNote).toHaveLength(0); + expect(internalNotes).toHaveLength(0); }); }); diff --git a/src/__tests__/internalNoteMutations.test.ts b/src/__tests__/internalNoteMutations.test.ts index 0cfdfc690..1de79f50b 100644 --- a/src/__tests__/internalNoteMutations.test.ts +++ b/src/__tests__/internalNoteMutations.test.ts @@ -1,18 +1,53 @@ +import { MODULE_NAMES } from '../data/constants'; import { graphqlRequest } from '../db/connection'; -import { internalNoteFactory, userFactory } from '../db/factories'; -import { InternalNotes, Users } from '../db/models'; - +import { + companyFactory, + customerFactory, + dealFactory, + growthHackFactory, + internalNoteFactory, + notificationConfigurationFactory, + productFactory, + taskFactory, + ticketFactory, + userFactory, +} from '../db/factories'; +import { InternalNotes, Notifications, Users } from '../db/models'; +import { NOTIFICATION_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; +const checkContentType = (target, src) => { + expect(src.contentType).toBe(target.contentType); + expect(src.contentTypeId).toBe(target.contentTypeId); +}; + describe('InternalNotes mutations', () => { let _user; - let _internalNote; let context; + const addMutation = ` + mutation internalNotesAdd( + $contentType: String! + $contentTypeId: String + $content: String + $mentionedUserIds: [String] + ) { + internalNotesAdd( + contentType: $contentType + contentTypeId: $contentTypeId + content: $content + mentionedUserIds: $mentionedUserIds + ) { + contentType + contentTypeId + content + } + } + `; + beforeEach(async () => { // Creating test data _user = await userFactory({}); - _internalNote = await internalNoteFactory({}); context = { user: _user }; }); @@ -24,37 +59,140 @@ describe('InternalNotes mutations', () => { }); test('Add internal note', async () => { - const { contentType, contentTypeId, content } = _internalNote; - const args = { contentType, contentTypeId, content }; + await notificationConfigurationFactory({ + isAllowed: true, + user: _user, + notifType: NOTIFICATION_TYPES.CUSTOMER_MENTION, + }); - const mutation = ` - mutation internalNotesAdd( - $contentType: String! - $contentTypeId: String - $content: String - ) { - internalNotesAdd( - contentType: $contentType - contentTypeId: $contentTypeId - content: $content - ) { - contentType - contentTypeId - content - } - } - `; + // add internalNote on deal + const mentionedUser = await userFactory({}); + const watchedUser = await userFactory({}); + const assignedUser = await userFactory({}); + const modifiedUser = await userFactory({}); + + if (!mentionedUser || !mentionedUser.details) { + throw new Error('User not found'); + } + + const deal = await dealFactory({ + watchedUserIds: [watchedUser._id], + assignedUserIds: [assignedUser._id], + modifiedBy: modifiedUser._id, + }); + + let notification = await Notifications.findOne({ receiver: _user._id }); + + await notificationConfigurationFactory({ isAllowed: true, user: _user, notifType: NOTIFICATION_TYPES.DEAL_EDIT }); + + const args: any = { + contentType: MODULE_NAMES.DEAL, + contentTypeId: deal._id, + content: `@${mentionedUser.details.fullName}`, + mentionedUserIds: mentionedUser._id, + }; + + let internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + notification = await Notifications.findOne({ receiver: assignedUser._id }); + expect(notification).toBeDefined(); + + notification = await Notifications.findOne({ receiver: _user._id }); + expect(notification).toBeDefined(); + + notification = await Notifications.findOne({ receiver: assignedUser._id }); + expect(notification).toBeDefined(); + + expect(internalNote.content).toBe(args.content); + checkContentType(internalNote, args); + + // task + const task = await taskFactory(); + + args.contentType = MODULE_NAMES.TASK; + args.contentTypeId = task._id; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + + // ticket + const ticket = await ticketFactory(); - const internalNote = await graphqlRequest(mutation, 'internalNotesAdd', args, context); + args.contentType = MODULE_NAMES.TICKET; + args.contentTypeId = ticket._id; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + + // company + const company = await companyFactory(); + + args.contentType = MODULE_NAMES.COMPANY; + args.contentTypeId = company._id; + args.mentionedUserIds = undefined; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + + // growthHack + const hack = await growthHackFactory(); + + args.contentType = MODULE_NAMES.GROWTH_HACK; + args.contentTypeId = hack._id; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + + // user + const user = await userFactory(); + + args.contentType = MODULE_NAMES.USER; + args.contentTypeId = user._id; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + + // product + const product = await productFactory(); + + args.contentType = MODULE_NAMES.PRODUCT; + args.contentTypeId = product._id; + + internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); + + checkContentType(internalNote, args); + }); + + test('Add customer internal note', async () => { + const customer = await customerFactory({}); + + const args: any = { + contentType: MODULE_NAMES.CUSTOMER, + contentTypeId: customer._id, + content: `@${_user.details.fullName}`, + }; + + const internalNote = await graphqlRequest(addMutation, 'internalNotesAdd', args, context); - expect(internalNote.contentType).toBe(args.contentType); - expect(internalNote.contentTypeId).toBe(args.contentTypeId); expect(internalNote.content).toBe(args.content); + checkContentType(internalNote, args); + + const notification = await Notifications.findOne(args); + + expect(notification).toBeDefined(); }); test('Edit internal note', async () => { - const { _id, content } = _internalNote; - const args = { _id, content }; + const customer = await customerFactory(); + const deal = await dealFactory(); + + const note = await internalNoteFactory({ contentType: MODULE_NAMES.CUSTOMER, contentTypeId: customer._id }); + const dealNote = await internalNoteFactory({ contentType: MODULE_NAMES.DEAL, contentTypeId: deal._id }); const mutation = ` mutation internalNotesEdit( @@ -71,13 +209,44 @@ describe('InternalNotes mutations', () => { } `; - const internalNote = await graphqlRequest(mutation, 'internalNotesEdit', args, context); + const internalNote = await graphqlRequest( + mutation, + 'internalNotesEdit', + { _id: note._id, content: note.content }, + context, + ); - expect(internalNote._id).toBe(args._id); - expect(internalNote.content).toBe(args.content); + expect(internalNote._id).toBe(note._id); + expect(internalNote.content).toBe(note.content); + + const dealInternalNote = await graphqlRequest( + mutation, + 'internalNotesEdit', + { _id: dealNote._id, content: dealNote.content }, + context, + ); + + expect(dealInternalNote._id).toBe(dealNote._id); + expect(dealInternalNote.content).toBe(dealNote.content); }); test('Remove internal note', async () => { + // test different type of notes + const company = await companyFactory(); + const deal = await dealFactory(); + const task = await taskFactory(); + const ticket = await ticketFactory(); + const hack = await growthHackFactory(); + const product = await productFactory(); + + const note1 = await internalNoteFactory({ contentType: MODULE_NAMES.DEAL, contentTypeId: deal._id }); + const note2 = await internalNoteFactory({ contentType: MODULE_NAMES.COMPANY, contentTypeId: company._id }); + const note3 = await internalNoteFactory({ contentType: MODULE_NAMES.TASK, contentTypeId: task._id }); + const note4 = await internalNoteFactory({ contentType: MODULE_NAMES.TICKET, contentTypeId: ticket._id }); + const note5 = await internalNoteFactory({ contentType: MODULE_NAMES.GROWTH_HACK, contentTypeId: hack._id }); + const note6 = await internalNoteFactory({ contentType: MODULE_NAMES.USER, contentTypeId: _user._id }); + const note7 = await internalNoteFactory({ contentType: MODULE_NAMES.PRODUCT, contentTypeId: product._id }); + const mutation = ` mutation internalNotesRemove($_id: String!) { internalNotesRemove(_id: $_id) { @@ -86,8 +255,20 @@ describe('InternalNotes mutations', () => { } `; - await graphqlRequest(mutation, 'internalNotesRemove', { _id: _internalNote._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note1._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note2._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note3._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note4._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note5._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note6._id }, context); + await graphqlRequest(mutation, 'internalNotesRemove', { _id: note7._id }, context); - expect(await InternalNotes.findOne({ _id: _internalNote._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note1._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note2._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note3._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note4._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note5._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note6._id })).toBe(null); + expect(await InternalNotes.findOne({ _id: note7._id })).toBe(null); }); }); diff --git a/src/__tests__/internalNoteQueries.test.ts b/src/__tests__/internalNoteQueries.test.ts index db598a7a1..140c54057 100644 --- a/src/__tests__/internalNoteQueries.test.ts +++ b/src/__tests__/internalNoteQueries.test.ts @@ -26,7 +26,7 @@ describe('internalNoteQueries', () => { contentTypeId content createdUserId - createdDate + createdAt createdUser { _id } } @@ -49,4 +49,29 @@ describe('internalNoteQueries', () => { expect(responses.length).toBe(1); }); + + test('Internal note details', async () => { + const internalNote = await internalNoteFactory({}); + + const qry = ` + query internalNoteDetail($_id: String!) { + internalNoteDetail(_id: $_id) { + _id + contentType + contentTypeId + content + createdUserId + createdAt + + createdUser { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'internalNoteDetail', { + _id: internalNote._id, + }); + + expect(response._id).toBe(internalNote._id); + }); }); diff --git a/src/__tests__/knowledgeBaseDb.test.ts b/src/__tests__/knowledgeBaseDb.test.ts index b00b9045e..405a8c811 100644 --- a/src/__tests__/knowledgeBaseDb.test.ts +++ b/src/__tests__/knowledgeBaseDb.test.ts @@ -24,13 +24,27 @@ describe('test knowledge base models', () => { await Users.deleteMany({}); }); - describe('KnowledgeBaseTopics', () => { + describe('Knowledge base topics', () => { afterEach(async () => { await KnowledgeBaseTopics.deleteMany({}); await Brands.deleteMany({}); await KnowledgeBaseCategories.deleteMany({}); }); + test('Get knowledge base topic', async () => { + const topic = await knowledgeBaseTopicFactory(); + + try { + await KnowledgeBaseTopics.getTopic('fakeId'); + } catch (e) { + expect(e.message).toBe('Knowledge base topic not found'); + } + + const response = await KnowledgeBaseTopics.getTopic(topic._id); + + expect(response).toBeDefined(); + }); + test(`check if Error('userId must be supplied') is being called as intended on create method`, () => { expect.assertions(1); @@ -53,7 +67,7 @@ describe('test knowledge base models', () => { } }); - test('create', async () => { + test('Create topic', async () => { const categoryA = await knowledgeBaseCategoryFactory({}); const categoryB = await knowledgeBaseCategoryFactory({}); const brand = await brandFactory({}); @@ -73,7 +87,7 @@ describe('test knowledge base models', () => { expect(topic.brandId).toBe(doc.brandId); }); - test('update', async () => { + test('Update topic', async () => { const categoryA = await knowledgeBaseCategoryFactory({}); const categoryB = await knowledgeBaseCategoryFactory({}); @@ -94,7 +108,7 @@ describe('test knowledge base models', () => { topic.categoryIds = [categoryA._id, categoryB._id]; topic.brandId = brandB._id; - const newTopic = await KnowledgeBaseTopics.updateDoc(topic._id, topic.toObject(), _user); + const newTopic = await KnowledgeBaseTopics.updateDoc(topic._id, topic.toObject(), _user._id); expect(newTopic._id).toBe(topic._id); expect(newTopic.title).toBe(topic.title); @@ -103,7 +117,7 @@ describe('test knowledge base models', () => { expect(newTopic.brandId).toBe(brandB._id); }); - test('remove', async () => { + test('Remove topic', async () => { const categoryA = await knowledgeBaseCategoryFactory({}); const categoryB = await knowledgeBaseCategoryFactory({}); @@ -124,16 +138,36 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseTopics.find().countDocuments()).toBe(0); expect(await KnowledgeBaseCategories.find().countDocuments()).toBe(0); + + try { + await KnowledgeBaseTopics.removeDoc('fakeId'); + } catch (e) { + expect(e.message).toBe('Topic not found'); + } }); }); - describe('KnowledgeBaseCategories', () => { + describe('Knowledge base categories', () => { afterEach(async () => { await KnowledgeBaseCategories.deleteMany({}); await KnowledgeBaseArticles.deleteMany({}); }); - test(`expect Error('userId must be supplied') to be called as intended`, async () => { + test('Get knowledge base category', async () => { + const category = await knowledgeBaseCategoryFactory(); + + try { + await KnowledgeBaseCategories.getCategory('fakeId'); + } catch (e) { + expect(e.message).toBe('Knowledge base category not found'); + } + + const response = await KnowledgeBaseCategories.getCategory(category._id); + + expect(response).toBeDefined(); + }); + + test(`check if Error('userId must be supplied') in create`, async () => { expect.assertions(1); try { @@ -143,10 +177,11 @@ describe('test knowledge base models', () => { } }); - test('create', async () => { + test('Create category', async () => { const article = await knowledgeBaseArticleFactory({}); - const topicA = await knowledgeBaseTopicFactory({}); + const topicCategory = await knowledgeBaseCategoryFactory({ articleIds: [article._id] }); + const topicA = await knowledgeBaseTopicFactory({ categoryIds: [topicCategory._id] }); const topicB = await knowledgeBaseTopicFactory({}); const doc = { @@ -178,20 +213,26 @@ describe('test knowledge base models', () => { throw new Error('Topic not found'); } - expect(topicAObj.categoryIds.length).toBe(1); - expect(topicAObj.categoryIds[0]).toBe(category._id.toString()); + expect(topicAObj.categoryIds.length).toBe(2); expect(topicBObj.categoryIds.length).toBe(1); expect(topicBObj.categoryIds[0]).toBe(category._id.toString()); }); - test('update', async () => { + test(`check if Error('userId must be supplied') in update`, async () => { + expect.assertions(1); + + try { + await KnowledgeBaseCategories.updateDoc('fakeId', {}); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + + test('Update category', async () => { const article = await knowledgeBaseArticleFactory({}); const articleB = await knowledgeBaseArticleFactory({}); - const topicA = await knowledgeBaseTopicFactory({}); - const topicB = await knowledgeBaseTopicFactory({}); - const doc = { title: 'Test category title', description: 'Test category description', @@ -206,7 +247,10 @@ describe('test knowledge base models', () => { category.articleIds = [article._id, articleB._id]; category.icon = 'test icon 2'; - const newCategory = await KnowledgeBaseCategories.updateDoc( + const topicA = await knowledgeBaseTopicFactory({ categoryIds: [category._id] }); + const topicB = await knowledgeBaseTopicFactory({}); + + const updatedCategory = await KnowledgeBaseCategories.updateDoc( category._id, { ...category.toObject(), @@ -215,16 +259,16 @@ describe('test knowledge base models', () => { _user._id, ); - expect(newCategory._id).toBe(category._id); - expect(newCategory.title).toBe(category.title); - expect(newCategory.description).toBe(category.description); - expect(newCategory.articleIds).toContain(article._id); - expect(newCategory.articleIds).toContain(articleB._id); - expect(newCategory.icon).toBe(category.icon); + expect(updatedCategory._id).toBe(category._id); + expect(updatedCategory.title).toBe(category.title); + expect(updatedCategory.description).toBe(category.description); + expect(updatedCategory.articleIds).toContain(article._id); + expect(updatedCategory.articleIds).toContain(articleB._id); + expect(updatedCategory.icon).toBe(category.icon); // Values related to modification ====== - expect(newCategory.modifiedBy).toBe(_user._id); - expect(newCategory.modifiedDate).toBeDefined(); + expect(updatedCategory.modifiedBy).toBe(_user._id); + expect(updatedCategory.modifiedDate).toBeDefined(); const topicAObj = await KnowledgeBaseTopics.findOne({ _id: topicA._id.toString(), @@ -238,17 +282,50 @@ describe('test knowledge base models', () => { } expect(topicAObj.categoryIds.length).toBe(1); - expect(topicAObj.categoryIds[0]).toBe(newCategory._id.toString()); + expect(topicAObj.categoryIds[0]).toBe(updatedCategory._id.toString()); expect(topicBObj.categoryIds.length).toBe(1); - expect(topicBObj.categoryIds[0]).toBe(newCategory._id.toString()); + expect(topicBObj.categoryIds[0]).toBe(updatedCategory._id.toString()); + }); + + test('update', async () => { + const category = await KnowledgeBaseCategories.createDoc({}, _user._id); + const updatedCategoryNoTopic = await KnowledgeBaseCategories.updateDoc(category._id, {}, _user._id); + + const topics = await KnowledgeBaseTopics.find({ categoryIds: [updatedCategoryNoTopic._id] }); + expect(topics).toHaveLength(0); + }); + + test('Remove category', async () => { + const doc = { + title: 'Test category title', + description: 'Test category description', + icon: 'test icon', + }; + + const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); + + expect(await KnowledgeBaseCategories.find().countDocuments()).toBe(1); + + await KnowledgeBaseCategories.removeDoc(category._id); + + expect(await KnowledgeBaseCategories.find().countDocuments()).toBe(0); + + try { + await KnowledgeBaseCategories.removeDoc('fakeId'); + } catch (e) { + expect(e.message).toBe('Category not found'); + } }); - test('remove', async () => { + test('Remove with article', async () => { + const article = await knowledgeBaseArticleFactory(); + const doc = { title: 'Test category title', description: 'Test category description', icon: 'test icon', + articleIds: [article._id], }; const category = await KnowledgeBaseCategories.createDoc(doc, _user._id); @@ -258,15 +335,31 @@ describe('test knowledge base models', () => { await KnowledgeBaseCategories.removeDoc(category._id); expect(await KnowledgeBaseCategories.find().countDocuments()).toBe(0); + + expect(await KnowledgeBaseArticles.find().countDocuments()).toBe(0); }); }); - describe('KnowledgeBaseArticles', () => { + describe('Knowledge base articles', () => { afterEach(async () => { await KnowledgeBaseArticles.deleteMany({}); }); - test(`expect Error('userId must be supplied') to be called as intended`, async () => { + test('Get knowledge base article', async () => { + const article = await knowledgeBaseArticleFactory(); + + try { + await KnowledgeBaseArticles.getArticle('fakeId'); + } catch (e) { + expect(e.message).toBe('Knowledge base article not found'); + } + + const response = await KnowledgeBaseArticles.getArticle(article._id); + + expect(response).toBeDefined(); + }); + + test(`check if Error('userId must be supplied') when creating`, async () => { expect.assertions(1); try { @@ -276,7 +369,7 @@ describe('test knowledge base models', () => { } }); - test('create', async () => { + test('Create article', async () => { const categoryA = await knowledgeBaseCategoryFactory({}); const categoryB = await knowledgeBaseCategoryFactory({}); @@ -306,16 +399,23 @@ describe('test knowledge base models', () => { throw new Error('Topic not found'); } - expect(categoryAObj.articleIds.length).toBe(3); - expect(categoryAObj.articleIds[2]).toBe(article._id.toString()); - expect(categoryBObj.articleIds.length).toBe(3); - expect(categoryBObj.articleIds[2]).toBe(article._id.toString()); + expect(categoryAObj.articleIds.length).toBe(1); + expect(categoryAObj.articleIds[0]).toBe(article._id.toString()); + expect(categoryBObj.articleIds.length).toBe(1); + expect(categoryBObj.articleIds[0]).toBe(article._id.toString()); }); - test('update', async () => { - const categoryA = await knowledgeBaseCategoryFactory({ articleIds: [] }); - const categoryB = await knowledgeBaseCategoryFactory({}); + test(`check if Error('userId must be supplied') when updating`, async () => { + expect.assertions(1); + + try { + await KnowledgeBaseArticles.updateDoc('fakeId', {}); + } catch (e) { + expect(e.message).toBe('userId must be supplied'); + } + }); + test('Update article', async () => { const doc = { title: 'Test article title', summary: 'Test article description', @@ -330,7 +430,10 @@ describe('test knowledge base models', () => { article.content = 'Test article content 2'; article.status = PUBLISH_STATUSES.PUBLISH; - const updatedArticle = await KnowledgeBaseArticles.updateDoc( + const categoryA = await knowledgeBaseCategoryFactory({ articleIds: [article._id] }); + const categoryB = await knowledgeBaseCategoryFactory({}); + + let updatedArticle = await KnowledgeBaseArticles.updateDoc( article._id, { ...article.toObject(), @@ -358,11 +461,18 @@ describe('test knowledge base models', () => { expect(categoryAObj.articleIds.length).toBe(1); expect(categoryAObj.articleIds[0]).toBe(article._id.toString()); - expect(categoryBObj.articleIds.length).toBe(3); - expect(categoryBObj.articleIds[2]).toBe(article._id.toString()); + expect(categoryBObj.articleIds.length).toBe(1); + expect(categoryBObj.articleIds[0]).toBe(article._id.toString()); + + const articleNoCategory = await knowledgeBaseArticleFactory(); + updatedArticle = await KnowledgeBaseArticles.updateDoc(articleNoCategory._id, {}, _user._id); + + const cats = await KnowledgeBaseCategories.find({ articleIds: { $in: [articleNoCategory._id] } }); + + expect(cats).toHaveLength(0); }); - test('remove', async () => { + test('Remove article', async () => { const doc = { title: 'Test article title', summary: 'Test article description', @@ -378,5 +488,23 @@ describe('test knowledge base models', () => { expect(await KnowledgeBaseArticles.find().countDocuments()).toBe(0); }); + + test('Article: incReactionCount', async () => { + try { + await KnowledgeBaseArticles.incReactionCount('_id', 'wow'); + } catch (e) { + expect(e.message).toBe('Article not found'); + } + + const article = await knowledgeBaseArticleFactory({ + reactionChoices: ['wow'], + }); + + await KnowledgeBaseArticles.incReactionCount(article._id, 'wow'); + + const updated = (await KnowledgeBaseArticles.findOne({ _id: article._id })) || ({ reactionCounts: {} } as any); + + expect(updated.reactionCounts.wow).toBe(1); + }); }); }); diff --git a/src/__tests__/knowledgeBaseMutations.test.ts b/src/__tests__/knowledgeBaseMutations.test.ts index cef5977b6..af2855e0c 100644 --- a/src/__tests__/knowledgeBaseMutations.test.ts +++ b/src/__tests__/knowledgeBaseMutations.test.ts @@ -1,6 +1,7 @@ import * as faker from 'faker'; import { graphqlRequest } from '../db/connection'; import { Brands, KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics, Users } from '../db/models'; +import { PUBLISH_STATUSES } from '../db/models/definitions/constants'; import { brandFactory, @@ -31,7 +32,7 @@ const categoryArgs = { const articleArgs = { title: faker.random.word(), summary: faker.random.word(), - status: 'draft', + status: PUBLISH_STATUSES.DRAFT, content: faker.random.word(), }; @@ -40,18 +41,13 @@ describe('mutations', () => { let _knowledgeBaseCategory; let _knowledgeBaseArticle; let _brand; - let _user; - let context; beforeEach(async () => { // Creating test data - _knowledgeBaseTopic = await knowledgeBaseTopicFactory({}); - _knowledgeBaseCategory = await knowledgeBaseCategoryFactory({}); - _knowledgeBaseArticle = await knowledgeBaseArticleFactory({}); + _knowledgeBaseArticle = await knowledgeBaseArticleFactory({ status: PUBLISH_STATUSES.PUBLISH }); + _knowledgeBaseCategory = await knowledgeBaseCategoryFactory({ articleIds: [_knowledgeBaseArticle._id] }); + _knowledgeBaseTopic = await knowledgeBaseTopicFactory({ categoryIds: [_knowledgeBaseCategory._id] }); _brand = await brandFactory({}); - _user = await userFactory({}); - - context = { user: _user }; }); afterEach(async () => { @@ -87,7 +83,7 @@ describe('mutations', () => { } `; - const knowledgeBaseTopic = await graphqlRequest(mutation, 'knowledgeBaseTopicsAdd', { doc }, context); + const knowledgeBaseTopic = await graphqlRequest(mutation, 'knowledgeBaseTopicsAdd', { doc }); expect(knowledgeBaseTopic.title).toBe(doc.title); expect(knowledgeBaseTopic.description).toBe(doc.description); @@ -122,12 +118,10 @@ describe('mutations', () => { } `; - const knowledgeBaseTopic = await graphqlRequest( - mutation, - 'knowledgeBaseTopicsEdit', - { _id: _knowledgeBaseTopic._id, doc }, - context, - ); + const knowledgeBaseTopic = await graphqlRequest(mutation, 'knowledgeBaseTopicsEdit', { + _id: _knowledgeBaseTopic._id, + doc, + }); expect(knowledgeBaseTopic._id).toBe(_knowledgeBaseTopic._id); expect(knowledgeBaseTopic.title).toBe(doc.title); @@ -147,7 +141,7 @@ describe('mutations', () => { } `; - await graphqlRequest(mutation, 'knowledgeBaseTopicsRemove', { _id }, context); + await graphqlRequest(mutation, 'knowledgeBaseTopicsRemove', { _id }); expect(await KnowledgeBaseTopics.findOne({ _id })).toBe(null); }); @@ -175,7 +169,7 @@ describe('mutations', () => { } `; - const knowledgeBaseCategory = await graphqlRequest(mutation, 'knowledgeBaseCategoriesAdd', { doc }, context); + const knowledgeBaseCategory = await graphqlRequest(mutation, 'knowledgeBaseCategoriesAdd', { doc }); expect(knowledgeBaseCategory.title).toBe(doc.title); expect(knowledgeBaseCategory.description).toBe(doc.description); @@ -211,12 +205,10 @@ describe('mutations', () => { } `; - const knowledgeBaseCategory = await graphqlRequest( - mutation, - 'knowledgeBaseCategoriesEdit', - { _id: _knowledgeBaseCategory._id, doc }, - context, - ); + const knowledgeBaseCategory = await graphqlRequest(mutation, 'knowledgeBaseCategoriesEdit', { + _id: _knowledgeBaseCategory._id, + doc, + }); expect(knowledgeBaseCategory._id).toBe(_knowledgeBaseCategory._id); expect(knowledgeBaseCategory.title).toBe(doc.title); @@ -238,7 +230,7 @@ describe('mutations', () => { } `; - await graphqlRequest(mutation, 'knowledgeBaseCategoriesRemove', { _id }, context); + await graphqlRequest(mutation, 'knowledgeBaseCategoriesRemove', { _id }); expect(await KnowledgeBaseCategories.findOne({ _id })).toBe(null); }); @@ -261,7 +253,7 @@ describe('mutations', () => { } `; - const article = await graphqlRequest(mutation, 'knowledgeBaseArticlesAdd', { doc }, context); + const article = await graphqlRequest(mutation, 'knowledgeBaseArticlesAdd', { doc }); const [category] = await KnowledgeBaseCategories.find({ _id: { $in: doc.categoryIds }, @@ -292,12 +284,10 @@ describe('mutations', () => { } `; - const article = await graphqlRequest( - mutation, - 'knowledgeBaseArticlesEdit', - { _id: _knowledgeBaseArticle._id, doc }, - context, - ); + const article = await graphqlRequest(mutation, 'knowledgeBaseArticlesEdit', { + _id: _knowledgeBaseArticle._id, + doc, + }); const [category] = await KnowledgeBaseCategories.find({ _id: { $in: doc.categoryIds }, @@ -312,7 +302,8 @@ describe('mutations', () => { }); test('Remove knowledge base article', async () => { - const _id = _knowledgeBaseArticle._id; + const user = await userFactory(); + const article = await knowledgeBaseArticleFactory({ modifiedBy: user._id }); const mutation = ` mutation knowledgeBaseArticlesRemove($_id: String!) { @@ -320,8 +311,8 @@ describe('mutations', () => { } `; - await graphqlRequest(mutation, 'knowledgeBaseArticlesRemove', { _id }, context); + await graphqlRequest(mutation, 'knowledgeBaseArticlesRemove', { _id: article._id }); - expect(await KnowledgeBaseArticles.findOne({ _id })).toBe(null); + expect(await KnowledgeBaseArticles.findOne({ _id: article._id })).toBe(null); }); }); diff --git a/src/__tests__/knowledgeBaseQueries.test.ts b/src/__tests__/knowledgeBaseQueries.test.ts index 3dedc4da1..35f0627ca 100644 --- a/src/__tests__/knowledgeBaseQueries.test.ts +++ b/src/__tests__/knowledgeBaseQueries.test.ts @@ -1,5 +1,10 @@ import { graphqlRequest } from '../db/connection'; -import { knowledgeBaseArticleFactory, knowledgeBaseCategoryFactory, knowledgeBaseTopicFactory } from '../db/factories'; +import { + knowledgeBaseArticleFactory, + knowledgeBaseCategoryFactory, + knowledgeBaseTopicFactory, + userFactory, +} from '../db/factories'; import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../db/models'; import './setup.ts'; @@ -22,16 +27,6 @@ describe('knowledgeBaseQueries', () => { query knowledgeBaseTopics($page: Int $perPage: Int) { knowledgeBaseTopics(page: $page perPage: $perPage) { _id - title - description - categories { _id } - brand { _id } - color - languageCode - createdBy - createdDate - modifiedBy - modifiedDate } } `; @@ -45,21 +40,37 @@ describe('knowledgeBaseQueries', () => { }); test('Knowledge base topic detail', async () => { - const knowledgeBaseTopic = await knowledgeBaseTopicFactory(); - const qry = ` query knowledgeBaseTopicDetail($_id: String!) { knowledgeBaseTopicDetail(_id: $_id) { _id + title + description + categories { _id } + brand { _id } + color + languageCode + createdBy + createdDate + modifiedBy + modifiedDate } } `; - const response = await graphqlRequest(qry, 'knowledgeBaseTopicDetail', { + const knowledgeBaseTopic = await knowledgeBaseTopicFactory(); + let response = await graphqlRequest(qry, 'knowledgeBaseTopicDetail', { _id: knowledgeBaseTopic._id, }); expect(response._id).toBe(knowledgeBaseTopic._id); + + const knowledgeBaseTopicWithColor = await knowledgeBaseTopicFactory({ color: '#fff' }); + response = await graphqlRequest(qry, 'knowledgeBaseTopicDetail', { + _id: knowledgeBaseTopicWithColor._id, + }); + + expect(response._id).toBe(knowledgeBaseTopicWithColor._id); }); test('Get total count of knowledge base topic', async () => { @@ -80,13 +91,16 @@ describe('knowledgeBaseQueries', () => { }); test('Knowledge base categories', async () => { + const user = await userFactory({}); const topic = await knowledgeBaseTopicFactory(); // Creating test data - await knowledgeBaseCategoryFactory({ topicIds: [topic._id] }); + const category = await knowledgeBaseCategoryFactory({ topicIds: [topic._id] }); await knowledgeBaseCategoryFactory({ topicIds: [topic._id] }); await knowledgeBaseCategoryFactory({}); + await knowledgeBaseArticleFactory({ categoryIds: [category._id], status: 'publish', userId: user._id }); + const args = { page: 1, perPage: 5, @@ -138,13 +152,24 @@ describe('knowledgeBaseQueries', () => { modifiedBy modifiedDate } + + authors { + _id + } + + numOfArticles } } `; - const responses = await graphqlRequest(qry, 'knowledgeBaseCategories', args); + let responses = await graphqlRequest(qry, 'knowledgeBaseCategories', args, { user }); expect(responses.length).toBe(2); + + delete args.topicIds; + responses = await graphqlRequest(qry, 'knowledgeBaseCategories', args); + + expect(responses.length).toBe(3); }); test('Knowledge base category detail', async () => { @@ -183,11 +208,35 @@ describe('knowledgeBaseQueries', () => { } `; - const response = await graphqlRequest(qry, 'knowledgeBaseCategoriesTotalCount', { + let response = await graphqlRequest(qry, 'knowledgeBaseCategoriesTotalCount', { topicIds: topic._id, }); expect(response).toBe(2); + + response = await graphqlRequest(qry, 'knowledgeBaseCategoriesTotalCount'); + + expect(response).toBe(3); + }); + + test('Knowledge base category detail', async () => { + const topic = await knowledgeBaseTopicFactory(); + + const lastCategory = await knowledgeBaseCategoryFactory({ + topicIds: [topic._id], + }); + + const qry = ` + query knowledgeBaseCategoriesGetLast { + knowledgeBaseCategoriesGetLast { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'knowledgeBaseCategoriesGetLast'); + + expect(response._id).toBe(lastCategory._id); }); test('Knowledge base articles', async () => { @@ -229,9 +278,14 @@ describe('knowledgeBaseQueries', () => { } `; - const responses = await graphqlRequest(qry, 'knowledgeBaseArticles', args); + let responses = await graphqlRequest(qry, 'knowledgeBaseArticles', args); expect(responses.length).toBe(2); + + delete args.categoryIds; + responses = await graphqlRequest(qry, 'knowledgeBaseArticles', args); + + expect(responses.length).toBe(3); }); test('Knowledge base article detail', async () => { @@ -270,10 +324,14 @@ describe('knowledgeBaseQueries', () => { } `; - const response = await graphqlRequest(qry, 'knowledgeBaseArticlesTotalCount', { + let response = await graphqlRequest(qry, 'knowledgeBaseArticlesTotalCount', { categoryIds: [category._id], }); expect(response).toBe(2); + + response = await graphqlRequest(qry, 'knowledgeBaseArticlesTotalCount'); + + expect(response).toBe(3); }); }); diff --git a/src/__tests__/logQueries.test.ts b/src/__tests__/logQueries.test.ts new file mode 100644 index 000000000..37b9579d5 --- /dev/null +++ b/src/__tests__/logQueries.test.ts @@ -0,0 +1,60 @@ +import * as sinon from 'sinon'; +import { MODULE_NAMES } from '../data/constants'; +import * as utils from '../data/logUtils'; +import { graphqlRequest } from '../db/connection'; +import './setup.ts'; + +describe('log queries', () => { + test('Logs', async () => { + const mock = sinon.stub(utils, 'fetchLogs').callsFake(() => { + return Promise.resolve({ logs: [], totalCount: 0 }); + }); + + const qry = ` + query logs( + $start: String + $end: String + $userId: String + $action: String + $page: Int + $perPage: Int + ) { + logs( + start: $start + end: $end + userId: $userId + action: $action + page: $page + perPage: $perPage + ) { + logs { + _id + } + totalCount + } + } + `; + + const response = await graphqlRequest(qry, 'logs', {}); + + expect(response.logs).toHaveLength(0); + expect(response.totalCount).toBe(0); + + mock.restore(); + }); + + test('getDbSchemaLabels', async () => { + const query = ` + query getDbSchemaLabels($type: String) { + getDbSchemaLabels(type: $type) { + name + label + } + } + `; + + const response = await graphqlRequest(query, 'getDbSchemaLabels', { type: MODULE_NAMES.BRAND }); + + expect(response).toBeDefined(); + }); +}); diff --git a/src/__tests__/messengerAppDb.test.ts b/src/__tests__/messengerAppDb.test.ts new file mode 100644 index 000000000..51b8c8dbc --- /dev/null +++ b/src/__tests__/messengerAppDb.test.ts @@ -0,0 +1,49 @@ +import { messengerAppFactory } from '../db/factories'; +import { MessengerApps, Users } from '../db/models'; + +import './setup.ts'; + +describe('Messenger apps', () => { + afterEach(async () => { + await Users.deleteMany({}); + await MessengerApps.deleteMany({}); + }); + + test('Get messenger app', async () => { + const messengerApp = await messengerAppFactory({}); + + try { + await MessengerApps.getApp('fakeId'); + } catch (e) { + expect(e.message).toBe('Messenger app not found'); + } + + const response = await MessengerApps.getApp(messengerApp._id); + + expect(response).toBeDefined(); + }); + + test('Create messenger app', async () => { + const app = await MessengerApps.createApp({ + kind: 'googleMeet', + name: 'name', + }); + + expect(app._id).toBeDefined(); + expect(app.kind).toBe('googleMeet'); + expect(app.name).toBe('name'); + }); + + test('Update messenger app', async () => { + const messengerApp = await messengerAppFactory({}); + + const updatedApp = await MessengerApps.updateApp(messengerApp._id, { + kind: 'googleMeet', + name: 'name', + }); + + expect(updatedApp._id).toBeDefined(); + expect(updatedApp.kind).toBe('googleMeet'); + expect(updatedApp.name).toBe('name'); + }); +}); diff --git a/src/__tests__/messengerAppMutations.ts b/src/__tests__/messengerAppMutations.ts index 31fcd333a..42032876a 100644 --- a/src/__tests__/messengerAppMutations.ts +++ b/src/__tests__/messengerAppMutations.ts @@ -1,99 +1,42 @@ import { graphqlRequest } from '../db/connection'; -import { formFactory, messengerAppFactory, userFactory } from '../db/factories'; -import { MessengerApps, Users } from '../db/models'; +import { integrationFactory } from '../db/factories'; +import { MessengerApps } from '../db/models'; +import { KIND_CHOICES } from '../db/models/definitions/constants'; import './setup.ts'; describe('mutations', () => { - let _user; - let context; - - beforeEach(async () => { - // Creating test data - _user = await userFactory({}); - - context = { user: _user }; - }); - afterEach(async () => { // Clearing test data await MessengerApps.deleteMany({}); - await Users.deleteMany({}); }); - test('Add knowledgebase', async () => { - const args = { - name: 'knowledgebase', - integrationId: 'integrationId', - topicId: 'topicId', - }; + test('Save messenger app', async () => { + const integration = await integrationFactory({ kind: KIND_CHOICES.MESSENGER }); const mutation = ` - mutation messengerAppsAddKnowledgebase($name: String!, $integrationId: String!, $topicId: String!) { - messengerAppsAddKnowledgebase(name: $name, integrationId: $integrationId, topicId: $topicId) { - name - kind - showInInbox - credentials - } + mutation messengerAppSave($integrationId: String!, $messengerApps: MessengerAppsInput) { + messengerAppSave(integrationId: $integrationId, messengerApps: $messengerApps) } `; - const app = await graphqlRequest(mutation, 'messengerAppsAddKnowledgebase', args, context); - - expect(app.kind).toBe('knowledgebase'); - expect(app.showInInbox).toBe(false); - expect(app.name).toBe(args.name); - expect(app.credentials).toEqual({ - integrationId: args.integrationId, - topicId: args.topicId, - }); - }); - - test('Add lead', async () => { - const form = await formFactory({}); - - const args = { - name: 'lead', - integrationId: 'integrationId', - formId: form._id, + const args: any = { + integrationId: integration._id, + messengerApps: { + websites: [{ description: 'description' }], + knowledgebases: [{ topicId: 'topicId' }], + leads: [{ formCode: 'formCode' }], + }, }; - const mutation = ` - mutation messengerAppsAddLead($name: String!, $integrationId: String!, $formId: String!) { - messengerAppsAddLead(name: $name, integrationId: $integrationId, formId: $formId) { - name - kind - showInInbox - credentials - } - } - `; - - const app = await graphqlRequest(mutation, 'messengerAppsAddLead', args, context); - - expect(app.kind).toBe('lead'); - expect(app.showInInbox).toBe(false); - expect(app.name).toBe(args.name); - expect(app.credentials).toEqual({ - integrationId: args.integrationId, - formCode: form.code, - }); - }); + let response = await graphqlRequest(mutation, 'messengerAppSave', args); - test('Remove', async () => { - const app = await messengerAppFactory({ credentials: { integrationId: '_id', formCode: 'code' } }); - - const mutation = ` - mutation messengerAppsRemove($_id: String!) { - messengerAppsRemove(_id: $_id) - } - `; + expect(response).toBe('success'); - await graphqlRequest(mutation, 'messengerAppsRemove', { _id: app._id }, context); + args.messengerApps = {}; - const count = await MessengerApps.find().countDocuments(); + response = await graphqlRequest(mutation, 'messengerAppSave', args); - expect(count).toBe(0); + expect(response).toBe('success'); }); }); diff --git a/src/__tests__/messengerAppsQueries.test.ts b/src/__tests__/messengerAppsQueries.test.ts deleted file mode 100644 index 1440156d5..000000000 --- a/src/__tests__/messengerAppsQueries.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { graphqlRequest } from '../db/connection'; -import { messengerAppFactory } from '../db/factories'; -import { MessengerApps } from '../db/models'; - -import './setup.ts'; - -describe('Messenger app queries', () => { - afterEach(async () => { - // Clearing test data - await MessengerApps.deleteMany({}); - }); - - test('Messenger Apps list', async () => { - await messengerAppFactory({ - credentials: { - access_token: '123', - expiry_date: Date.now(), - formCode: '123', - integrationId: '123', - }, - kind: 'knowledgebase', - }); - await messengerAppFactory({ - credentials: { - access_token: '123', - expiry_date: Date.now(), - formCode: '123', - integrationId: '123', - }, - kind: 'knowledgebase', - }); - - const qry = ` - query messengerApps($kind: String) { - messengerApps(kind: $kind) { - _id - } - } - `; - - const responses = await graphqlRequest(qry, 'messengerApps', { - kind: 'knowledgebase', - }); - - expect(responses.length).toBe(2); - }); - - test('Messenger Apps count', async () => { - await messengerAppFactory({ - credentials: { - access_token: '123', - expiry_date: Date.now(), - formCode: '123', - integrationId: '123', - }, - kind: 'knowledgebase', - }); - await messengerAppFactory({ - credentials: { - access_token: '123', - expiry_date: Date.now(), - formCode: '123', - integrationId: '123', - }, - kind: 'lead', - }); - - const qry = ` - query messengerAppsCount($kind: String) { - messengerAppsCount(kind: $kind) - } - `; - - // customer =========================== - const response = await graphqlRequest(qry, 'messengerAppsCount', { - kind: 'knowledgebase', - }); - - expect(response).toBe(1); - }); -}); diff --git a/src/__tests__/notificationDb.test.ts b/src/__tests__/notificationDb.test.ts index 557764cbd..a020fa976 100644 --- a/src/__tests__/notificationDb.test.ts +++ b/src/__tests__/notificationDb.test.ts @@ -1,4 +1,4 @@ -import { notificationConfigurationFactory, userFactory } from '../db/factories'; +import { customerFactory, notificationConfigurationFactory, notificationFactory, userFactory } from '../db/factories'; import { NotificationConfigurations, Notifications, Users } from '../db/models'; import { NOTIFICATION_TYPES } from '../db/models/definitions/constants'; @@ -47,6 +47,12 @@ describe('Notification model tests', () => { test('model create, update, remove', async () => { // Create notification ================ + await notificationConfigurationFactory({ + isAllowed: true, + notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, + user: _user2._id, + }); + let doc = { notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, title: 'new Notification title', @@ -83,10 +89,23 @@ describe('Notification model tests', () => { expect(notification.link).toEqual(doc.link); expect(notification.receiver).toBe(user3._id); + // check method markAsRead by user ============= + await Notifications.markAsRead([], user3._id); + + let notificationObj = await Notifications.findOne({ + _id: notification._id, + }); + + if (!notificationObj) { + throw new Error('Notification not found'); + } + + expect(notificationObj.isRead).toEqual(true); + // check method markAsRead ============= await Notifications.markAsRead([notification._id]); - const notificationObj = await Notifications.findOne({ + notificationObj = await Notifications.findOne({ _id: notification._id, }); @@ -102,12 +121,27 @@ describe('Notification model tests', () => { expect(await Notifications.find({}).countDocuments()).toEqual(0); }); - test('sending notifications', () => { - return; + test('check if read', async () => { + const receiver = await userFactory(); + const customer = await customerFactory(); + + await notificationFactory({ + receiver, + contentType: 'customer', + contentTypeId: customer._id, + }); + + let response = await Notifications.checkIfRead(receiver._id, customer._id); + + expect(response).toBeFalsy(); + + response = await Notifications.checkIfRead('fakeUserId', 'fakeId'); + + expect(response).toBeTruthy(); }); }); -describe('NotificationConfiguration model tests', async () => { +describe('NotificationConfiguration model tests', () => { test('test if model methods are working correctly', async () => { // creating new notification configuration ========== const user = await userFactory({}); diff --git a/src/__tests__/notificationMutations.test.ts b/src/__tests__/notificationMutations.test.ts index 53905f5f3..2f0b96318 100644 --- a/src/__tests__/notificationMutations.test.ts +++ b/src/__tests__/notificationMutations.test.ts @@ -1,5 +1,5 @@ import { graphqlRequest } from '../db/connection'; -import { notificationFactory, userFactory } from '../db/factories'; +import { dealFactory, notificationFactory, userFactory } from '../db/factories'; import { Notifications, Users } from '../db/models'; import './setup.ts'; @@ -7,13 +7,11 @@ import './setup.ts'; describe('testing mutations', () => { let _user; let _notification; - let context; beforeEach(async () => { // Creating test data _user = await userFactory({}); _notification = await notificationFactory({ createdUser: _user }); - context = { user: _user }; }); afterEach(async () => { @@ -37,7 +35,7 @@ describe('testing mutations', () => { } `; - const notification = await graphqlRequest(mutation, 'notificationsSaveConfig', args, context); + const notification = await graphqlRequest(mutation, 'notificationsSaveConfig', args); expect(notification.notifType).toBe(args.notifType); expect(notification.isAllowed).toBe(args.isAllowed); @@ -45,17 +43,29 @@ describe('testing mutations', () => { test('Mark as read notification', async () => { const mutation = ` - mutation notificationsMarkAsRead($_ids: [String]) { - notificationsMarkAsRead(_ids: $_ids) + mutation notificationsMarkAsRead($_ids: [String], $contentTypeId: String) { + notificationsMarkAsRead(_ids: $_ids, contentTypeId: $contentTypeId) } `; - await graphqlRequest(mutation, 'notificationsMarkAsRead', { _ids: [_notification._id] }, context); + await graphqlRequest(mutation, 'notificationsMarkAsRead', { _ids: [_notification._id] }); - const [notification] = await Notifications.find({ - _id: { $in: _notification._id }, - }); + const [notification] = await Notifications.find({ _id: _notification._id }); expect(notification.isRead).toBe(true); + + const deal = await dealFactory(); + const _dealNotification = await notificationFactory({ + contentType: 'deal', + contentTypeId: deal._id, + createdUser: _user, + }); + + // filter by contentTypeId + await graphqlRequest(mutation, 'notificationsMarkAsRead', { contentTypeId: deal._id }); + + const [dealNotification] = await Notifications.find({ _id: _dealNotification._id }); + + expect(dealNotification.isRead).toBe(true); }); }); diff --git a/src/__tests__/notificationQueries.test.ts b/src/__tests__/notificationQueries.test.ts index fcdff3842..8a3856c03 100644 --- a/src/__tests__/notificationQueries.test.ts +++ b/src/__tests__/notificationQueries.test.ts @@ -40,14 +40,14 @@ describe('notificationsQueries', () => { title: title1, createdUser: user, receiver, - requireRead: true, + isRead: true, }); await notificationFactory({ title: title2, createdUser: user, receiver, - requireRead: false, + isRead: false, }); const qry = ` @@ -86,6 +86,11 @@ describe('notificationsQueries', () => { expect(response.length).toBe(1); expect(response[0].title).toBe(title2); + + response = await graphqlRequest(qry, 'notifications', { requireRead: true }, { user: receiver }); + + expect(response.length).toBe(1); + expect(response[0].isRead).toBe(false); }); test('Count notifications', async () => { @@ -110,7 +115,7 @@ describe('notificationsQueries', () => { expect(response).toBe(2); // notification for receiver 2 - response = await graphqlRequest(qry, 'notificationCounts', { requireRead: false }, { user: receiver2 }); + response = await graphqlRequest(qry, 'notificationCounts', { requireRead: true }, { user: receiver2 }); expect(response).toBe(1); }); diff --git a/src/__tests__/notificationTools.test.ts b/src/__tests__/notificationTools.test.ts index a2cc95efc..363488614 100644 --- a/src/__tests__/notificationTools.test.ts +++ b/src/__tests__/notificationTools.test.ts @@ -3,7 +3,7 @@ import utils from '../data/utils'; import { channelFactory, notificationConfigurationFactory, userFactory } from '../db/factories'; import { NotificationConfigurations, Notifications, Users } from '../db/models'; -import { NOTIFICATION_TYPES } from '../db/models/definitions/constants'; +import { NOTIFICATION_CONTENT_TYPES, NOTIFICATION_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('testings helper methods', () => { @@ -46,11 +46,14 @@ describe('testings helper methods', () => { const doc = { notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, - createdUser: _user._id, + createdUser: _user, title: 'new Notification title', content: 'new Notification content', link: 'new Notification link', + action: 'action', receivers: [_user._id, _user2._id, _user3._id], + contentType: NOTIFICATION_CONTENT_TYPES.CHANNEL, + contentTypeId: 'channelId', }; await utils.sendNotification(doc); @@ -59,7 +62,7 @@ describe('testings helper methods', () => { expect(notifications.length).toEqual(0); // Send notifications when there is config allowing it ==================== - await NotificationConfigurations.updateMany({}, { isAllowed: true }, { multi: true }); + await NotificationConfigurations.updateMany({}, { $set: { isAllowed: true } }, { multi: true }); await utils.sendNotification(doc); @@ -68,7 +71,7 @@ describe('testings helper methods', () => { expect(notifications.length).toEqual(3); expect(notifications[0].notifType).toEqual(doc.notifType); - expect(notifications[0].createdUser).toEqual(doc.createdUser); + expect(notifications[0].createdUser).toBe(doc.createdUser._id); expect(notifications[0].title).toEqual(doc.title); expect(notifications[0].content).toEqual(doc.content); expect(notifications[0].link).toEqual(doc.link); @@ -86,18 +89,21 @@ describe('testings helper methods', () => { throw new Error('Couldnt create channel'); } - const content = `You have invited to '${channel.name}' channel.`; + const content = `${channel.name} channel`; - const spySendNotification = jest.spyOn(utils, 'sendNotification').mockImplementation(() => ({})); + const spySendNotification = jest.spyOn(utils, 'sendNotification').mockImplementation(() => Promise.resolve(true)); - await sendChannelNotifications(channel); + await sendChannelNotifications(channel, 'invited', _user); expect(utils.sendNotification).toBeCalledWith({ - createdUser: channel.userId, + createdUser: _user, + action: 'invited you to the', notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, - title: content, + title: `Channel updated`, content, - link: `/inbox?channelId=${channel._id}`, + link: `/inbox/index?channelId=${channel._id}`, + contentType: NOTIFICATION_CONTENT_TYPES.CHANNEL, + contentTypeId: channel._id, receivers: channel && channel.memberIds ? channel.memberIds.filter(id => id !== channel.userId) : null, }); diff --git a/src/__tests__/permissionDb.test.ts b/src/__tests__/permissionDb.test.ts index 1b16570a9..592330dd6 100644 --- a/src/__tests__/permissionDb.test.ts +++ b/src/__tests__/permissionDb.test.ts @@ -16,7 +16,6 @@ describe('Test permissions model', () => { const doc = { actions: ['up', ' test'], - allowed: true, module: 'module name', }; @@ -28,7 +27,7 @@ describe('Test permissions model', () => { { name: 'action', description: 'd', use: [] }, { name: 'action1', description: 'd', use: [] }, { name: 'action2', description: 'd', use: [] }, - { name: 'action3', description: 'd', use: [] }, + { name: 'action3', description: 'd' }, ], }, }); @@ -46,21 +45,32 @@ describe('Test permissions model', () => { await UsersGroups.deleteMany({}); }); - test('Create permission invalid action', async () => { + test('Create permission (Error: Invalid data)', async () => { expect.assertions(1); try { - await Permissions.createPermission({ userIds: [_user._id], ...doc }); + await Permissions.createPermission({ userIds: [_user._id], ...doc, allowed: true }); } catch (e) { expect(e.message).toEqual('Invalid data'); } }); + test('Create permission without user and group', async () => { + const permission = await Permissions.createPermission({ + ...doc, + actions: ['action', 'action1', 'action2', 'action3'], + allowed: true, + }); + + expect(permission.length).toEqual(0); + }); + test('Create permission', async () => { const permission = await Permissions.createPermission({ ...doc, + allowed: true, userIds: [_user._id], groupIds: [_group._id], - actions: ['action', 'action1', 'action2', 'action3'], + actions: ['action', 'action', 'action1', 'action2', 'action3'], }); expect(permission.length).toEqual(8); @@ -80,12 +90,42 @@ describe('Test permissions model', () => { } }); + test('Test getPermission() with fake id', async () => { + expect.assertions(1); + + try { + await Permissions.getPermission('not-found'); + } catch (e) { + expect(e.message).toBe('Permission not found'); + } + }); + + test('Test getPermission() with real id', async () => { + const perm = await Permissions.getPermission(_permission._id); + + expect(perm._id).toBe(_permission._id); + }); + test('Remove permission', async () => { const isDeleted = await Permissions.removePermission([_permission.id]); expect(isDeleted).toBeTruthy(); }); + test('Get user group', async () => { + try { + await UsersGroups.getGroup('fakeId'); + } catch (e) { + expect(e.message).toBe('User group not found'); + } + + const userGroup = await usersGroupFactory(); + + const response = await UsersGroups.getGroup(userGroup._id); + + expect(response).toBeDefined(); + }); + test('Create user group', async () => { const user1 = await userFactory({}); const user2 = await userFactory({}); @@ -143,4 +183,27 @@ describe('Test permissions model', () => { expect(e.message).toBe('Group not found with id groupId'); } }); + + test('Test copyGroup()', async () => { + const user1 = await userFactory({}); + const user2 = await userFactory({}); + const group = await UsersGroups.createGroup( + { + name: 'groupName', + description: 'groupDescription', + }, + [user1._id, user2._id], + ); + + await permissionFactory({ groupId: group._id }); + await permissionFactory({ groupId: group._id }); + + const clone = await UsersGroups.copyGroup(group._id, [user1._id, user2._id]); + const clonedPermissions = await Permissions.find({ groupId: clone._id }); + const nameCount = await UsersGroups.countDocuments({ name: new RegExp(`${group.name}`, 'i') }); + + expect(clone.name).toBe(`${group.name}-copied-${nameCount - 1}`); + expect(clone.description).toBe(`${group.description}-copied`); + expect(clonedPermissions.length).toBe(2); + }); }); diff --git a/src/__tests__/permissionMutations.test.ts b/src/__tests__/permissionMutations.test.ts index 2f0936fd8..7f6df987d 100644 --- a/src/__tests__/permissionMutations.test.ts +++ b/src/__tests__/permissionMutations.test.ts @@ -6,7 +6,8 @@ import { Permissions, Users, UsersGroups } from '../db/models'; import './setup.ts'; describe('Test permissions mutations', () => { - let _permission; + let userPermission; + let groupPermission; let _user; let _group; let context; @@ -17,11 +18,23 @@ describe('Test permissions mutations', () => { module: 'module name', }; + const permissionFields = ` + _id + module + action + userId + groupId + requiredActions + allowed + `; + beforeEach(async () => { // Creating test data - _permission = await permissionFactory(); - _group = await usersGroupFactory(); _user = await userFactory({ isOwner: true }); + _group = await usersGroupFactory(); + + userPermission = await permissionFactory({ userId: _user._id }); + groupPermission = await permissionFactory({ groupId: _group._id }); context = { user: _user }; }); @@ -66,7 +79,7 @@ describe('Test permissions mutations', () => { await checkLogin(permissionMutations.permissionsAdd, doc); // remove permission - await checkLogin(permissionMutations.permissionsRemove, { ids: [_permission._id] }); + await checkLogin(permissionMutations.permissionsRemove, { ids: [userPermission._id] }); }); test('Create permission', async () => { @@ -93,13 +106,7 @@ describe('Test permissions mutations', () => { groupIds: $groupIds allowed: $allowed ) { - _id - module - action - userId - groupId - requiredActions - allowed + ${permissionFields} } } `; @@ -110,7 +117,7 @@ describe('Test permissions mutations', () => { }); test('Remove permission', async () => { - const ids = [_permission._id]; + const ids = [userPermission._id, groupPermission._id]; const mutation = ` mutation permissionsRemove($ids: [String]!) { @@ -120,7 +127,8 @@ describe('Test permissions mutations', () => { await graphqlRequest(mutation, 'permissionsRemove', { ids }, context); - expect(await Permissions.find({ _id: _permission._id })).toEqual([]); + expect(await Permissions.find({ _id: userPermission._id })).toEqual([]); + expect(await Permissions.find({ _id: groupPermission._id })).toEqual([]); }); test('Create group', async () => { @@ -153,6 +161,9 @@ describe('Test permissions mutations', () => { }); test('Update group', async () => { + await userFactory({ groupIds: [_group._id] }); + await userFactory({ groupIds: [_group._id] }); + const user1 = await userFactory({}); const user2 = await userFactory({}); @@ -193,8 +204,25 @@ describe('Test permissions mutations', () => { } `; + await userFactory({ groupIds: [_group._id] }); + await userFactory({ groupIds: [_group._id] }); + await graphqlRequest(mutation, 'usersGroupsRemove', { _id: _group._id }, context); expect(await UsersGroups.findOne({ _id: _group._id })).toBe(null); }); + + test('Test usersGroupsCopy()', async () => { + const mutation = ` + mutation usersGroupsCopy($_id: String!) { + usersGroupsCopy(_id: $_id) { + _id + } + } + `; + + const clone = await graphqlRequest(mutation, 'usersGroupsCopy', { _id: _group._id }, context); + + expect(clone._id).toBeDefined(); + }); }); diff --git a/src/__tests__/permissionQueries.test.ts b/src/__tests__/permissionQueries.test.ts index 4086c6664..4574b7106 100644 --- a/src/__tests__/permissionQueries.test.ts +++ b/src/__tests__/permissionQueries.test.ts @@ -1,8 +1,31 @@ import { permissionQueries, usersGroupQueries } from '../data/resolvers/queries/permissions'; +import { graphqlRequest } from '../db/connection'; +import { permissionFactory, userFactory, usersGroupFactory } from '../db/factories'; +import { Permissions, Users, UsersGroups } from '../db/models'; import './setup.ts'; describe('permissionQueries', () => { + const commonParamDefs = ` + $module: String + $action: String + $userId: String + $groupId: String + `; + + const commonParams = ` + module: $module + action: $action + userId: $userId + groupId: $groupId + `; + + afterEach(async () => { + // Clearing test data + await Permissions.deleteMany({}); + await Users.deleteMany({}); + }); + test(`test if Error('Login required') exception is working as intended`, async () => { expect.assertions(4); @@ -19,9 +42,157 @@ describe('permissionQueries', () => { expectError(permissionQueries.permissionActions); expectError(permissionQueries.permissionsTotalCount); }); + + test(`Permissions`, async () => { + await permissionFactory(); + await permissionFactory(); + await permissionFactory(); + + const qry = ` + query permissions($page: Int $perPage: Int ${commonParamDefs}) { + permissions(page: $page perPage: $perPage ${commonParams}) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'permissions', { page: 1, perPage: 2 }); + + expect(response.length).toBe(2); + }); + + test(`Permissions by module`, async () => { + await permissionFactory({ module: 'brands' }); + await permissionFactory({ module: 'brands' }); + await permissionFactory(); + + const qry = ` + query permissions(${commonParamDefs}) { + permissions(${commonParams}) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'permissions', { module: 'brands' }); + + expect(response.length).toBe(2); + }); + + test(`Permissions by action`, async () => { + await permissionFactory({ action: 'brandsAll' }); + await permissionFactory({ action: 'brandsAll' }); + await permissionFactory(); + + const qry = ` + query permissions(${commonParamDefs}) { + permissions(${commonParams}) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'permissions', { action: 'brandsAll' }); + + expect(response.length).toBe(2); + }); + + test(`Permissions by userId`, async () => { + const group = await usersGroupFactory({}); + await permissionFactory({ groupId: group._id }); + const user = await userFactory({ groupIds: [group._id] }); + + await permissionFactory({ userId: user._id }); + await permissionFactory({ userId: user._id }); + await permissionFactory(); + + const qry = ` + query permissions(${commonParamDefs}) { + permissions(${commonParams}) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'permissions', { userId: user._id }); + + expect(response.length).toBe(3); + }); + + test(`Permissions by groupId`, async () => { + await permissionFactory({ groupId: 'groupId' }); + await permissionFactory({ groupId: 'groupId' }); + await permissionFactory(); + + const qry = ` + query permissions(${commonParamDefs}) { + permissions(${commonParams}) { + _id + user { _id } + group { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'permissions', { groupId: 'groupId' }); + + expect(response.length).toBe(2); + }); + + test(`Permissions total count`, async () => { + await permissionFactory(); + await permissionFactory(); + await permissionFactory(); + + const qry = ` + query permissionsTotalCount { + permissionsTotalCount + } + `; + + const count = await graphqlRequest(qry, 'permissionsTotalCount'); + + expect(count).toBe(3); + }); + + test(`Permissions modules`, async () => { + const qry = ` + query permissionModules { + permissionModules { + name + description + } + } + `; + + const modules = await graphqlRequest(qry, 'permissionModules'); + + expect(modules.length).toBe(28); + }); + + test(`Permissions actions`, async () => { + const qry = ` + query permissionActions { + permissionActions { + name + description + module + } + } + `; + + const modules = await graphqlRequest(qry, 'permissionActions'); + + expect(modules.length).toBe(186); + }); }); describe('usersGroupQueries', () => { + afterEach(async () => { + // Clearing test data + await UsersGroups.deleteMany({}); + }); + test(`test if Error('Login required') exception is working as intended`, async () => { expect.assertions(2); @@ -36,4 +207,42 @@ describe('usersGroupQueries', () => { expectError(usersGroupQueries.usersGroups); expectError(usersGroupQueries.usersGroupsTotalCount); }); + + test(`User groups`, async () => { + await usersGroupFactory(); + await usersGroupFactory(); + const userGroup = await usersGroupFactory(); + + await userFactory({ groupIds: [userGroup._id] }); + + const qry = ` + query usersGroups($page: Int $perPage: Int) { + usersGroups(page: $page perPage: $perPage) { + _id + memberIds + members { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'usersGroups', { page: 1, perPage: 2 }); + + expect(response.length).toBe(2); + }); + + test(`User group total count`, async () => { + await usersGroupFactory(); + await usersGroupFactory(); + await usersGroupFactory(); + + const qry = ` + query usersGroupsTotalCount { + usersGroupsTotalCount + } + `; + + const count = await graphqlRequest(qry, 'usersGroupsTotalCount'); + + expect(count).toBe(3); + }); }); diff --git a/src/__tests__/pipelineLabelDb.test.ts b/src/__tests__/pipelineLabelDb.test.ts new file mode 100644 index 000000000..7abb1bb49 --- /dev/null +++ b/src/__tests__/pipelineLabelDb.test.ts @@ -0,0 +1,196 @@ +import { + dealFactory, + growthHackFactory, + pipelineFactory, + pipelineLabelFactory, + taskFactory, + ticketFactory, + userFactory, +} from '../db/factories'; +import { Deals, GrowthHacks, PipelineLabels, Pipelines, Tasks, Tickets } from '../db/models'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; + +import { IPipelineDocument } from '../db/models/definitions/boards'; +import { BOARD_TYPES } from '../db/models/definitions/constants'; +import { IUserDocument } from '../db/models/definitions/users'; +import './setup.ts'; + +describe('Test pipeline label model', () => { + let pipelineLabel: IPipelineLabelDocument; + let duplicatedPipelineLabel: IPipelineLabelDocument; + let pipeline: IPipelineDocument; + let duplicatedPipeline: IPipelineDocument; + let user: IUserDocument; + + beforeEach(async () => { + // Creating test data + pipeline = await pipelineFactory({ type: BOARD_TYPES.DEAL }); + duplicatedPipeline = await pipelineFactory({ type: BOARD_TYPES.DEAL }); + + pipelineLabel = await pipelineLabelFactory({ pipelineId: pipeline._id }); + duplicatedPipelineLabel = await pipelineLabelFactory({ pipelineId: duplicatedPipeline._id }); + + user = await userFactory(); + }); + + afterEach(async () => { + // Clearing test data + await PipelineLabels.deleteMany({}); + await Pipelines.deleteMany({}); + }); + + test('Get pipeline label', async () => { + try { + await PipelineLabels.getPipelineLabel('fakeId'); + } catch (e) { + expect(e.message).toBe('Label not found'); + } + + const response = await PipelineLabels.getPipelineLabel(pipelineLabel._id); + + expect(response).toBeDefined(); + }); + + // Create pipeline label + test('Create pipeline label', async () => { + const name = 'Name'; + const colorCode = 'colorCode'; + + const created = await PipelineLabels.createPipelineLabel({ + name, + colorCode, + pipelineId: pipeline._id, + createdBy: user._id, + }); + + expect(created).toBeDefined(); + expect(created.name).toEqual(name); + expect(created.colorCode).toEqual(colorCode); + expect(created.pipelineId).toEqual(pipeline._id); + }); + + test('Update pipeline label (Error: Label duplicated)', async () => { + const duplicatedLabel = await pipelineLabelFactory(); + + try { + await PipelineLabels.updatePipelineLabel(pipelineLabel._id, { + name: duplicatedLabel.name, + colorCode: duplicatedLabel.colorCode, + pipelineId: duplicatedLabel.pipelineId, + }); + } catch (e) { + expect(e.message).toBe('Label duplicated'); + } + }); + + test('Update pipeline label', async () => { + const name = 'Updated name'; + const colorCode = 'Updated colorCode'; + const pipelineId = 'Updated pipelineId'; + + const updated = await PipelineLabels.updatePipelineLabel(pipelineLabel._id, { + name, + colorCode, + pipelineId, + }); + + expect(updated).toBeDefined(); + expect(updated.name).toEqual(name); + expect(updated.colorCode).toEqual(colorCode); + expect(updated.pipelineId).toEqual(pipelineId); + }); + + // Test pipeline label + test('Pipeline label (Label duplicated)', async () => { + try { + await PipelineLabels.createPipelineLabel({ + name: duplicatedPipelineLabel.name, + colorCode: duplicatedPipelineLabel.colorCode, + pipelineId: duplicatedPipeline._id, + createdBy: user._id, + }); + } catch (e) { + expect(e.message).toBe('Label duplicated'); + } + }); + + test('Remove pipeline label', async () => { + const dealPipeline = await pipelineFactory({ type: 'deal' }); + const taskPipeline = await pipelineFactory({ type: 'task' }); + const ticketPipeline = await pipelineFactory({ type: 'ticket' }); + const growthHackPipeline = await pipelineFactory({ type: 'growthHack' }); + + const dealLabel = await pipelineLabelFactory({ type: 'deal', pipelineId: dealPipeline._id }); + const taskLabel = await pipelineLabelFactory({ type: 'task', pipelineId: taskPipeline._id }); + const ticketLabel = await pipelineLabelFactory({ type: 'ticket', pipelineId: ticketPipeline._id }); + const growthHackLabel = await pipelineLabelFactory({ type: 'growthHack', pipelineId: growthHackPipeline._id }); + + await dealFactory({ labelIds: [dealLabel._id] }); + await taskFactory({ labelIds: [taskLabel._id] }); + await ticketFactory({ labelIds: [ticketLabel._id] }); + await growthHackFactory({ labelIds: [growthHackLabel._id] }); + + await PipelineLabels.removePipelineLabel(dealLabel._id); + + expect(await Deals.find({ labelIds: [dealLabel._id] }).countDocuments()).toBe(0); + + await PipelineLabels.removePipelineLabel(taskLabel._id); + + expect(await Tasks.find({ labelIds: [taskLabel._id] }).countDocuments()).toBe(0); + + await PipelineLabels.removePipelineLabel(ticketLabel._id); + + expect(await Tickets.find({ labelIds: [ticketLabel._id] }).countDocuments()).toBe(0); + + await PipelineLabels.removePipelineLabel(growthHackLabel._id); + + expect(await GrowthHacks.find({ labelIds: [growthHackLabel._id] }).countDocuments()).toBe(0); + }); + + test('Remove pipeline label not found', async () => { + expect.assertions(1); + + const fakeId = 'fakeId'; + + try { + await PipelineLabels.removePipelineLabel(fakeId); + } catch (e) { + expect(e.message).toEqual('Label not found'); + } + }); + + test('Pipeline labels label', async () => { + const deal = await dealFactory(); + + const targetId = deal._id; + + const pipelineLabelTwo = await pipelineLabelFactory(); + + const labelIds = [pipelineLabel._id, pipelineLabelTwo._id]; + + // add label to specific object + await PipelineLabels.labelsLabel(pipeline._id, targetId, labelIds); + + const obj = await Deals.getDeal(deal._id); + + const updatedLabelIds = obj.labelIds || []; + + expect(updatedLabelIds[0]).toEqual(pipelineLabel.id); + expect(updatedLabelIds[1]).toEqual(pipelineLabelTwo.id); + expect(updatedLabelIds.length).toEqual(2); + }); + + test('Pipeline labels label (Error: Label not found)', async () => { + const deal = await dealFactory(); + const targetId = deal._id; + + const labelIds = ['fakeId']; + + try { + // add label to specific object + await PipelineLabels.labelsLabel(pipeline._id, targetId, labelIds); + } catch (e) { + expect(e.message).toBe('Label not found'); + } + }); +}); diff --git a/src/__tests__/pipelineLabelMutations.test.ts b/src/__tests__/pipelineLabelMutations.test.ts new file mode 100644 index 000000000..370b79caf --- /dev/null +++ b/src/__tests__/pipelineLabelMutations.test.ts @@ -0,0 +1,121 @@ +import * as faker from 'faker'; +import { graphqlRequest } from '../db/connection'; +import { dealFactory, pipelineFactory, pipelineLabelFactory, userFactory } from '../db/factories'; +import { Deals, PipelineLabels, Pipelines, Users } from '../db/models'; + +import './setup.ts'; + +/* + * Generate test data + */ + +describe('PipelineLabels mutations', () => { + let pipeline; + let pipelineLabel; + let user; + + const commonParamDefs = ` + $name: String! + $pipelineId: String! + $colorCode: String! + `; + + const commonParams = ` + name: $name + pipelineId: $pipelineId + colorCode: $colorCode + `; + + const commonReturn = ` + name + pipelineId + colorCode + `; + + const args = { + name: faker.name.findName(), + pipelineId: faker.random.word(), + colorCode: faker.random.word(), + }; + + beforeEach(async () => { + user = await userFactory(); + pipeline = await pipelineFactory(); + pipelineLabel = await pipelineLabelFactory({ pipelineId: pipeline._id, createdBy: user._id }); + }); + + afterEach(async () => { + // Clearing test data + await PipelineLabels.deleteMany({}); + await Pipelines.deleteMany({}); + await Users.deleteMany({}); + }); + + test('Add pipelineLabel', async () => { + const mutation = ` + mutation pipelineLabelsAdd(${commonParamDefs}){ + pipelineLabelsAdd(${commonParams}) { + ${commonReturn} + } + } + `; + + args.pipelineId = pipeline._id; + + const created = await graphqlRequest(mutation, 'pipelineLabelsAdd', args); + + expect(created.name).toBe(args.name); + expect(created.pipelineId).toBe(args.pipelineId); + expect(created.colorCode).toBe(args.colorCode); + }); + + test('Edit pipelineLabel', async () => { + const mutation = ` + mutation pipelineLabelsEdit($_id: String! ${commonParamDefs}){ + pipelineLabelsEdit(_id: $_id ${commonParams}) { + _id + ${commonReturn} + } + } + `; + + const edited = await graphqlRequest(mutation, 'pipelineLabelsEdit', { _id: pipelineLabel._id, ...args }); + + expect(edited._id).toBe(pipelineLabel._id); + expect(edited.name).toBe(args.name); + expect(edited.colorCode).toBe(args.colorCode); + expect(edited.pipelineId).toBe(args.pipelineId); + }); + + test('Remove pipelineLabel', async () => { + const mutation = ` + mutation pipelineLabelsRemove($_id: String!) { + pipelineLabelsRemove(_id: $_id) + } + `; + + await graphqlRequest(mutation, 'pipelineLabelsRemove', { _id: pipelineLabel._id }); + + expect(await PipelineLabels.find({ _id: { $in: [pipelineLabel._id] } })).toEqual([]); + }); + + test('Pipeline labels label', async () => { + const mutation = ` + mutation pipelineLabelsLabel($pipelineId: String!, $targetId: String!, $labelIds: [String!]!) { + pipelineLabelsLabel(pipelineId: $pipelineId, targetId: $targetId, labelIds: $labelIds) + } + `; + + const deal = await dealFactory(); + + const pipelineLabelsLabelArgs = { + pipelineId: pipeline._id, + targetId: deal._id, + labelIds: [pipelineLabel._id], + }; + + await graphqlRequest(mutation, 'pipelineLabelsLabel', pipelineLabelsLabelArgs); + + expect((await Deals.getDeal(deal._id)).labelIds).toContain(pipelineLabel._id); + }); +}); diff --git a/src/__tests__/pipelineLabelQueries.test.ts b/src/__tests__/pipelineLabelQueries.test.ts new file mode 100644 index 000000000..6b036e261 --- /dev/null +++ b/src/__tests__/pipelineLabelQueries.test.ts @@ -0,0 +1,54 @@ +import { graphqlRequest } from '../db/connection'; +import { pipelineFactory, pipelineLabelFactory } from '../db/factories'; +import { PipelineLabels } from '../db/models'; + +import './setup.ts'; + +describe('pipelineLabelQueries', () => { + afterEach(async () => { + // Clearing test data + await PipelineLabels.deleteMany({}); + }); + + test('Pipeline labels', async () => { + const pipeline = await pipelineFactory(); + const pipelineId = pipeline._id; + + const args = { pipelineId }; + + await pipelineLabelFactory({ pipelineId }); + await pipelineLabelFactory({ pipelineId }); + await pipelineLabelFactory({ pipelineId }); + + const qry = ` + query pipelineLabels($pipelineId: String!) { + pipelineLabels(pipelineId: $pipelineId) { + _id + name + pipelineId + colorCode + } + } + `; + + const response = await graphqlRequest(qry, 'pipelineLabels', args); + + expect(response.length).toBe(3); + }); + + test('Pipeline label detail', async () => { + const qry = ` + query pipelineLabelDetail($_id: String!) { + pipelineLabelDetail(_id: $_id) { + _id + } + } + `; + + const pipelineLabel = await pipelineLabelFactory(); + + const response = await graphqlRequest(qry, 'pipelineLabelDetail', { _id: pipelineLabel._id }); + + expect(response._id).toBe(pipelineLabel._id); + }); +}); diff --git a/src/__tests__/pipelineTemplateDb.test.ts b/src/__tests__/pipelineTemplateDb.test.ts new file mode 100644 index 000000000..d5896f16e --- /dev/null +++ b/src/__tests__/pipelineTemplateDb.test.ts @@ -0,0 +1,120 @@ +import * as faker from 'faker'; +import { formFactory, pipelineTemplateFactory } from '../db/factories'; +import { PipelineTemplates } from '../db/models'; +import { IPipelineTemplateDocument, IPipelineTemplateStage } from '../db/models/definitions/pipelineTemplates'; + +import './setup.ts'; + +describe('Test pipeline template model', () => { + let pipelineTemplate: IPipelineTemplateDocument; + const stages: IPipelineTemplateStage[] = [ + { _id: Math.random().toString(), name: faker.random.word(), formId: faker.random.word() }, + { _id: Math.random().toString(), name: faker.random.word(), formId: faker.random.word() }, + ]; + + beforeEach(async () => { + // Creating test data + pipelineTemplate = await pipelineTemplateFactory(); + }); + + afterEach(async () => { + // Clearing test data + await PipelineTemplates.deleteMany({}); + }); + + test('Get pipeline template', async () => { + try { + await PipelineTemplates.getPipelineTemplate('fakeId'); + } catch (e) { + expect(e.message).toBe('Pipeline template not found'); + } + + const response = await PipelineTemplates.getPipelineTemplate(pipelineTemplate._id); + + expect(response).toBeDefined(); + }); + + // Test deal pipeline template + test('Create pipeline template', async () => { + const created = await PipelineTemplates.createPipelineTemplate( + { + name: pipelineTemplate.name, + description: pipelineTemplate.description || '', + type: pipelineTemplate.type, + }, + stages, + ); + + expect(created).toBeDefined(); + expect(created.name).toEqual(pipelineTemplate.name); + expect(created.description).toEqual(pipelineTemplate.description); + expect(created.type).toEqual(pipelineTemplate.type); + expect(created.stages.length).toEqual(pipelineTemplate.stages.length); + }); + + test('Update pipeline template', async () => { + const name = 'Updated name'; + const description = 'Updated description'; + const type = 'Updated type'; + + const updated = await PipelineTemplates.updatePipelineTemplate( + pipelineTemplate._id, + { + name, + description, + type, + }, + stages, + ); + + expect(updated).toBeDefined(); + expect(updated.name).toEqual(name); + expect(updated.description).toEqual(description); + expect(updated.type).toEqual(type); + expect(updated.stages.length).toEqual(pipelineTemplate.stages.length); + }); + + test(`Duplicate pipeline template Error('pipeline template not found')`, async () => { + try { + await PipelineTemplates.duplicatePipelineTemplate('fakeId'); + } catch (e) { + expect(e.message).toBe('Pipeline template not found'); + } + }); + + test('Duplicate pipeline template', async () => { + const form1 = await formFactory(); + const form2 = await formFactory(); + + // Creating test data + const template = await pipelineTemplateFactory({ + stages: [{ name: 'stage 1', formId: form1._id }, { name: 'stage 2', formId: form2._id }], + }); + + const duplicated = await PipelineTemplates.duplicatePipelineTemplate(template._id); + + expect(duplicated).toBeDefined(); + expect(duplicated.description).toEqual(template.description); + expect(duplicated.type).toEqual(template.type); + expect(duplicated.stages[0].name).toBe('stage 1'); + expect(duplicated.stages[1].name).toBe('stage 2'); + }); + + test('Remove pipeline template', async () => { + const isDeleted = await PipelineTemplates.removePipelineTemplate(pipelineTemplate._id); + + expect(isDeleted).toBeTruthy(); + }); + + test(`Remove pipeline Error('pipeline template not found')`, async () => { + expect.assertions(1); + + const fakeId = 'fakeId'; + + try { + await PipelineTemplates.removePipelineTemplate(fakeId); + } catch (e) { + expect(e.message).toEqual('Pipeline template not found'); + } + }); +}); diff --git a/src/__tests__/pipelineTemplateMutations.test.ts b/src/__tests__/pipelineTemplateMutations.test.ts new file mode 100644 index 000000000..7081c7890 --- /dev/null +++ b/src/__tests__/pipelineTemplateMutations.test.ts @@ -0,0 +1,145 @@ +import * as faker from 'faker'; +import { graphqlRequest } from '../db/connection'; +import { formFactory, pipelineTemplateFactory } from '../db/factories'; +import { Forms, PipelineTemplates } from '../db/models'; + +import { BOARD_TYPES } from '../db/models/definitions/constants'; +import './setup.ts'; + +/* + * Generate test data + */ + +describe('PipelineTemplates mutations', () => { + let pipelineTemplate; + + const commonParamDefs = ` + $name: String! + $description: String + $type: String! + $stages: [PipelineTemplateStageInput] + `; + + const commonParams = ` + name: $name + description: $description + type: $type + stages: $stages + `; + + const commonReturn = ` + name + description + type + stages { + name + formId + order + } + `; + + const args = { + name: faker.name.findName(), + description: faker.random.word(), + type: BOARD_TYPES.GROWTH_HACK, + stages: [ + { _id: Math.random().toString(), name: 'Stage 1', formId: 'formId1' }, + { _id: Math.random().toString(), name: 'Stage 2', formId: 'formId2' }, + { _id: Math.random().toString(), name: 'Stage 3', formId: 'formId3' }, + { _id: Math.random().toString(), name: 'Stage 4', formId: 'formId4' }, + ], + }; + + beforeEach(async () => { + // Creating test data + pipelineTemplate = await pipelineTemplateFactory(); + }); + + afterEach(async () => { + // Clearing test data + await PipelineTemplates.deleteMany({}); + await Forms.deleteMany({}); + }); + + test('Add pipelineTemplate', async () => { + const mutation = ` + mutation pipelineTemplatesAdd(${commonParamDefs}){ + pipelineTemplatesAdd(${commonParams}) { + ${commonReturn} + } + } + `; + + const created = await graphqlRequest(mutation, 'pipelineTemplatesAdd', args); + + expect(created.name).toBe(args.name); + expect(created.description).toBe(args.description); + expect(created.type).toBe(args.type); + + expect(created.stages[0].formId).toBe('formId1'); + expect(created.stages[1].formId).toBe('formId2'); + expect(created.stages[2].formId).toBe('formId3'); + expect(created.stages[3].formId).toBe('formId4'); + }); + + test('Edit pipelineTemplate', async () => { + const mutation = ` + mutation pipelineTemplatesEdit($_id: String! ${commonParamDefs}){ + pipelineTemplatesEdit(_id: $_id ${commonParams}) { + _id + ${commonReturn} + } + } + `; + + const edited = await graphqlRequest(mutation, 'pipelineTemplatesEdit', { _id: pipelineTemplate._id, ...args }); + + expect(edited._id).toBe(pipelineTemplate._id); + expect(edited.name).toBe(args.name); + expect(edited.type).toBe(args.type); + expect(edited.description).toBe(args.description); + expect(edited.stages.length).toBe(args.stages.length); + }); + + test('Remove pipelineTemplate', async () => { + const mutation = ` + mutation pipelineTemplatesRemove($_id: String!) { + pipelineTemplatesRemove(_id: $_id) + } + `; + + await graphqlRequest(mutation, 'pipelineTemplatesRemove', { _id: pipelineTemplate._id }); + + expect(await PipelineTemplates.find({ _id: { $in: [pipelineTemplate._id] } })).toEqual([]); + }); + + test('Duplicate pipelineTemplate', async () => { + const mutation = ` + mutation pipelineTemplatesDuplicate($_id: String!) { + pipelineTemplatesDuplicate(_id: $_id) { + _id + name + description + stages { + name + formId + } + } + } + `; + + const form1 = await formFactory(); + const form2 = await formFactory(); + + // Creating test data + const template = await pipelineTemplateFactory({ + stages: [{ name: 'stage 1', formId: form1._id }, { name: 'stage 2', formId: form2._id }], + }); + + const duplicated = await graphqlRequest(mutation, 'pipelineTemplatesDuplicate', { _id: template._id }); + + expect(duplicated.description).toBe(template.description); + expect(duplicated.stages[0].name).toBe('stage 1'); + expect(duplicated.stages[1].name).toBe('stage 2'); + }); +}); diff --git a/src/__tests__/pipelineTemplateQueries.test.ts b/src/__tests__/pipelineTemplateQueries.test.ts new file mode 100644 index 000000000..030462d02 --- /dev/null +++ b/src/__tests__/pipelineTemplateQueries.test.ts @@ -0,0 +1,70 @@ +import { graphqlRequest } from '../db/connection'; +import { pipelineTemplateFactory } from '../db/factories'; +import { PipelineTemplates } from '../db/models'; + +import { BOARD_TYPES } from '../db/models/definitions/constants'; +import './setup.ts'; + +describe('pipelineTemplateQueries', () => { + afterEach(async () => { + // Clearing test data + await PipelineTemplates.deleteMany({}); + }); + + test('Pipeline templates', async () => { + const args = { + type: BOARD_TYPES.GROWTH_HACK, + }; + + await pipelineTemplateFactory(); + await pipelineTemplateFactory(); + await pipelineTemplateFactory(); + + const qry = ` + query pipelineTemplates($type: String!) { + pipelineTemplates(type: $type) { + _id + name + description + } + } + `; + + const response = await graphqlRequest(qry, 'pipelineTemplates', args); + + expect(response.length).toBe(3); + }); + + test('Pipeline template detail', async () => { + const qry = ` + query pipelineTemplateDetail($_id: String!) { + pipelineTemplateDetail(_id: $_id) { + _id + } + } + `; + + const pipelineTemplate = await pipelineTemplateFactory(); + + const response = await graphqlRequest(qry, 'pipelineTemplateDetail', { _id: pipelineTemplate._id }); + + expect(response._id).toBe(pipelineTemplate._id); + }); + + test('Pipeline template total count', async () => { + await pipelineTemplateFactory(); + await pipelineTemplateFactory(); + await pipelineTemplateFactory(); + await pipelineTemplateFactory(); + + const qry = ` + query pipelineTemplatesTotalCount { + pipelineTemplatesTotalCount + } + `; + + const response = await graphqlRequest(qry, 'pipelineTemplatesTotalCount'); + + expect(response).toBe(4); + }); +}); diff --git a/src/__tests__/productDb.test.ts b/src/__tests__/productDb.test.ts index 82b8cfb1d..0ffd044c9 100644 --- a/src/__tests__/productDb.test.ts +++ b/src/__tests__/productDb.test.ts @@ -1,78 +1,207 @@ -import { dealFactory, productFactory } from '../db/factories'; -import { Deals, Products } from '../db/models'; +import { dealFactory, fieldFactory, productCategoryFactory, productFactory } from '../db/factories'; +import { Deals, ProductCategories, Products } from '../db/models'; +import { IDealDocument, IProductCategoryDocument, IProductDocument } from '../db/models/definitions/deals'; import './setup.ts'; describe('Test products model', () => { - let product; - let deal; + let product: IProductDocument; + let deal: IDealDocument; + let deal2: IDealDocument; + let productCategory: IProductCategoryDocument; beforeEach(async () => { // Creating test data product = await productFactory({ type: 'service' }); + productCategory = await productCategoryFactory({}); deal = await dealFactory({ productsData: [{ productId: product._id }] }); + deal2 = await dealFactory({ productsData: [{ productId: product._id }] }); }); afterEach(async () => { // Clearing test data await Products.deleteMany({}); await Deals.deleteMany({}); + await ProductCategories.deleteMany({}); + }); + + test('Get product', async () => { + try { + await Products.getProduct({ _id: 'fakeId' }); + } catch (e) { + expect(e.message).toBe('Product not found'); + } + + const response = await Products.getProduct({ _id: product._id }); + + expect(response).toBeDefined(); }); test('Create product', async () => { - const productObj = await Products.createProduct({ + const args: any = { name: product.name, type: product.type, description: product.description, sku: product.sku, - }); + categoryId: productCategory._id, + code: '123', + }; + + let productObj = await Products.createProduct(args); expect(productObj).toBeDefined(); expect(productObj.name).toEqual(product.name); expect(productObj.type).toEqual(product.type); expect(productObj.description).toEqual(product.description); expect(productObj.sku).toEqual(product.sku); + + // testing product category + args.categoryCode = productCategory.code; + args.code = '234'; + productObj = await Products.createProduct(args); + + expect(productObj.categoryId).toBe(productCategory._id); }); test('Update product', async () => { - const productObj = await Products.updateProduct(product._id, { + const args: any = { name: `${product.name}-update`, type: `${product.type}-update`, description: `${product.description}-update`, sku: `${product.sku}-update`, - }); + categoryId: productCategory._id, + code: '321', + }; + + let productObj = await Products.updateProduct(product._id, args); expect(productObj).toBeDefined(); expect(productObj.name).toEqual(`${product.name}-update`); expect(productObj.type).toEqual(`${product.type}-update`); expect(productObj.description).toEqual(`${product.description}-update`); expect(productObj.sku).toEqual(`${product.sku}-update`); - }); - test('Remove product not found', async () => { - expect.assertions(1); + // testing custom field data + const field1 = await fieldFactory({ contentType: 'product', contentTypeId: product._id }); + const field2 = await fieldFactory({ contentType: 'product', contentTypeId: product._id, validation: 'date' }); - try { - await Products.removeProduct(deal._id); - } catch (e) { - expect(e.message).toEqual('Product not found'); + args.customFieldsData = [ + { field: field1._id, value: 10 }, + { field: field2._id, value: '2011-01-01' }, + ]; + + productObj = await Products.updateProduct(product._id, args); + + if (productObj.customFieldsData) { + expect(productObj.customFieldsData[0].value).toBe(10); } }); - test("Can't remove a product", async () => { + test('Can not remove products', async () => { expect.assertions(1); try { - await Products.removeProduct(product._id); + await Products.removeProducts([product._id]); } catch (e) { - expect(e.message).toEqual("Can't remove a product"); + expect(e.message).toEqual(`Can not remove products. Following deals are used ${deal.name},${deal2.name}`); } }); test('Remove product', async () => { await Deals.updateOne({ _id: deal._id }, { $set: { productsData: [] } }); - const isDeleted = await Products.removeProduct(product.id); + await Deals.updateOne({ _id: deal2._id }, { $set: { productsData: [] } }); + + const isDeleted = await Products.removeProducts([product._id]); expect(isDeleted).toBeTruthy(); }); + + test('Get product category', async () => { + try { + await ProductCategories.getProductCatogery({ _id: 'fakeId' }); + } catch (e) { + expect(e.message).toBe('Product & service category not found'); + } + + const response = await ProductCategories.getProductCatogery({ _id: productCategory._id }); + + expect(response).toBeDefined(); + }); + + test('Create product category', async () => { + const doc: any = { + name: 'Product name', + code: 'create1234', + }; + + let response = await ProductCategories.createProductCategory(doc); + + expect(response.name).toBe(doc.name); + expect(response.code).toBe(doc.code); + + // if parentId + doc.parentId = productCategory._id; + doc.code = 'create12345'; + + response = await ProductCategories.createProductCategory(doc); + + expect(response.parentId).toBe(productCategory._id); + }); + + test('Update product category (Error: Cannot change category)', async () => { + const parentCategory = await productCategoryFactory({ parentId: productCategory._id }); + + const doc: any = { + name: 'Updated product name', + code: 'error1234', + parentId: parentCategory._id, + }; + + try { + await ProductCategories.updateProductCategory(productCategory._id, doc); + } catch (e) { + expect(e.message).toBe('Cannot change category'); + } + }); + + test('Update product category', async () => { + const doc: any = { + name: 'Updated product name', + code: 'update1234', + }; + + let response = await ProductCategories.updateProductCategory(productCategory._id, doc); + + expect(response.name).toBe(doc.name); + expect(response.code).toBe(doc.code); + + // add child category + const childCategory = await ProductCategories.createProductCategory({ + name: 'name', + code: 'create123456', + parentId: productCategory._id, + order: 'order', + }); + + response = await ProductCategories.updateProductCategory(productCategory._id, doc); + + expect(childCategory.order).toBe(`${response.order}/${childCategory.name}${childCategory.code}`); + }); + + test('Remove product category', async () => { + await ProductCategories.removeProductCategory(productCategory._id); + + expect(await ProductCategories.find().countDocuments()).toBe(0); + }); + + test('Remove product category (Error: Can`t remove a product category)', async () => { + await productFactory({ categoryId: productCategory._id }); + await productFactory({ categoryId: productCategory._id }); + + try { + await ProductCategories.removeProductCategory(productCategory._id); + } catch (e) { + expect(e.message).toBe("Can't remove a product category"); + } + }); }); diff --git a/src/__tests__/productMutations.test.ts b/src/__tests__/productMutations.test.ts index 02ddaeaf6..0f50cdac8 100644 --- a/src/__tests__/productMutations.test.ts +++ b/src/__tests__/productMutations.test.ts @@ -1,36 +1,61 @@ import { graphqlRequest } from '../db/connection'; -import { productFactory, userFactory } from '../db/factories'; -import { Products } from '../db/models'; +import { productCategoryFactory, productFactory, tagsFactory } from '../db/factories'; +import { ProductCategories, Products, Tags } from '../db/models'; import './setup.ts'; describe('Test products mutations', () => { let product; - let context; + let productCategory; + let parentCategory; const commonParamDefs = ` $name: String!, $type: String!, $description: String, + $categoryId: String, $sku: String + $code: String `; const commonParams = ` name: $name type: $type description: $description, + categoryId: $categoryId sku: $sku + code: $code + `; + + const commonCategoryParamDefs = ` + $name: String!, + $code: String!, + $description: String, + $parentId: String, + `; + + const commonCategoryParams = ` + name: $name, + code: $code, + description: $description, + parentId: $parentId, `; beforeEach(async () => { // Creating test data - product = await productFactory({ type: 'product' }); - context = { user: await userFactory({}) }; + parentCategory = await productCategoryFactory(); + productCategory = await productCategoryFactory({ parentId: parentCategory._id }); + + const tag = await tagsFactory(); + + product = await productFactory({ type: 'product', categoryId: productCategory._id, tagIds: [tag._id] }); }); afterEach(async () => { // Clearing test data await Products.deleteMany({}); + await ProductCategories.deleteMany({}); + await Tags.deleteMany({}); }); test('Create product', async () => { @@ -39,6 +64,8 @@ describe('Test products mutations', () => { type: product.type, sku: product.sku, description: product.description, + categoryId: productCategory._id, + code: '123', }; const mutation = ` @@ -49,25 +76,31 @@ describe('Test products mutations', () => { type description sku + code } } `; - const createdProduct = await graphqlRequest(mutation, 'productsAdd', args, context); + const createdProduct = await graphqlRequest(mutation, 'productsAdd', args); expect(createdProduct.name).toEqual(args.name); expect(createdProduct.type).toEqual(args.type); expect(createdProduct.description).toEqual(args.description); expect(createdProduct.sku).toEqual(args.sku); + expect(createdProduct.code).toEqual(args.code); }); test('Update product', async () => { + const category2 = await productCategoryFactory(); + const args = { _id: product._id, name: product.name, type: product.type, sku: product.sku, description: product.description, + code: product.code, + categoryId: category2._id, }; const mutation = ` @@ -78,11 +111,12 @@ describe('Test products mutations', () => { type description sku + code } } `; - const updatedProduct = await graphqlRequest(mutation, 'productsEdit', args, context); + const updatedProduct = await graphqlRequest(mutation, 'productsEdit', args); expect(updatedProduct.name).toEqual(args.name); expect(updatedProduct.type).toEqual(args.type); @@ -92,13 +126,85 @@ describe('Test products mutations', () => { test('Remove product', async () => { const mutation = ` - mutation productsRemove($_id: String!) { - productsRemove(_id: $_id) + mutation productsRemove($productIds: [String!]) { + productsRemove(productIds: $productIds) } `; - await graphqlRequest(mutation, 'productsRemove', { _id: product._id }, context); + await graphqlRequest(mutation, 'productsRemove', { productIds: [product._id] }); expect(await Products.findOne({ _id: product._id })).toBe(null); }); + + test('Create product category', async () => { + const args = { + name: productCategory.name, + code: '123', + description: productCategory.description, + parentId: productCategory._id, + }; + + const mutation = ` + mutation productCategoriesAdd(${commonCategoryParamDefs}) { + productCategoriesAdd(${commonCategoryParams}) { + _id + name + code + description + parentId + } + } + `; + + const createdProduct = await graphqlRequest(mutation, 'productCategoriesAdd', args); + + expect(createdProduct.name).toEqual(args.name); + expect(createdProduct.code).toEqual(args.code); + expect(createdProduct.description).toEqual(args.description); + expect(createdProduct.parentId).toEqual(args.parentId); + }); + + test('Update product category', async () => { + const secondParent = await productCategoryFactory(); + + const args = { + _id: productCategory._id, + name: 'updated', + code: 'updatedCode', + parentId: secondParent._id, + }; + + const mutation = ` + mutation productCategoriesEdit($_id: String!, ${commonCategoryParamDefs}) { + productCategoriesEdit(_id: $_id, ${commonCategoryParams}) { + _id + name + code + description + parentId + } + } + `; + + const updatedProductCategory = await graphqlRequest(mutation, 'productCategoriesEdit', args); + + expect(updatedProductCategory._id).toEqual(args._id); + expect(updatedProductCategory.name).toEqual(args.name); + expect(updatedProductCategory.code).toEqual(args.code); + }); + + test('Remove product category', async () => { + const mutation = ` + mutation productCategoriesRemove($_id: String!) { + productCategoriesRemove(_id: $_id) + } + `; + + // remove product before the category + await Products.remove({ categoryId: productCategory._id }); + + await graphqlRequest(mutation, 'productCategoriesRemove', { _id: productCategory._id }); + + expect(await ProductCategories.findOne({ _id: productCategory._id })).toBe(null); + }); }); diff --git a/src/__tests__/productQueries.test.ts b/src/__tests__/productQueries.test.ts index b7de689aa..378fddc64 100644 --- a/src/__tests__/productQueries.test.ts +++ b/src/__tests__/productQueries.test.ts @@ -1,7 +1,7 @@ import { graphqlRequest } from '../db/connection'; -import { productFactory } from '../db/factories'; -import { Products } from '../db/models'; -import { PRODUCT_TYPES } from '../db/models/definitions/constants'; +import { productCategoryFactory, productFactory, tagsFactory } from '../db/factories'; +import { ProductCategories, Products } from '../db/models'; +import { PRODUCT_TYPES, TAG_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; @@ -9,21 +9,20 @@ describe('productQueries', () => { afterEach(async () => { // Clearing test data await Products.deleteMany({}); + await ProductCategories.deleteMany({}); }); test('Products', async () => { - const args = { - page: 1, - perPage: 2, - }; + const category = await productCategoryFactory({ code: '1' }); + const tag = await tagsFactory({ type: TAG_TYPES.PRODUCT }); - await productFactory(); - await productFactory(); - await productFactory(); + const product = await productFactory({ categoryId: category._id, type: PRODUCT_TYPES.PRODUCT }); + await productFactory({ categoryId: category._id, type: PRODUCT_TYPES.SERVICE }); + await productFactory({ tagIds: [tag._id], type: PRODUCT_TYPES.SERVICE }); const qry = ` - query products($page: Int $perPage: Int) { - products(page: $page perPage: $perPage) { + query products($page: Int $perPage: Int $type: String $categoryId: String $ids: [String] $tag: String $searchValue: String) { + products(page: $page perPage: $perPage type: $type categoryId: $categoryId ids: $ids tag: $tag searchValue: $searchValue) { _id name type @@ -34,14 +33,32 @@ describe('productQueries', () => { } `; - const response = await graphqlRequest(qry, 'products', args); + let response = await graphqlRequest(qry, 'products', { page: 1, perPage: 2 }); expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'products', { type: PRODUCT_TYPES.PRODUCT }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'products', { categoryId: category._id }); + + expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'products', { ids: [product._id] }); + + expect(response[0]._id).toBe(product._id); + + response = await graphqlRequest(qry, 'products', { tag: tag._id }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'products', { searchValue: product.name }); + + expect(response[0].name).toBe(product.name); }); test('Products total count', async () => { - const args = { type: PRODUCT_TYPES.PRODUCT }; - await productFactory({ type: PRODUCT_TYPES.PRODUCT }); await productFactory({ type: PRODUCT_TYPES.SERVICE }); await productFactory({ type: PRODUCT_TYPES.PRODUCT }); @@ -52,8 +69,115 @@ describe('productQueries', () => { } `; - const response = await graphqlRequest(qry, 'productsTotalCount', args); + const args = { type: PRODUCT_TYPES.PRODUCT }; + + let response = await graphqlRequest(qry, 'productsTotalCount', args); expect(response).toBe(2); + + response = await graphqlRequest(qry, 'productsTotalCount'); + + expect(response).toBe(3); + }); + + test('Product categories', async () => { + const parent = await productCategoryFactory({ code: '1' }); + await productCategoryFactory({ parentId: parent._id, code: '2' }); + await productCategoryFactory({ parentId: parent._id, code: '3' }); + + const qry = ` + query productCategories($parentId: String $searchValue: String) { + productCategories(parentId: $parentId searchValue: $searchValue) { + _id + name + parentId + isRoot + productCount + } + } + `; + + let response = await graphqlRequest(qry, 'productCategories'); + + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'productCategories', { parentId: parent._id }); + + expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'productCategories', { searchValue: parent.name }); + + expect(response[0].name).toBe(parent.name); + }); + + test('Product categories total count', async () => { + await productCategoryFactory(); + await productCategoryFactory(); + await productCategoryFactory(); + await productCategoryFactory(); + + const qry = ` + query productCategoriesTotalCount { + productCategoriesTotalCount + } + `; + + const totalCount = await graphqlRequest(qry, 'productCategoriesTotalCount'); + + expect(totalCount).toBe(4); + }); + + test('Product detail', async () => { + const qry = ` + query productDetail($_id: String!) { + productDetail(_id: $_id) { + _id + category { _id } + getTags { _id } + } + } + `; + + const product = await productFactory(); + + const response = await graphqlRequest(qry, 'productDetail', { _id: product._id }); + + expect(response._id).toBe(product._id); + }); + + test('Product category detail', async () => { + const qry = ` + query productCategoryDetail($_id: String!) { + productCategoryDetail(_id: $_id) { + _id + } + } + `; + + const productCategory = await productCategoryFactory(); + + const response = await graphqlRequest(qry, 'productCategoryDetail', { _id: productCategory._id }); + + expect(response._id).toBe(productCategory._id); + }); + + test('Product count by tag', async () => { + const tag1 = await tagsFactory({ type: TAG_TYPES.PRODUCT }); + const tag2 = await tagsFactory({ type: TAG_TYPES.PRODUCT }); + + const qry = ` + query productCountByTags { + productCountByTags + } + `; + + await productFactory({ tagIds: [tag1._id, tag2._id] }); + await productFactory({ tagIds: [tag2._id] }); + await productFactory({ tagIds: [tag1._id, tag2._id] }); + + const response = await graphqlRequest(qry, 'productCountByTags'); + + expect(response[tag1._id]).toBe(2); + expect(response[tag2._id]).toBe(3); }); }); diff --git a/src/__tests__/responseTemplateDb.test.ts b/src/__tests__/responseTemplateDb.test.ts index 65246737b..ca4d3debb 100644 --- a/src/__tests__/responseTemplateDb.test.ts +++ b/src/__tests__/responseTemplateDb.test.ts @@ -16,6 +16,18 @@ describe('Response template db', () => { await ResponseTemplates.deleteMany({}); }); + test('Get response template', async () => { + try { + await ResponseTemplates.getResponseTemplate('fakeId'); + } catch (e) { + expect(e.message).toBe('Response template not found'); + } + + const response = await ResponseTemplates.getResponseTemplate(_responseTemplate._id); + + expect(response).toBeDefined(); + }); + test('Create response template', async () => { const responseTemplateObj = await ResponseTemplates.create({ name: _responseTemplate.name, diff --git a/src/__tests__/responseTemplateMutations.test.ts b/src/__tests__/responseTemplateMutations.test.ts index e785203ab..e2da3be32 100644 --- a/src/__tests__/responseTemplateMutations.test.ts +++ b/src/__tests__/responseTemplateMutations.test.ts @@ -1,5 +1,5 @@ import { graphqlRequest } from '../db/connection'; -import { responseTemplateFactory, userFactory } from '../db/factories'; +import { brandFactory, responseTemplateFactory, userFactory } from '../db/factories'; import { ResponseTemplates, Users } from '../db/models'; import './setup.ts'; @@ -65,9 +65,11 @@ describe('Response template mutations', () => { }); test('Edit response template', async () => { + const brand = await brandFactory(); + const args = { _id: _responseTemplate._id, - brandId: _responseTemplate.brandId, + brandId: brand._id, name: _responseTemplate.name, content: _responseTemplate.content, files: _responseTemplate.files, diff --git a/src/__tests__/responseTemplateQueries.test.ts b/src/__tests__/responseTemplateQueries.test.ts index 08f0e4245..cfa597097 100644 --- a/src/__tests__/responseTemplateQueries.test.ts +++ b/src/__tests__/responseTemplateQueries.test.ts @@ -1,54 +1,68 @@ import { graphqlRequest } from '../db/connection'; -import { responseTemplateFactory } from '../db/factories'; -import { ResponseTemplates } from '../db/models'; +import { brandFactory, responseTemplateFactory } from '../db/factories'; +import { Brands, ResponseTemplates } from '../db/models'; import './setup.ts'; describe('responseTemplateQueries', () => { + let brand; + let firstResponseTemplate; + + beforeEach(async () => { + // Clearing test data + brand = await brandFactory(); + + firstResponseTemplate = await responseTemplateFactory({ brandId: brand._id, name: 'first' }); + await responseTemplateFactory({ brandId: brand._id }); + await responseTemplateFactory(); + }); + afterEach(async () => { // Clearing test data await ResponseTemplates.deleteMany({}); + await Brands.deleteMany({}); }); test('Response templates', async () => { - // Creating test data - await responseTemplateFactory(); - await responseTemplateFactory(); - await responseTemplateFactory(); - const qry = ` query responseTemplates($page: Int, $perPage: Int, $searchValue: String, $brandId: String) { responseTemplates(page: $page, perPage: $perPage, searchValue: $searchValue, brandId: $brandId) { _id - name - brandId - content - brand { _id } - files } } `; - const response = await graphqlRequest(qry, 'responseTemplates', { page: 1, perPage: 2 }); + let response = await graphqlRequest(qry, 'responseTemplates', { page: 1, perPage: 2 }); + + expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'responseTemplates', { brandId: brand._id }); expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'responseTemplates', { searchValue: 'first' }); + + expect(response[0]._id).toBe(firstResponseTemplate._id); }); test('Get total count of response template', async () => { - // Creating test data - await responseTemplateFactory(); - await responseTemplateFactory(); - await responseTemplateFactory(); - const qry = ` - query responseTemplatesTotalCount { - responseTemplatesTotalCount + query responseTemplatesTotalCount($searchValue: String, $brandId: String) { + responseTemplatesTotalCount(searchValue: $searchValue, brandId: $brandId) } `; - const response = await graphqlRequest(qry, 'responseTemplatesTotalCount'); + let totalCount = await graphqlRequest(qry, 'responseTemplatesTotalCount'); + + expect(totalCount).toBe(3); + + totalCount = await graphqlRequest(qry, 'responseTemplatesTotalCount', { brandId: brand._id }); + + expect(totalCount).toBe(2); + + totalCount = await graphqlRequest(qry, 'responseTemplatesTotalCount', { searchValue: 'first' }); - expect(response).toBe(3); + expect(totalCount).toBe(1); }); }); diff --git a/src/__tests__/scriptDb.test.ts b/src/__tests__/scriptDb.test.ts new file mode 100644 index 000000000..3a66aeb4b --- /dev/null +++ b/src/__tests__/scriptDb.test.ts @@ -0,0 +1,139 @@ +import { brandFactory, formFactory, integrationFactory, scriptFactory } from '../db/factories'; +import { Scripts } from '../db/models'; +import './setup.ts'; + +describe('Script model tests', () => { + afterEach(async () => { + // Clearing test data + await Scripts.deleteMany({}); + }); + + test('Get script', async () => { + try { + await Scripts.getScript('fakeId'); + } catch (e) { + expect(e.message).toBe('Script not found'); + } + + const script = await scriptFactory({}); + + const response = await Scripts.getScript(script._id); + + expect(response).toBeDefined(); + }); + + test('Create script', async () => { + const doc = { name: 'script' }; + + const response = await Scripts.createScript(doc); + + expect(response.name).toBe(doc.name); + }); + + test('Create script (Messenger)', async () => { + const brand = await brandFactory(); + const messenger = await integrationFactory({ kind: 'messenger', brandId: brand._id }); + + const doc = { + name: 'script', + messengerId: messenger._id, + }; + + const response = await Scripts.createScript(doc); + + expect(response.name).toBe(doc.name); + expect(response.messengerId).toBe(doc.messengerId); + }); + + test('Create script when messenger (Error: Brand not found)', async () => { + const messenger = await integrationFactory({ kind: 'messenger' }); + + const doc = { + name: 'script', + messengerId: messenger._id, + }; + + try { + await Scripts.createScript(doc); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + }); + + test('Create script when lead', async () => { + const brand = await brandFactory(); + const form = await formFactory(); + + const lead = await integrationFactory({ kind: 'lead', formId: form._id, brandId: brand._id }); + + const doc = { + name: 'script', + leadIds: [lead._id], + }; + + const response = await Scripts.createScript(doc); + + expect(response.name).toBe(doc.name); + expect(response.leadIds).toContain(lead._id); + }); + + test('Create script when lead (Error: Brand not found)', async () => { + const form = await formFactory(); + + const lead = await integrationFactory({ kind: 'lead', formId: form._id }); + + const doc = { + name: 'script', + leadIds: [lead._id], + }; + + try { + await Scripts.createScript(doc); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + }); + + test('Create script when lead (Error: Form not found)', async () => { + const brand = await brandFactory(); + + const lead = await integrationFactory({ kind: 'lead', brandId: brand._id }); + + const doc = { + name: 'script', + leadIds: [lead._id], + }; + + try { + await Scripts.createScript(doc); + } catch (e) { + expect(e.message).toBe('Form not found'); + } + }); + + test('Update script', async () => { + const doc = { name: 'script' }; + + const script = await scriptFactory({}); + + const response = await Scripts.updateScript(script._id, doc); + + expect(response.name).toBe(doc.name); + }); + + test('Remove script', async () => { + const script = await scriptFactory({}); + + await Scripts.removeScript(script._id); + + expect(await Scripts.findOne({ _id: script._id })).toBeDefined(); + }); + + test('Remove script (Error: Script not found)', async () => { + try { + await Scripts.removeScript('fakeId'); + } catch (e) { + expect(e.message).toBe('Script not found with id fakeId'); + } + }); +}); diff --git a/src/__tests__/scriptMutations.test.ts b/src/__tests__/scriptMutations.test.ts index bb122ab68..e65beecd6 100644 --- a/src/__tests__/scriptMutations.test.ts +++ b/src/__tests__/scriptMutations.test.ts @@ -1,24 +1,38 @@ import { graphqlRequest } from '../db/connection'; -import { userFactory } from '../db/factories'; -import { Scripts, Users } from '../db/models'; +import { brandFactory, formFactory, integrationFactory } from '../db/factories'; +import { Brands, Forms, Integrations, Scripts, Users } from '../db/models'; import './setup.ts'; describe('scriptMutations', () => { - let _user; - let context; + let doc; + let lead; + let messenger; + let brand; beforeEach(async () => { // Creating test data - _user = await userFactory({}); + const form = await formFactory(); - context = { user: _user }; + brand = await brandFactory(); + lead = await integrationFactory({ formId: form._id, kind: 'lead', brandId: brand._id }); + messenger = await integrationFactory({ kind: 'messenger', brandId: brand._id }); + + doc = { + name: 'name', + messengerId: messenger._id, + leadIds: [lead._id], + kbTopicId: 'kbTopicId', + }; }); afterEach(async () => { // Clearing test data await Users.deleteMany({}); await Scripts.deleteMany({}); + await Integrations.deleteMany({}); + await Forms.deleteMany({}); + await Brands.deleteMany({}); }); const commonParamDefs = ` @@ -35,13 +49,6 @@ describe('scriptMutations', () => { leadIds: $leadIds `; - const doc = { - name: 'name', - messengerId: 'messengerId', - leadIds: ['leadIds'], - kbTopicId: 'kbTopicId', - }; - test('scriptsAdd', async () => { const mutation = ` mutation scriptsAdd(${commonParamDefs}) { @@ -54,7 +61,7 @@ describe('scriptMutations', () => { } `; - const script = await graphqlRequest(mutation, 'scriptsAdd', doc, context); + const script = await graphqlRequest(mutation, 'scriptsAdd', doc); expect(script.name).toBe(doc.name); expect(script.messengerId).toBe(doc.messengerId); @@ -76,15 +83,16 @@ describe('scriptMutations', () => { `; const newScript = await Scripts.create(doc); + const integration = (messenger = await integrationFactory({ kind: 'messenger', brandId: brand._id })); const updateDoc = { name: 'name_updated', - messengerId: 'messengerId_updated', - leadIds: ['leadIds_updated'], + messengerId: integration._id, + leadIds: [lead._id], kbTopicId: 'kbTopicId_updated', }; - const script = await graphqlRequest(mutation, 'scriptsEdit', { _id: newScript._id, ...updateDoc }, context); + const script = await graphqlRequest(mutation, 'scriptsEdit', { _id: newScript._id, ...updateDoc }); expect(script.name).toBe(updateDoc.name); expect(script.messengerId).toBe(updateDoc.messengerId); @@ -100,7 +108,7 @@ describe('scriptMutations', () => { `; const script = await Scripts.create(doc); - await graphqlRequest(mutation, 'scriptsRemove', { _id: script._id }, context); + await graphqlRequest(mutation, 'scriptsRemove', { _id: script._id }); expect(await Scripts.find({}).countDocuments()).toBe(0); }); diff --git a/src/__tests__/scriptQueries.test.ts b/src/__tests__/scriptQueries.test.ts new file mode 100644 index 000000000..d1f794c5b --- /dev/null +++ b/src/__tests__/scriptQueries.test.ts @@ -0,0 +1,48 @@ +import { graphqlRequest } from '../db/connection'; +import { Scripts } from '../db/models'; + +import { scriptFactory } from '../db/factories'; +import './setup.ts'; + +describe('responseTemplateQueries', () => { + beforeEach(async () => { + // Clearing test data + await scriptFactory({}); + await scriptFactory({}); + await scriptFactory({}); + }); + + afterEach(async () => { + // Clearing test data + await Scripts.deleteMany({}); + }); + + test('Scripts', async () => { + const qry = ` + query scripts($page: Int, $perPage: Int) { + scripts(page: $page, perPage: $perPage) { + _id + messenger { _id } + kbTopic { _id } + leads { _id } + } + } + `; + + const response = await graphqlRequest(qry, 'scripts', { page: 1, perPage: 2 }); + + expect(response.length).toBe(2); + }); + + test('Get total count of script', async () => { + const qry = ` + query scriptsTotalCount { + scriptsTotalCount + } + `; + + const totalCount = await graphqlRequest(qry, 'scriptsTotalCount'); + + expect(totalCount).toBe(3); + }); +}); diff --git a/src/__tests__/segmentDb.test.ts b/src/__tests__/segmentDb.test.ts index eb7347c24..480dc2818 100644 --- a/src/__tests__/segmentDb.test.ts +++ b/src/__tests__/segmentDb.test.ts @@ -1,26 +1,24 @@ import { segmentFactory } from '../db/factories'; import { Segments, Users } from '../db/models'; +import { ISegment } from '../db/models/definitions/segments'; import './setup.ts'; /* * Generate test data */ -const generateData = () => ({ +const generateData = (): ISegment => ({ contentType: 'customer', name: 'New users', description: 'New users', subOf: 'DFSAFDSAFDFFFD', color: '#fdfdfd', - connector: 'any', conditions: [ { - field: 'messengerData.sessionCount', - operator: 'e', - value: '10', - dateUnit: 'days', - type: 'string', - brandId: '1231', + type: 'property', + propertyName: 'messengerData.sessionCount', + propertyOperator: 'e', + propertyValue: '10', }, ], }); @@ -34,7 +32,6 @@ const checkValues = (segmentObj, doc) => { expect(segmentObj.description).toBe(doc.description); expect(segmentObj.subOf).toBe(doc.subOf); expect(segmentObj.color).toBe(doc.color); - expect(segmentObj.connector).toBe(doc.connector); expect(segmentObj.conditions.field).toEqual(doc.conditions.field); expect(segmentObj.conditions.operator).toEqual(doc.conditions.operator); @@ -58,6 +55,18 @@ describe('Segments mutations', () => { await Users.deleteMany({}); }); + test('Get segment', async () => { + try { + await Segments.getSegment('fakeId'); + } catch (e) { + expect(e.message).toBe('Segment not found'); + } + + const response = await Segments.getSegment(_segment._id); + + expect(response).toBeDefined(); + }); + test('Create segment', async () => { // valid const data = generateData(); diff --git a/src/__tests__/segmentMutations.test.ts b/src/__tests__/segmentMutations.test.ts index ea699875e..05bbff807 100644 --- a/src/__tests__/segmentMutations.test.ts +++ b/src/__tests__/segmentMutations.test.ts @@ -13,13 +13,13 @@ describe('Segments mutations', () => { let _user; let _segment; let context; + let parentSegment; const commonParamDefs = ` $name: String! $description: String $subOf: String $color: String - $connector: String $conditions: [SegmentCondition] `; @@ -28,14 +28,14 @@ describe('Segments mutations', () => { description: $description subOf: $subOf color: $color - connector: $connector conditions: $conditions `; beforeEach(async () => { // Creating test data + parentSegment = await segmentFactory(); _user = await userFactory({}); - _segment = await segmentFactory({}); + _segment = await segmentFactory({ subOf: parentSegment._id }); context = { user: _user }; }); @@ -47,7 +47,7 @@ describe('Segments mutations', () => { }); test('Add segment', async () => { - const { contentType, name, description, color, connector } = _segment; + const { contentType, name, description, color } = _segment; const subOf = _segment.subOf || faker.random.word(); const args = { contentType, @@ -55,15 +55,12 @@ describe('Segments mutations', () => { description, subOf, color, - connector, conditions: [ { - field: faker.random.word(), - operator: faker.random.word(), - value: faker.random.word(), - dateUnit: faker.random.word(), - type: faker.random.word(), - brandId: faker.random.word(), + propertyName: faker.random.word(), + propertyOperator: faker.random.word(), + propertyValue: faker.random.word(), + type: 'property', }, ], }; @@ -76,7 +73,6 @@ describe('Segments mutations', () => { description subOf color - connector conditions } } @@ -89,28 +85,25 @@ describe('Segments mutations', () => { expect(segment.description).toBe(args.description); expect(segment.subOf).toBe(args.subOf); expect(segment.color).toBe(args.color); - expect(segment.connector).toBe(args.connector); expect(toJSON(segment.conditions)).toEqual(toJSON(args.conditions)); }); test('segmentsEdit', async () => { - const { _id, name, description, color, connector } = _segment; - const subOf = _segment.subOf || faker.random.word(); + const { _id, name, description, color } = _segment; + const secondParent = await segmentFactory(); + const args = { _id, name, description, - subOf, + subOf: secondParent._id, color, - connector, conditions: [ { - type: faker.random.word(), - dateUnit: faker.random.word(), - value: faker.random.word(), - operator: faker.random.word(), - field: faker.random.word(), - brandId: faker.random.word(), + type: 'property', + propertyValue: faker.random.word(), + propertyOperator: faker.random.word(), + propertyName: faker.random.word(), }, ], }; @@ -123,7 +116,6 @@ describe('Segments mutations', () => { description subOf color - connector conditions } } @@ -136,7 +128,6 @@ describe('Segments mutations', () => { expect(segment.description).toBe(args.description); expect(segment.subOf).toBe(args.subOf); expect(segment.color).toBe(args.color); - expect(segment.connector).toBe(args.connector); expect(toJSON(segment.conditions)).toEqual(toJSON(args.conditions)); }); diff --git a/src/__tests__/segmentQueries.test.ts b/src/__tests__/segmentQueries.test.ts index f3919061b..89489b96b 100644 --- a/src/__tests__/segmentQueries.test.ts +++ b/src/__tests__/segmentQueries.test.ts @@ -1,7 +1,9 @@ import * as faker from 'faker'; +import * as sinon from 'sinon'; import { graphqlRequest } from '../db/connection'; import { segmentFactory } from '../db/factories'; import { Segments } from '../db/models'; +import * as elk from '../elasticsearch'; import './setup.ts'; @@ -17,32 +19,23 @@ describe('segmentQueries', () => { await segmentFactory({ contentType: 'company' }); const qry = ` - query segments($contentType: String!) { - segments(contentType: $contentType) { + query segments($contentTypes: [String]!) { + segments(contentTypes: $contentTypes) { _id - contentType - name - description - subOf - color - connector - conditions - - getSubSegments { _id } } } `; // customer segment ================== let response = await graphqlRequest(qry, 'segments', { - contentType: 'customer', + contentTypes: ['customer'], }); expect(response.length).toBe(1); // company segment ================== response = await graphqlRequest(qry, 'segments', { - contentType: 'company', + contentTypes: ['company'], }); expect(response.length).toBe(1); @@ -51,10 +44,21 @@ describe('segmentQueries', () => { test('Segment detail', async () => { const segment = await segmentFactory(); + await segmentFactory({ subOf: segment._id }); + await segmentFactory({ subOf: segment._id }); + const qry = ` query segmentDetail($_id: String) { segmentDetail(_id: $_id) { _id + contentType + name + description + subOf + color + conditions + + getSubSegments { _id } } } `; @@ -64,6 +68,7 @@ describe('segmentQueries', () => { }); expect(response._id).toBe(segment._id); + expect(response.getSubSegments.length).toBe(2); }); test('Get segment head', async () => { @@ -85,4 +90,67 @@ describe('segmentQueries', () => { expect(responses.length).toBe(3); }); + + test('events', async () => { + const mock = sinon.stub(elk, 'fetchElk').callsFake(() => { + return Promise.resolve({ + aggregations: { + names: { + buckets: [ + { + key: 'pageView', + hits: { + hits: { + hits: [ + { + _source: { + name: 'pageView', + attributes: [ + { + field: 'url', + value: '/test', + }, + ], + }, + }, + ], + }, + }, + }, + ], + }, + }, + }); + }); + + const qry = ` + query segmentsEvents($contentType: String!) { + segmentsEvents(contentType: $contentType) + } + `; + + const response = await graphqlRequest(qry, 'segmentsEvents', { contentType: 'customer' }); + + expect(response.length).toBe(1); + + mock.restore(); + }); + + test('segmentsPreviewCount', async () => { + const qry = ` + query segmentsPreviewCount($contentType: String!, $conditions: JSON) { + segmentsPreviewCount(contentType: $contentType, conditions: $conditions) + } + `; + + const mock = sinon.stub(elk, 'fetchElk').callsFake(() => { + return Promise.reject('error'); + }); + + await graphqlRequest(qry, 'segmentsPreviewCount', { contentType: 'customer', conditions: [] }); + + mock.restore(); + + await graphqlRequest(qry, 'segmentsPreviewCount', { contentType: 'customer', conditions: [] }); + }); }); diff --git a/src/__tests__/setup.ts b/src/__tests__/setup.ts index aa043b557..8ceb229ab 100644 --- a/src/__tests__/setup.ts +++ b/src/__tests__/setup.ts @@ -1,23 +1,31 @@ -import { connect } from '../db/connection'; +import { MongoMemoryServer } from 'mongodb-memory-server'; +import * as mongoose from 'mongoose'; +import { connectionOptions } from '../db/connection'; +import { initMemoryStorage } from '../inmemoryStorage'; +import { initBroker } from '../messageBroker'; -let db; +// May require additional time for downloading MongoDB binaries +jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; -beforeAll(async done => { - jest.setTimeout(30000); +let mongoServer; - db = await connect( - (process.env.TEST_MONGO_URL || '').replace( - 'test', - `erxes-test-${Math.random() - .toString() - .replace(/\./g, '')}`, - ), - 3, - ); +beforeAll(async () => { + mongoServer = new MongoMemoryServer(); - done(); + await initBroker(); + + initMemoryStorage(); + + const mongoUri = await mongoServer.getConnectionString(); + + await mongoose.connect(mongoUri, { ...connectionOptions, useUnifiedTopology: true }); }); -afterAll(() => { - return db.connection.dropDatabase(); +afterAll(async () => { + await mongoose.disconnect(); + await mongoServer.stop(); + + if (global.gc) { + global.gc(); + } }); diff --git a/src/__tests__/tagDb.test.ts b/src/__tests__/tagDb.test.ts index 4416a210a..a17692a76 100644 --- a/src/__tests__/tagDb.test.ts +++ b/src/__tests__/tagDb.test.ts @@ -21,6 +21,18 @@ describe('Test tags model', () => { await EngageMessages.deleteMany({}); }); + test('Get tag', async () => { + try { + await Tags.getTag('fakeId'); + } catch (e) { + expect(e.message).toBe('Tag not found'); + } + + const response = await Tags.getTag(_tag._id); + + expect(response).toBeDefined(); + }); + test('Validate unique tag', async () => { const empty = await Tags.validateUniqueness({}, '', ''); @@ -47,10 +59,18 @@ describe('Test tags model', () => { } }); - test('Attach tag type', async () => { + test('Attach customer tag', async () => { Tags.tagsTag('customer', [], []); }); + test('Attach integration tag', async () => { + Tags.tagsTag('integration', [], []); + }); + + test('Attach product tag', async () => { + Tags.tagsTag('product', [], []); + }); + test('Create tag check duplicated', async () => { expect.assertions(1); try { diff --git a/src/__tests__/tagMutations.test.ts b/src/__tests__/tagMutations.test.ts index 98f2c04e8..bf4d25157 100644 --- a/src/__tests__/tagMutations.test.ts +++ b/src/__tests__/tagMutations.test.ts @@ -1,6 +1,6 @@ import { graphqlRequest } from '../db/connection'; -import { engageMessageFactory, tagsFactory, userFactory } from '../db/factories'; -import { EngageMessages, Tags, Users } from '../db/models'; +import { conversationFactory, engageMessageFactory, tagsFactory, userFactory } from '../db/factories'; +import { Conversations, EngageMessages, Tags, Users } from '../db/models'; import './setup.ts'; @@ -96,7 +96,7 @@ describe('Test tags mutations', () => { }); test('Tag tags', async () => { - const args = { + let args = { type: 'engageMessage', targetIds: [_message._id], tagIds: [_tag._id], @@ -118,12 +118,23 @@ describe('Test tags mutations', () => { await graphqlRequest(mutation, 'tagsTag', args, context); - const engageMessage = await EngageMessages.findOne({ _id: _message._id }); + const engageMessage = await EngageMessages.getEngageMessage(_message._id); - if (!engageMessage) { - throw new Error('Engage message not found'); - } + expect(engageMessage.tagIds).toContain(args.tagIds[0]); - expect(engageMessage.tagIds).toContain(args.tagIds); + // conversation + const conversation = await conversationFactory(); + + const conversationTag = await tagsFactory({ type: 'conversation' }); + + args = { + type: 'conversation', + targetIds: [conversation._id], + tagIds: [conversationTag._id], + }; + + await graphqlRequest(mutation, 'tagsTag', args, context); + + expect((await Conversations.getConversation(conversation._id)).tagIds).toContain(args.tagIds[0]); }); }); diff --git a/src/__tests__/taskDb.test.ts b/src/__tests__/taskDb.test.ts index 59efd95fb..c11b61dfc 100644 --- a/src/__tests__/taskDb.test.ts +++ b/src/__tests__/taskDb.test.ts @@ -1,7 +1,6 @@ import { boardFactory, - companyFactory, - customerFactory, + conversationFactory, pipelineFactory, stageFactory, taskFactory, @@ -38,6 +37,18 @@ describe('Test tasks model', () => { await Tasks.deleteMany({}); }); + test('Get task', async () => { + try { + await Tasks.getTask('fakeId'); + } catch (e) { + expect(e.message).toBe('Task not found'); + } + + const response = await Tasks.getTask(task._id); + + expect(response).toBeDefined(); + }); + // Test task test('Create task', async () => { const createdTask = await Tasks.createTask({ @@ -47,93 +58,61 @@ describe('Test tasks model', () => { expect(createdTask).toBeDefined(); expect(createdTask.stageId).toEqual(stage._id); - expect(createdTask.createdAt).toEqual(task.createdAt); expect(createdTask.userId).toEqual(user._id); }); - test('Update task', async () => { - const taskStageId = 'fakeId'; - const updatedTask = await Tasks.updateTask(task._id, { - stageId: taskStageId, - }); - - expect(updatedTask).toBeDefined(); - expect(updatedTask.stageId).toEqual(taskStageId); - expect(updatedTask.closeDate).toEqual(task.closeDate); - }); - - test('Update task orders', async () => { - const dealToOrder = await taskFactory({}); + test('Create task Error(`Already converted a task`)', async () => { + const conversation = await conversationFactory(); - const [updatedTask, updatedDealToOrder] = await Tasks.updateOrder(stage._id, [ - { _id: task._id, order: 9 }, - { _id: dealToOrder._id, order: 3 }, - ]); - - expect(updatedTask.stageId).toBe(stage._id); - expect(updatedTask.order).toBe(3); - expect(updatedDealToOrder.order).toBe(9); - }); - - test('Remove task', async () => { - const isDeleted = await Tasks.removeTask(task.id); - - expect(isDeleted).toBeTruthy(); - }); + const args = { + stageId: task.stageId, + userId: user._id, + sourceConversationId: conversation._id, + }; - test('Remove task not found', async () => { - expect.assertions(1); + const createdTicket = await Tasks.createTask(args); - const fakeDealId = 'fakeDealId'; + expect(createdTicket).toBeDefined(); + // Already converted a task try { - await Tasks.removeTask(fakeDealId); + await Tasks.createTask(args); } catch (e) { - expect(e.message).toEqual('Task not found'); + expect(e.message).toBe('Already converted a task'); } }); - test('Task change customer', async () => { - const newCustomer = await customerFactory({}); - - const customer1 = await customerFactory({}); - const customer2 = await customerFactory({}); - const dealObj = await taskFactory({ - customerIds: [customer2._id, customer1._id], + test('Update task', async () => { + const taskStageId = 'fakeId'; + const updatedTask = await Tasks.updateTask(task._id, { + stageId: taskStageId, }); - await Tasks.changeCustomer(newCustomer._id, [customer2._id, customer1._id]); + expect(updatedTask).toBeDefined(); + expect(updatedTask.stageId).toEqual(taskStageId); + expect(updatedTask.closeDate).toEqual(task.closeDate); + }); - const result = await Tasks.findOne({ _id: dealObj._id }); + test('Watch task', async () => { + await Tasks.watchTask(task._id, true, user._id); - if (!result) { - throw new Error('Task not found'); - } + const watchedTask = await Tasks.getTask(task._id); - expect(result.customerIds).toContain(newCustomer._id); - expect(result.customerIds).not.toContain(customer1._id); - expect(result.customerIds).not.toContain(customer2._id); - }); + expect(watchedTask.watchedUserIds).toContain(user._id); - test('Task change company', async () => { - const newCompany = await companyFactory({}); + // testing unwatch + await Tasks.watchTask(task._id, false, user._id); - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - const dealObj = await taskFactory({ - companyIds: [company1._id, company2._id], - }); + const unwatchedTask = await Tasks.getTask(task._id); - await Tasks.changeCompany(newCompany._id, [company1._id, company2._id]); + expect(unwatchedTask.watchedUserIds).not.toContain(user._id); + }); - const result = await Tasks.findOne({ _id: dealObj._id }); + test('Test removeTasks()', async () => { + await Tasks.removeTasks([task._id]); - if (!result) { - throw new Error('Task not found'); - } + const removed = await Tasks.findOne({ _id: task._id }); - expect(result.companyIds).toContain(newCompany._id); - expect(result.companyIds).not.toContain(company1._id); - expect(result.companyIds).not.toContain(company2._id); + expect(removed).toBe(null); }); }); diff --git a/src/__tests__/taskMutations.test.ts b/src/__tests__/taskMutations.test.ts index eee1d13c3..67282f29f 100644 --- a/src/__tests__/taskMutations.test.ts +++ b/src/__tests__/taskMutations.test.ts @@ -1,8 +1,30 @@ import { graphqlRequest } from '../db/connection'; -import { boardFactory, pipelineFactory, stageFactory, taskFactory, userFactory } from '../db/factories'; -import { Boards, Pipelines, Stages, Tasks } from '../db/models'; +import { + boardFactory, + checklistFactory, + checklistItemFactory, + companyFactory, + conformityFactory, + customerFactory, + pipelineFactory, + pipelineLabelFactory, + stageFactory, + taskFactory, + userFactory, +} from '../db/factories'; +import { + Boards, + ChecklistItems, + Checklists, + Conformities, + PipelineLabels, + Pipelines, + Stages, + Tasks, +} from '../db/models'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; -import { BOARD_TYPES } from '../db/models/definitions/constants'; +import { BOARD_STATUSES, BOARD_TYPES, TIME_TRACK_TYPES } from '../db/models/definitions/constants'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; import { ITaskDocument } from '../db/models/definitions/tasks'; import './setup.ts'; @@ -12,16 +34,37 @@ describe('Test tasks mutations', () => { let pipeline: IPipelineDocument; let stage: IStageDocument; let task: ITaskDocument; - let context; + let label: IPipelineLabelDocument; + let label2: IPipelineLabelDocument; const commonTaskParamDefs = ` $name: String!, $stageId: String! + $assignedUserIds: [String] + $status: String `; const commonTaskParams = ` name: $name stageId: $stageId + assignedUserIds: $assignedUserIds + status: $status + `; + + const commonDragParamDefs = ` + $itemId: String!, + $aboveItemId: String, + $destinationStageId: String!, + $sourceStageId: String, + $proccessId: String + `; + + const commonDragParams = ` + itemId: $itemId, + aboveItemId: $aboveItemId, + destinationStageId: $destinationStageId, + sourceStageId: $sourceStageId, + proccessId: $proccessId `; beforeEach(async () => { @@ -29,8 +72,9 @@ describe('Test tasks mutations', () => { board = await boardFactory({ type: BOARD_TYPES.TASK }); pipeline = await pipelineFactory({ boardId: board._id }); stage = await stageFactory({ pipelineId: pipeline._id }); - task = await taskFactory({ stageId: stage._id }); - context = { user: await userFactory({}) }; + label = await pipelineLabelFactory({ pipelineId: pipeline._id }); + label2 = await pipelineLabelFactory({ pipelineId: pipeline._id, name: 'new label' }); + task = await taskFactory({ initialStageId: stage._id, stageId: stage._id, labelIds: [label._id, label2._id] }); }); afterEach(async () => { @@ -39,6 +83,7 @@ describe('Test tasks mutations', () => { await Pipelines.deleteMany({}); await Stages.deleteMany({}); await Tasks.deleteMany({}); + await PipelineLabels.deleteMany({}); }); test('Create task', async () => { @@ -57,13 +102,13 @@ describe('Test tasks mutations', () => { } `; - const createdTask = await graphqlRequest(mutation, 'tasksAdd', args, context); + const response = await graphqlRequest(mutation, 'tasksAdd', args); - expect(createdTask.stageId).toEqual(stage._id); + expect(response.stageId).toEqual(stage._id); }); test('Update task', async () => { - const args = { + const args: any = { _id: task._id, name: task.name, stageId: stage._id, @@ -75,58 +120,150 @@ describe('Test tasks mutations', () => { _id name stageId + labelIds + assignedUserIds } } `; - const updatedTask = await graphqlRequest(mutation, 'tasksEdit', args, context); + let updatedTask = await graphqlRequest(mutation, 'tasksEdit', args); expect(updatedTask.stageId).toEqual(stage._id); + + const user = await userFactory(); + args.assignedUserIds = [user.id]; + args.status = 'archived'; + + updatedTask = await graphqlRequest(mutation, 'tasksEdit', args); + + expect(updatedTask.assignedUserIds.length).toBe(1); }); - test('Change task', async () => { - const args = { + test('Move task between pipelines', async () => { + expect.assertions(3); + + const pipeline2 = await pipelineFactory(); + const stage2 = await stageFactory({ pipelineId: pipeline2._id }); + + await pipelineLabelFactory({ + pipelineId: pipeline2._id, + name: label.name, + colorCode: label.colorCode, + }); + + const args: any = { _id: task._id, - destinationStageId: task.stageId || '', + name: 'Edited task', + stageId: stage2._id, }; const mutation = ` - mutation tasksChange($_id: String!, $destinationStageId: String) { - tasksChange(_id: $_id, destinationStageId: $destinationStageId) { - _id, + mutation tasksEdit($_id: String!, ${commonTaskParamDefs}) { + tasksEdit(_id: $_id, ${commonTaskParams}) { + _id + name stageId + labelIds } } `; - const updatedTask = await graphqlRequest(mutation, 'tasksChange', args, context); + let updatedTask = await graphqlRequest(mutation, 'tasksEdit', args); - expect(updatedTask._id).toEqual(args._id); + expect(updatedTask.stageId).toBe(stage2._id); + + if (task.labelIds) { + const copiedLabels = await PipelineLabels.find({ pipelineId: pipeline2._id }); + + expect(copiedLabels.length).toBe(2); + } + + try { + // to improve boardUtils coverage + args.stageId = 'demo-stage'; + + updatedTask = await graphqlRequest(mutation, 'tasksEdit', args); + } catch (e) { + expect(e[0].message).toBe('Stage not found'); + } + }); + + test('Change task', async () => { + const args = { + proccessId: Math.random().toString(), + itemId: task._id, + aboveItemId: '', + destinationStageId: task.stageId, + sourceStageId: task.stageId, + }; + + const mutation = ` + mutation tasksChange(${commonDragParamDefs}) { + tasksChange(${commonDragParams}) { + _id + name + stageId + order + } + } + `; + const updatedTask = await graphqlRequest(mutation, 'tasksChange', args); + + expect(updatedTask._id).toEqual(args.itemId); }); - test('Task update orders', async () => { - const taskToStage = await taskFactory({}); + test('Change task if move to another stage', async () => { + const anotherStage = await stageFactory({ pipelineId: pipeline._id }); const args = { - orders: [{ _id: task._id, order: 9 }, { _id: taskToStage._id, order: 3 }], - stageId: stage._id, + proccessId: Math.random().toString(), + itemId: task._id, + aboveItemId: '', + destinationStageId: anotherStage._id, + sourceStageId: task.stageId, }; const mutation = ` - mutation tasksUpdateOrder($stageId: String!, $orders: [OrderItem]) { - tasksUpdateOrder(stageId: $stageId, orders: $orders) { + mutation tasksChange(${commonDragParamDefs}) { + tasksChange(${commonDragParams}) { _id + name stageId order } } `; - const [updatedTask, updatedTaskToOrder] = await graphqlRequest(mutation, 'tasksUpdateOrder', args, context); + const updatedTask = await graphqlRequest(mutation, 'tasksChange', args); - expect(updatedTask.order).toBe(3); - expect(updatedTaskToOrder.order).toBe(9); - expect(updatedTask.stageId).toBe(stage._id); + expect(updatedTask._id).toEqual(args.itemId); + }); + + test('Update task move to pipeline stage', async () => { + const mutation = ` + mutation tasksEdit($_id: String!, ${commonTaskParamDefs}) { + tasksEdit(_id: $_id, ${commonTaskParams}) { + _id + name + stageId + assignedUserIds + } + } + `; + + const anotherPipeline = await pipelineFactory({ boardId: board._id }); + const anotherStage = await stageFactory({ pipelineId: anotherPipeline._id }); + + const args = { + _id: task._id, + stageId: anotherStage._id, + name: task.name || '', + }; + + const updatedTask = await graphqlRequest(mutation, 'tasksEdit', args); + + expect(updatedTask._id).toEqual(args._id); + expect(updatedTask.stageId).toEqual(args.stageId); }); test('Remove task', async () => { @@ -138,7 +275,7 @@ describe('Test tasks mutations', () => { } `; - await graphqlRequest(mutation, 'tasksRemove', { _id: task._id }, context); + await graphqlRequest(mutation, 'tasksRemove', { _id: task._id }); expect(await Tasks.findOne({ _id: task._id })).toBe(null); }); @@ -153,12 +290,123 @@ describe('Test tasks mutations', () => { } `; - const watchAddTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: true }, context); + const watchAddTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: true }); expect(watchAddTask.isWatched).toBe(true); - const watchRemoveTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: false }, context); + const watchRemoveTask = await graphqlRequest(mutation, 'tasksWatch', { _id: task._id, isAdd: false }); expect(watchRemoveTask.isWatched).toBe(false); }); + + test('Test tasksCopy()', async () => { + const mutation = ` + mutation tasksCopy($_id: String!) { + tasksCopy(_id: $_id) { + _id + userId + name + stageId + } + } + `; + + const checklist = await checklistFactory({ + contentType: 'task', + contentTypeId: task._id, + title: 'task-checklist', + }); + + await checklistItemFactory({ + checklistId: checklist._id, + content: 'Improve task mutation test coverage', + isChecked: true, + }); + + const company = await companyFactory(); + const customer = await customerFactory(); + const user = await userFactory(); + + await conformityFactory({ + mainType: 'task', + mainTypeId: task._id, + relType: 'company', + relTypeId: company._id, + }); + + await conformityFactory({ + mainType: 'task', + mainTypeId: task._id, + relType: 'customer', + relTypeId: customer._id, + }); + + const result = await graphqlRequest(mutation, 'tasksCopy', { _id: task._id }, { user }); + + const clonedTaskCompanies = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedTaskCustomers = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedTaskChecklist = await Checklists.findOne({ contentTypeId: result._id }); + + if (clonedTaskChecklist) { + const clonedTaskChecklistItems = await ChecklistItems.find({ checklistId: clonedTaskChecklist._id }); + + expect(clonedTaskChecklist.contentTypeId).toBe(result._id); + expect(clonedTaskChecklistItems.length).toBe(1); + } + + expect(result.userId).toBe(user._id); + expect(result.name).toBe(`${task.name}-copied`); + expect(result.stageId).toBe(task.stageId); + + expect(clonedTaskCompanies.length).toBe(1); + expect(clonedTaskCustomers.length).toBe(1); + }); + + test('Task archive', async () => { + const mutation = ` + mutation tasksArchive($stageId: String!) { + tasksArchive(stageId: $stageId) + } + `; + + const taskStage = await stageFactory({ type: BOARD_TYPES.TASK }); + + await taskFactory({ stageId: taskStage._id }); + await taskFactory({ stageId: taskStage._id }); + await taskFactory({ stageId: taskStage._id }); + + await graphqlRequest(mutation, 'tasksArchive', { stageId: taskStage._id }); + + const tasks = await Tasks.find({ stageId: taskStage._id, status: BOARD_STATUSES.ARCHIVED }); + + expect(tasks.length).toBe(3); + }); + + test('Task update time track', async () => { + const mutation = ` + mutation taskUpdateTimeTracking($_id: String!, $status: String!, $timeSpent: Int!, $startDate: String) { + taskUpdateTimeTracking(_id: $_id, status: $status, timeSpent: $timeSpent, startDate: $startDate) + } + `; + + const taskStage = await stageFactory({ type: BOARD_TYPES.TASK }); + + await taskFactory({ stageId: taskStage._id }); + await taskFactory({ stageId: taskStage._id }); + await taskFactory({ stageId: taskStage._id }); + + await graphqlRequest(mutation, 'taskUpdateTimeTracking', { + _id: task._id, + status: TIME_TRACK_TYPES.STARTED, + timeSpent: 10, + startDate: new Date().toISOString(), + }); + + const updatedTask = await Tasks.findOne({ _id: task._id }); + + if (updatedTask && updatedTask.timeTrack) { + expect(updatedTask.timeTrack.status).toBe(TIME_TRACK_TYPES.STARTED); + expect(updatedTask.timeTrack.timeSpent).toBe(10); + } + }); }); diff --git a/src/__tests__/taskQueries.test.ts b/src/__tests__/taskQueries.test.ts index 4ca81585e..26af2e882 100644 --- a/src/__tests__/taskQueries.test.ts +++ b/src/__tests__/taskQueries.test.ts @@ -1,7 +1,17 @@ import { graphqlRequest } from '../db/connection'; -import { companyFactory, customerFactory, stageFactory, taskFactory, userFactory } from '../db/factories'; +import { + boardFactory, + companyFactory, + conformityFactory, + customerFactory, + pipelineFactory, + stageFactory, + taskFactory, + userFactory, +} from '../db/factories'; import { Tasks } from '../db/models'; +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('taskQueries', () => { @@ -9,8 +19,6 @@ describe('taskQueries', () => { _id name stageId - companyIds - customerIds assignedUserIds closeDate description @@ -23,36 +31,45 @@ describe('taskQueries', () => { assignedUsers { _id } + isWatched + hasNotified + labels { _id } + pipeline { _id } + boardId + stage { _id } + createdUser { _id } `; const qryTaskFilter = ` query tasks( - $stageId: String + $stageId: String $assignedUserIds: [String] $customerIds: [String] $companyIds: [String] - $nextDay: String - $nextWeek: String - $nextMonth: String - $noCloseDate: String - $overdue: String + $priority: [String] + $closeDateType: String ) { tasks( - stageId: $stageId + stageId: $stageId customerIds: $customerIds assignedUserIds: $assignedUserIds companyIds: $companyIds - nextDay: $nextDay - nextWeek: $nextWeek - nextMonth: $nextMonth - noCloseDate: $noCloseDate - overdue: $overdue + priority: $priority + closeDateType: $closeDateType ) { ${commonTaskTypes} } } `; + const qryDetail = ` + query taskDetail($_id: String!) { + taskDetail(_id: $_id) { + ${commonTaskTypes} + } + } + `; + afterEach(async () => { // Clearing test data await Tasks.deleteMany({}); @@ -71,7 +88,14 @@ describe('taskQueries', () => { test('Task filter by customers', async () => { const { _id } = await customerFactory(); - await taskFactory({ customerIds: [_id] }); + const task = await taskFactory({}); + + await conformityFactory({ + mainType: 'task', + mainTypeId: task._id, + relType: 'customer', + relTypeId: _id, + }); const response = await graphqlRequest(qryTaskFilter, 'tasks', { customerIds: [_id] }); @@ -81,15 +105,32 @@ describe('taskQueries', () => { test('Task filter by companies', async () => { const { _id } = await companyFactory(); - await taskFactory({ companyIds: [_id] }); + const task = await taskFactory({}); + + await conformityFactory({ + mainType: 'company', + mainTypeId: _id, + relType: 'task', + relTypeId: task._id, + }); const response = await graphqlRequest(qryTaskFilter, 'tasks', { companyIds: [_id] }); expect(response.length).toBe(1); }); + test('Task filter by priority', async () => { + await taskFactory({ priority: 'critical' }); + + const response = await graphqlRequest(qryTaskFilter, 'tasks', { priority: ['critical'] }); + + expect(response.length).toBe(1); + }); + test('Tasks', async () => { - const stage = await stageFactory(); + const board = await boardFactory({ type: BOARD_TYPES.TASK }); + const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TASK }); + const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TASK }); const args = { stageId: stage._id }; @@ -97,7 +138,7 @@ describe('taskQueries', () => { await taskFactory(args); await taskFactory(args); - const qry = ` + const qryList = ` query tasks($stageId: String!) { tasks(stageId: $stageId) { ${commonTaskTypes} @@ -105,26 +146,126 @@ describe('taskQueries', () => { } `; - const response = await graphqlRequest(qry, 'tasks', args); + const response = await graphqlRequest(qryList, 'tasks', args); expect(response.length).toBe(3); }); test('Task detail', async () => { const task = await taskFactory(); + const response = await graphqlRequest(qryDetail, 'taskDetail', { _id: task._id }); + + expect(response._id).toBe(task._id); + }); + + test('Task detail with watchedUserIds', async () => { + const user = await userFactory(); + const watchedTask = await taskFactory({ watchedUserIds: [user._id] }); - const args = { _id: task._id }; + const response = await graphqlRequest( + qryDetail, + 'taskDetail', + { + _id: watchedTask._id, + }, + { user }, + ); + + expect(response._id).toBe(watchedTask._id); + expect(response.isWatched).toBe(true); + }); + + test('Get archived task', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.TASK }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await taskFactory({ ...args, name: 'james' }); + await taskFactory({ ...args, name: 'jone' }); + await taskFactory({ ...args, name: 'gerrad' }); const qry = ` - query taskDetail($_id: String!) { - taskDetail(_id: $_id) { - ${commonTaskTypes} + query archivedTasks( + $pipelineId: String!, + $search: String, + $page: Int, + $perPage: Int + ) { + archivedTasks( + pipelineId: $pipelineId + search: $search + page: $page + perPage: $perPage + ) { + _id } } `; - const response = await graphqlRequest(qry, 'taskDetail', args); + let response = await graphqlRequest(qry, 'archivedTasks', { + pipelineId: pipeline._id, + }); - expect(response._id).toBe(task._id); + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'archivedTasks', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'archivedTasks', { + pipelineId: 'fakeId', + }); + + expect(response.length).toBe(0); + }); + + test('Get archived task count ', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.TASK }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await taskFactory({ ...args, name: 'james' }); + await taskFactory({ ...args, name: 'jone' }); + await taskFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedTasksCount( + $pipelineId: String!, + $search: String, + ) { + archivedTasksCount( + pipelineId: $pipelineId + search: $search + ) + } + `; + + let response = await graphqlRequest(qry, 'archivedTasksCount', { + pipelineId: pipeline._id, + }); + + expect(response).toBe(3); + + response = await graphqlRequest(qry, 'archivedTasksCount', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response).toBe(1); + + response = await graphqlRequest(qry, 'archivedTasksCount', { + pipelineId: 'fakeId', + }); + + expect(response).toBe(0); }); }); diff --git a/src/__tests__/ticketDb.test.ts b/src/__tests__/ticketDb.test.ts index b21cd2b74..98844fe77 100644 --- a/src/__tests__/ticketDb.test.ts +++ b/src/__tests__/ticketDb.test.ts @@ -1,7 +1,6 @@ import { boardFactory, - companyFactory, - customerFactory, + conversationFactory, pipelineFactory, stageFactory, ticketFactory, @@ -38,7 +37,18 @@ describe('Test Tickets model', () => { await Tickets.deleteMany({}); }); - // Test ticket + test('Get ticket', async () => { + try { + await Tickets.getTicket('fakeId'); + } catch (e) { + expect(e.message).toBe('Ticket not found'); + } + + const response = await Tickets.getTicket(ticket._id); + + expect(response).toBeDefined(); + }); + test('Create ticket', async () => { const createdTicket = await Tickets.createTicket({ stageId: ticket.stageId, @@ -47,93 +57,61 @@ describe('Test Tickets model', () => { expect(createdTicket).toBeDefined(); expect(createdTicket.stageId).toEqual(stage._id); - expect(createdTicket.createdAt).toEqual(ticket.createdAt); expect(createdTicket.userId).toEqual(user._id); }); - test('Update ticket', async () => { - const ticketStageId = 'fakeId'; - const updatedTicket = await Tickets.updateTicket(ticket._id, { - stageId: ticketStageId, - }); - - expect(updatedTicket).toBeDefined(); - expect(updatedTicket.stageId).toEqual(ticketStageId); - expect(updatedTicket.closeDate).toEqual(ticket.closeDate); - }); - - test('Update ticket orders', async () => { - const dealToOrder = await ticketFactory({}); - - const [updatedTicket, updatedDealToOrder] = await Tickets.updateOrder(stage._id, [ - { _id: ticket._id, order: 9 }, - { _id: dealToOrder._id, order: 3 }, - ]); - - expect(updatedTicket.stageId).toBe(stage._id); - expect(updatedTicket.order).toBe(3); - expect(updatedDealToOrder.order).toBe(9); - }); - - test('Remove ticket', async () => { - const isDeleted = await Tickets.removeTicket(ticket.id); + test('Create ticket Error(`Already converted a ticket`)', async () => { + const conversation = await conversationFactory(); - expect(isDeleted).toBeTruthy(); - }); + const args = { + stageId: ticket.stageId, + sourceConversationId: conversation._id, + userId: user._id, + }; - test('Remove ticket not found', async () => { - expect.assertions(1); + const createdTicket = await Tickets.createTicket(args); - const fakeDealId = 'fakeDealId'; + expect(createdTicket).toBeDefined(); + // Already converted a ticket try { - await Tickets.removeTicket(fakeDealId); + await Tickets.createTicket(args); } catch (e) { - expect(e.message).toEqual('Ticket not found'); + expect(e.message).toBe('Already converted a ticket'); } }); - test('Ticket change customer', async () => { - const newCustomer = await customerFactory({}); - - const customer1 = await customerFactory({}); - const customer2 = await customerFactory({}); - const dealObj = await ticketFactory({ - customerIds: [customer2._id, customer1._id], + test('Update ticket', async () => { + const ticketStageId = 'fakeId'; + const updatedTicket = await Tickets.updateTicket(ticket._id, { + stageId: ticketStageId, }); - await Tickets.changeCustomer(newCustomer._id, [customer2._id, customer1._id]); + expect(updatedTicket).toBeDefined(); + expect(updatedTicket.stageId).toEqual(ticketStageId); + expect(updatedTicket.closeDate).toEqual(ticket.closeDate); + }); - const result = await Tickets.findOne({ _id: dealObj._id }); + test('Watch ticket', async () => { + await Tickets.watchTicket(ticket._id, true, user._id); - if (!result) { - throw new Error('Ticket not found'); - } + const watchedTicket = await Tickets.getTicket(ticket._id); - expect(result.customerIds).toContain(newCustomer._id); - expect(result.customerIds).not.toContain(customer1._id); - expect(result.customerIds).not.toContain(customer2._id); - }); + expect(watchedTicket.watchedUserIds).toContain(user._id); - test('Ticket change company', async () => { - const newCompany = await companyFactory({}); + // testing unwatch + await Tickets.watchTicket(ticket._id, false, user._id); - const company1 = await companyFactory({}); - const company2 = await companyFactory({}); - const dealObj = await ticketFactory({ - companyIds: [company1._id, company2._id], - }); + const unwatchedTicket = await Tickets.getTicket(ticket._id); - await Tickets.changeCompany(newCompany._id, [company1._id, company2._id]); + expect(unwatchedTicket.watchedUserIds).not.toContain(user._id); + }); - const result = await Tickets.findOne({ _id: dealObj._id }); + test('Test removeTickets()', async () => { + await Tickets.removeTickets([ticket._id]); - if (!result) { - throw new Error('Ticket not found'); - } + const removed = await Tickets.findOne({ _id: ticket._id }); - expect(result.companyIds).toContain(newCompany._id); - expect(result.companyIds).not.toContain(company1._id); - expect(result.companyIds).not.toContain(company2._id); + expect(removed).toBe(null); }); }); diff --git a/src/__tests__/ticketMutations.test.ts b/src/__tests__/ticketMutations.test.ts index de05f5b1d..62cc5f95c 100644 --- a/src/__tests__/ticketMutations.test.ts +++ b/src/__tests__/ticketMutations.test.ts @@ -1,8 +1,30 @@ import { graphqlRequest } from '../db/connection'; -import { boardFactory, pipelineFactory, stageFactory, ticketFactory, userFactory } from '../db/factories'; -import { Boards, Pipelines, Stages, Tickets } from '../db/models'; +import { + boardFactory, + checklistFactory, + checklistItemFactory, + companyFactory, + conformityFactory, + customerFactory, + pipelineFactory, + pipelineLabelFactory, + stageFactory, + ticketFactory, + userFactory, +} from '../db/factories'; +import { + Boards, + ChecklistItems, + Checklists, + Conformities, + PipelineLabels, + Pipelines, + Stages, + Tickets, +} from '../db/models'; import { IBoardDocument, IPipelineDocument, IStageDocument } from '../db/models/definitions/boards'; -import { BOARD_TYPES } from '../db/models/definitions/constants'; +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; +import { IPipelineLabelDocument } from '../db/models/definitions/pipelineLabels'; import { ITicketDocument } from '../db/models/definitions/tickets'; import './setup.ts'; @@ -12,16 +34,37 @@ describe('Test tickets mutations', () => { let pipeline: IPipelineDocument; let stage: IStageDocument; let ticket: ITicketDocument; + let label: IPipelineLabelDocument; let context; const commonTicketParamDefs = ` $name: String!, $stageId: String! + $assignedUserIds: [String] + $status: String `; const commonTicketParams = ` name: $name stageId: $stageId + assignedUserIds: $assignedUserIds + status: $status + `; + + const commonDragParamDefs = ` + $itemId: String!, + $aboveItemId: String, + $destinationStageId: String!, + $sourceStageId: String, + $proccessId: String + `; + + const commonDragParams = ` + itemId: $itemId, + aboveItemId: $aboveItemId, + destinationStageId: $destinationStageId, + sourceStageId: $sourceStageId, + proccessId: $proccessId `; beforeEach(async () => { @@ -29,7 +72,8 @@ describe('Test tickets mutations', () => { board = await boardFactory({ type: BOARD_TYPES.TICKET }); pipeline = await pipelineFactory({ boardId: board._id }); stage = await stageFactory({ pipelineId: pipeline._id }); - ticket = await ticketFactory({ stageId: stage._id }); + label = await pipelineLabelFactory({ pipelineId: pipeline._id }); + ticket = await ticketFactory({ stageId: stage._id, labelIds: [label._id] }); context = { user: await userFactory({}) }; }); @@ -39,6 +83,7 @@ describe('Test tickets mutations', () => { await Pipelines.deleteMany({}); await Stages.deleteMany({}); await Tickets.deleteMany({}); + await PipelineLabels.deleteMany({}); }); test('Create ticket', async () => { @@ -63,7 +108,7 @@ describe('Test tickets mutations', () => { }); test('Update ticket', async () => { - const args = { + const args: any = { _id: ticket._id, name: ticket.name, stageId: stage._id, @@ -79,54 +124,96 @@ describe('Test tickets mutations', () => { } `; - const updatedTicket = await graphqlRequest(mutation, 'ticketsEdit', args, context); + let updatedTicket = await graphqlRequest(mutation, 'ticketsEdit', args, context); + + expect(updatedTicket.stageId).toEqual(stage._id); + + const user = await userFactory(); + args.assignedUserIds = [user.id]; + args.status = 'archived'; + + updatedTicket = await graphqlRequest(mutation, 'ticketsEdit', args); expect(updatedTicket.stageId).toEqual(stage._id); }); test('Change ticket', async () => { const args = { - _id: ticket._id, - destinationStageId: ticket.stageId || '', + proccessId: Math.random().toString(), + itemId: ticket._id, + aboveItemId: '', + destinationStageId: ticket.stageId, + sourceStageId: ticket.stageId, }; const mutation = ` - mutation ticketsChange($_id: String!, $destinationStageId: String) { - ticketsChange(_id: $_id, destinationStageId: $destinationStageId) { - _id, + mutation ticketsChange(${commonDragParamDefs}) { + ticketsChange(${commonDragParams}) { + _id + name stageId + order } } `; const updatedTicket = await graphqlRequest(mutation, 'ticketsChange', args, context); - expect(updatedTicket._id).toEqual(args._id); + expect(updatedTicket._id).toEqual(args.itemId); }); - test('Ticket update orders', async () => { - const ticketToStage = await ticketFactory({}); + test('Change ticket if move to another stage', async () => { + const anotherStage = await stageFactory({ pipelineId: pipeline._id }); const args = { - orders: [{ _id: ticket._id, order: 9 }, { _id: ticketToStage._id, order: 3 }], - stageId: stage._id, + proccessId: Math.random().toString(), + itemId: ticket._id, + aboveItemId: '', + destinationStageId: anotherStage._id, + sourceStageId: ticket.stageId, }; const mutation = ` - mutation ticketsUpdateOrder($stageId: String!, $orders: [OrderItem]) { - ticketsUpdateOrder(stageId: $stageId, orders: $orders) { + mutation ticketsChange(${commonDragParamDefs}) { + ticketsChange(${commonDragParams}) { _id + name stageId order } } `; - const [updatedTicket, updatedTicketToOrder] = await graphqlRequest(mutation, 'ticketsUpdateOrder', args, context); + const updatedTicket = await graphqlRequest(mutation, 'ticketsChange', args); - expect(updatedTicket.order).toBe(3); - expect(updatedTicketToOrder.order).toBe(9); - expect(updatedTicket.stageId).toBe(stage._id); + expect(updatedTicket._id).toEqual(args.itemId); + }); + + test('Update ticket move to pipeline stage', async () => { + const mutation = ` + mutation ticketsEdit($_id: String!, ${commonTicketParamDefs}) { + ticketsEdit(_id: $_id, ${commonTicketParams}) { + _id + name + stageId + assignedUserIds + } + } + `; + + const anotherPipeline = await pipelineFactory({ boardId: board._id }); + const anotherStage = await stageFactory({ pipelineId: anotherPipeline._id }); + + const args = { + _id: ticket._id, + stageId: anotherStage._id, + name: ticket.name || '', + }; + + const updatedTicket = await graphqlRequest(mutation, 'ticketsEdit', args); + + expect(updatedTicket._id).toEqual(args._id); + expect(updatedTicket.stageId).toEqual(args.stageId); }); test('Remove ticket', async () => { @@ -166,4 +253,85 @@ describe('Test tickets mutations', () => { expect(watchRemoveTicket.isWatched).toBe(false); }); + + test('Test ticketsCopy()', async () => { + const mutation = ` + mutation ticketsCopy($_id: String!) { + ticketsCopy(_id: $_id) { + _id + userId + name + stageId + } + } + `; + + const checklist = await checklistFactory({ + contentType: 'ticket', + contentTypeId: ticket._id, + title: 'ticket-checklist', + }); + + await checklistItemFactory({ + checklistId: checklist._id, + content: 'Improve ticket mutation test coverage', + isChecked: true, + }); + + const company = await companyFactory(); + const customer = await customerFactory(); + + await conformityFactory({ + mainType: 'ticket', + mainTypeId: ticket._id, + relType: 'company', + relTypeId: company._id, + }); + + await conformityFactory({ + mainType: 'ticket', + mainTypeId: ticket._id, + relType: 'customer', + relTypeId: customer._id, + }); + + const result = await graphqlRequest(mutation, 'ticketsCopy', { _id: ticket._id }, context); + + const clonedTicketCompanies = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedTicketCustomers = await Conformities.find({ mainTypeId: result._id, relTypeId: company._id }); + const clonedTicketChecklist = await Checklists.findOne({ contentTypeId: result._id }); + + if (clonedTicketChecklist) { + const clonedTicketChecklistItems = await ChecklistItems.find({ checklistId: clonedTicketChecklist._id }); + + expect(clonedTicketChecklist.contentTypeId).toBe(result._id); + expect(clonedTicketChecklistItems.length).toBe(1); + } + + expect(result.name).toBe(`${ticket.name}-copied`); + expect(result.stageId).toBe(ticket.stageId); + + expect(clonedTicketCompanies.length).toBe(1); + expect(clonedTicketCustomers.length).toBe(1); + }); + + test('Ticket archive', async () => { + const mutation = ` + mutation ticketsArchive($stageId: String!) { + ticketsArchive(stageId: $stageId) + } + `; + + const ticketStage = await stageFactory({ type: BOARD_TYPES.TICKET }); + + await ticketFactory({ stageId: ticketStage._id }); + await ticketFactory({ stageId: ticketStage._id }); + await ticketFactory({ stageId: ticketStage._id }); + + await graphqlRequest(mutation, 'ticketsArchive', { stageId: ticketStage._id }); + + const tickets = await Tickets.find({ stageId: ticketStage._id, status: BOARD_STATUSES.ARCHIVED }); + + expect(tickets.length).toBe(3); + }); }); diff --git a/src/__tests__/ticketQueries.test.ts b/src/__tests__/ticketQueries.test.ts index cac58a910..ebb760647 100644 --- a/src/__tests__/ticketQueries.test.ts +++ b/src/__tests__/ticketQueries.test.ts @@ -1,7 +1,17 @@ import { graphqlRequest } from '../db/connection'; -import { companyFactory, customerFactory, stageFactory, ticketFactory, userFactory } from '../db/factories'; +import { + boardFactory, + companyFactory, + conformityFactory, + customerFactory, + pipelineFactory, + stageFactory, + ticketFactory, + userFactory, +} from '../db/factories'; import { Tickets } from '../db/models'; +import { BOARD_STATUSES, BOARD_TYPES } from '../db/models/definitions/constants'; import './setup.ts'; describe('ticketQueries', () => { @@ -9,50 +19,53 @@ describe('ticketQueries', () => { _id name stageId - companyIds - customerIds assignedUserIds closeDate description - companies { - _id - } - customers { - _id - } - assignedUsers { - _id - } + companies { _id } + customers { _id } + assignedUsers { _id } + boardId + pipeline { _id } + stage { _id } + isWatched + hasNotified + labels { _id } + createdUser { _id } `; const qryTicketFilter = ` query tickets( - $stageId: String + $stageId: String $assignedUserIds: [String] $customerIds: [String] $companyIds: [String] - $nextDay: String - $nextWeek: String - $nextMonth: String - $noCloseDate: String - $overdue: String + $priority: [String] + $source: [String] + $closeDateType: String ) { tickets( - stageId: $stageId + stageId: $stageId customerIds: $customerIds assignedUserIds: $assignedUserIds companyIds: $companyIds - nextDay: $nextDay - nextWeek: $nextWeek - nextMonth: $nextMonth - noCloseDate: $noCloseDate - overdue: $overdue + priority: $priority + source: $source + closeDateType: $closeDateType ) { ${commonTicketTypes} } } `; + const qryDetail = ` + query ticketDetail($_id: String!) { + ticketDetail(_id: $_id) { + ${commonTicketTypes} + } + } + `; + afterEach(async () => { // Clearing test data await Tickets.deleteMany({}); @@ -71,7 +84,14 @@ describe('ticketQueries', () => { test('Ticket filter by customers', async () => { const { _id } = await customerFactory(); - await ticketFactory({ customerIds: [_id] }); + const ticket = await ticketFactory({}); + + await conformityFactory({ + mainType: 'ticket', + mainTypeId: ticket._id, + relType: 'customer', + relTypeId: _id, + }); const response = await graphqlRequest(qryTicketFilter, 'tickets', { customerIds: [_id] }); @@ -81,15 +101,40 @@ describe('ticketQueries', () => { test('Ticket filter by companies', async () => { const { _id } = await companyFactory(); - await ticketFactory({ companyIds: [_id] }); + const ticket = await ticketFactory({}); + + await conformityFactory({ + mainType: 'company', + mainTypeId: _id, + relType: 'ticket', + relTypeId: ticket._id, + }); const response = await graphqlRequest(qryTicketFilter, 'tickets', { companyIds: [_id] }); expect(response.length).toBe(1); }); + test('Ticket filter by priority', async () => { + await ticketFactory({ priority: 'critical' }); + + const response = await graphqlRequest(qryTicketFilter, 'tickets', { priority: ['critical'] }); + + expect(response.length).toBe(1); + }); + + test('Ticket filter by source', async () => { + await ticketFactory({ source: 'messenger' }); + + const response = await graphqlRequest(qryTicketFilter, 'tickets', { source: ['messenger'] }); + + expect(response.length).toBe(1); + }); + test('Tickets', async () => { - const stage = await stageFactory(); + const board = await boardFactory({ type: BOARD_TYPES.TICKET }); + const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TICKET }); + const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TICKET }); const args = { stageId: stage._id }; @@ -97,7 +142,7 @@ describe('ticketQueries', () => { await ticketFactory(args); await ticketFactory(args); - const qry = ` + const qryList = ` query tickets($stageId: String!) { tickets(stageId: $stageId) { ${commonTicketTypes} @@ -105,7 +150,7 @@ describe('ticketQueries', () => { } `; - const response = await graphqlRequest(qry, 'tickets', args); + const response = await graphqlRequest(qryList, 'tickets', args); expect(response.length).toBe(3); }); @@ -115,16 +160,119 @@ describe('ticketQueries', () => { const args = { _id: ticket._id }; + const response = await graphqlRequest(qryDetail, 'ticketDetail', args); + + expect(response._id).toBe(ticket._id); + }); + + test('Ticket detail with watchedUserIds', async () => { + const user = await userFactory(); + const watchedTask = await ticketFactory({ watchedUserIds: [user._id] }); + + const response = await graphqlRequest( + qryDetail, + 'ticketDetail', + { + _id: watchedTask._id, + }, + { user }, + ); + + expect(response._id).toBe(watchedTask._id); + expect(response.isWatched).toBe(true); + }); + + test('Get archived tickets', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.TICKET }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await ticketFactory({ ...args, name: 'james' }); + await ticketFactory({ ...args, name: 'jone' }); + await ticketFactory({ ...args, name: 'gerrad' }); + const qry = ` - query ticketDetail($_id: String!) { - ticketDetail(_id: $_id) { - ${commonTicketTypes} + query archivedTickets( + $pipelineId: String!, + $search: String, + $page: Int, + $perPage: Int + ) { + archivedTickets( + pipelineId: $pipelineId + search: $search + page: $page + perPage: $perPage + ) { + _id } } `; - const response = await graphqlRequest(qry, 'ticketDetail', args); + let response = await graphqlRequest(qry, 'archivedTickets', { + pipelineId: pipeline._id, + }); - expect(response._id).toBe(ticket._id); + expect(response.length).toBe(3); + + response = await graphqlRequest(qry, 'archivedTickets', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response.length).toBe(1); + + response = await graphqlRequest(qry, 'archivedTickets', { + pipelineId: 'fakeId', + }); + + expect(response.length).toBe(0); + }); + + test('Get archived tickets count', async () => { + const pipeline = await pipelineFactory({ type: BOARD_TYPES.TICKET }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + const args = { + stageId: stage._id, + status: BOARD_STATUSES.ARCHIVED, + }; + + await ticketFactory({ ...args, name: 'james' }); + await ticketFactory({ ...args, name: 'jone' }); + await ticketFactory({ ...args, name: 'gerrad' }); + + const qry = ` + query archivedTicketsCount( + $pipelineId: String!, + $search: String + ) { + archivedTicketsCount( + pipelineId: $pipelineId + search: $search + ) + } + `; + + let response = await graphqlRequest(qry, 'archivedTicketsCount', { + pipelineId: pipeline._id, + }); + + expect(response).toBe(3); + + response = await graphqlRequest(qry, 'archivedTicketsCount', { + pipelineId: pipeline._id, + search: 'james', + }); + + expect(response).toBe(1); + + response = await graphqlRequest(qry, 'archivedTicketsCount', { + pipelineId: 'fakeId', + }); + + expect(response).toBe(0); }); }); diff --git a/src/__tests__/userDb.test.ts b/src/__tests__/userDb.test.ts index ea0072547..f77d80e42 100644 --- a/src/__tests__/userDb.test.ts +++ b/src/__tests__/userDb.test.ts @@ -12,6 +12,7 @@ beforeAll(() => { describe('User db utils', () => { let _user; + const strongPassword = 'Password123'; beforeEach(async () => { // Creating test data @@ -23,14 +24,24 @@ describe('User db utils', () => { await Users.deleteMany({}); }); - test('Create user', async () => { - const testPassword = 'test'; + test('Get user', async () => { + try { + await Users.getUser('fakeId'); + } catch (e) { + expect(e.message).toBe('User not found'); + } + + const response = await Users.getUser(_user._id); + + expect(response).toBeDefined(); + }); + test('Create user', async () => { const userObj = await Users.createUser({ ..._user._doc, details: { ..._user.details.toJSON() }, - links: { ..._user.links.toJSON() }, - password: testPassword, + links: { ..._user.links }, + password: strongPassword, email: 'qwerty@qwerty.com', }); @@ -42,11 +53,10 @@ describe('User db utils', () => { expect(userObj._id).toBeDefined(); expect(userObj.username).toBe(_user.username); expect(userObj.email).toBe('qwerty@qwerty.com'); - expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); + expect(bcrypt.compare(strongPassword, userObj.password)).toBeTruthy(); expect(userObj.details.position).toBe(_user.details.position); expect(userObj.details.fullName).toBe(_user.details.fullName); expect(userObj.details.avatar).toBe(_user.details.avatar); - expect(userObj.links.toJSON()).toEqual(_user.links.toJSON()); }); test('Create user with empty string password', async () => { @@ -88,7 +98,7 @@ describe('User db utils', () => { await Users.createUser({ ..._user._doc, details: { ..._user.details.toJSON() }, - password: '123', + password: strongPassword, email: user.email, }); } catch (e) { @@ -118,7 +128,7 @@ describe('User db utils', () => { test('createUserWithConfirmation', async () => { const group = await usersGroupFactory(); - const token = await Users.createUserWithConfirmation({ email: '123@gmail.com', groupId: group._id }); + const token = await Users.invite({ email: '123@gmail.com', password: strongPassword, groupId: group._id }); const userObj = await Users.findOne({ registrationToken: token }).lean(); @@ -136,7 +146,7 @@ describe('User db utils', () => { test('resendInvitation', async () => { const email = '123@gmail.com'; const group = await usersGroupFactory(); - const token = await Users.createUserWithConfirmation({ email, groupId: group._id }); + const token = await Users.invite({ email, password: strongPassword, groupId: group._id }); const newToken = await Users.resendInvitation({ email }); const user = await Users.findOne({ email }).lean(); @@ -150,30 +160,28 @@ describe('User db utils', () => { expect(user.registrationTokenExpires).toBeDefined(); }); - test('resendInvitation: invalid', async () => { - expect.assertions(1); - - const user = await userFactory({}); + test('invite: invalid group', async () => { + try { + await Users.invite({ email: 'email', password: strongPassword, groupId: 'fakeId' }); + } catch (e) { + expect(e.message).toBe('Invalid group'); + } + }); + test('resendInvitation: invalid request', async () => { try { - await Users.resendInvitation({ email: user.email || 'invalid' }); + await Users.resendInvitation({ email: _user.email || 'invalid' }); } catch (e) { expect(e.message).toBe('Invalid request'); } }); - test('updateOnBoardSeen', async () => { - const user = await userFactory({}); - - await Users.updateOnBoardSeen({ _id: user._id }); - - const userObj = await Users.findOne({ _id: user._id }); - - if (!userObj) { - throw new Error('User not found'); + test('resendInvitation: user not found', async () => { + try { + await Users.resendInvitation({ email: 'invalid' }); + } catch (e) { + expect(e.message).toBe('User not found'); } - - expect(userObj.hasSeenOnBoard).toBeTruthy(); }); test('confirmInvitation', async () => { @@ -194,8 +202,8 @@ describe('User db utils', () => { await Users.confirmInvitation({ token, - password: '123', - passwordConfirmation: '123', + password: strongPassword, + passwordConfirmation: strongPassword, fullName: 'fullname', username: 'username', }); @@ -252,7 +260,7 @@ describe('User db utils', () => { expect(e.message).toBe('Password does not match'); } - await Users.update( + await Users.updateOne( { _id: userObj._id }, { $set: { @@ -276,13 +284,11 @@ describe('User db utils', () => { test('Update user', async () => { const updateDoc = await userFactory({}); - const testPassword = 'updatedPass'; - // try with password ============ await Users.updateUser(_user._id, { email: '123@gmail.com', username: updateDoc.username, - password: testPassword, + password: strongPassword, details: updateDoc.details, links: updateDoc.links, }); @@ -299,11 +305,10 @@ describe('User db utils', () => { expect(userObj.username).toBe(updateDoc.username); expect(userObj.email).toBe('123@gmail.com'); - expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); + expect(bcrypt.compare(strongPassword, userObj.password)).toBeTruthy(); expect(userObj.details.position).toBe(updateDoc.details.position); expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); - expect(userObj.links.toJSON()).toEqual(updateDoc.links.toJSON()); // try without password ============ await Users.updateUser(_user._id, { @@ -318,7 +323,7 @@ describe('User db utils', () => { } // password must stay untouched - expect(bcrypt.compare(testPassword, userObj.password)).toBeTruthy(); + expect(bcrypt.compare(strongPassword, userObj.password)).toBeTruthy(); }); test('Set user to active', async () => { @@ -329,6 +334,11 @@ describe('User db utils', () => { expect(e.message).toBe('User not found'); } + const inActiveUser = await userFactory({ isActive: false }); + const activeUser = await Users.setUserActiveOrInactive(inActiveUser._id); + + expect(activeUser.isActive).toBeTruthy(); + // Can not remove owner try { const user = await userFactory({}); @@ -382,7 +392,6 @@ describe('User db utils', () => { expect(userObj.details.position).toBe(updateDoc.details.position); expect(userObj.details.fullName).toBe(updateDoc.details.fullName); expect(userObj.details.avatar).toBe(updateDoc.details.avatar); - expect(userObj.links.toJSON()).toEqual(updateDoc.links.toJSON()); }); test('Config email signature', async () => { @@ -437,12 +446,12 @@ describe('User db utils', () => { // valid const user = await Users.resetPassword({ token: 'token', - newPassword: 'password', + newPassword: strongPassword, }); expect(user.resetPasswordToken).toBe(null); expect(user.resetPasswordExpires).toBe(null); - expect(bcrypt.compare('password', user.password)).toBeTruthy(); + expect(bcrypt.compare(strongPassword, user.password)).toBeTruthy(); }); test('Change password: incorrect current password', async () => { @@ -454,7 +463,7 @@ describe('User db utils', () => { await Users.changePassword({ _id: user._id, currentPassword: 'admin', - newPassword: '123321', + newPassword: strongPassword, }); } catch (e) { expect(e.message).toBe('Incorrect current password'); @@ -467,14 +476,14 @@ describe('User db utils', () => { const updatedUser = await Users.changePassword({ _id: user._id, currentPassword: 'pass', - newPassword: 'Lombo@123', + newPassword: strongPassword, }); if (!updatedUser || !updatedUser.password) { throw new Error('Updated user not found'); } - expect(await Users.comparePassword('Lombo@123', updatedUser.password)).toBeTruthy(); + expect(await Users.comparePassword(strongPassword, updatedUser.password)).toBeTruthy(); }); test('Forgot password', async () => { @@ -500,7 +509,7 @@ describe('User db utils', () => { }); test('Login', async () => { - expect.assertions(4); + expect.assertions(8); // invalid email ============== try { @@ -517,13 +526,40 @@ describe('User db utils', () => { } // valid - const { token, refreshToken } = await Users.login({ + let response = await Users.login({ email: _user.email.toUpperCase(), password: 'pass', }); - expect(token).toBeDefined(); - expect(refreshToken).toBeDefined(); + expect(response.token).toBeDefined(); + expect(response.refreshToken).toBeDefined(); + + // device token + const tokenUser = await userFactory({ deviceTokens: ['mobile'] }); + + if (!tokenUser) { + throw new Error('User not found'); + } + + // when device token + response = await Users.login({ + email: (tokenUser.email || '').toUpperCase(), + password: 'pass', + deviceToken: 'web', + }); + + expect(response.token).toBeDefined(); + expect(response.refreshToken).toBeDefined(); + + // when adding same device token + response = await Users.login({ + email: (tokenUser.email || '').toUpperCase(), + password: 'pass', + deviceToken: 'web', + }); + + expect(response.token).toBeDefined(); + expect(response.refreshToken).toBeDefined(); }); test('Refresh tokens', async () => { @@ -542,4 +578,35 @@ describe('User db utils', () => { expect(token).toBeDefined(); expect(refreshToken).toBeDefined(); }); + + test('Reset member password', async () => { + expect.assertions(2); + + try { + await Users.resetMemberPassword({ _id: _user._id, newPassword: '' }); + } catch (e) { + expect(e.message).toBe('Password is required.'); + } + + // valid + const updatedUser = await Users.resetMemberPassword({ + _id: _user._id, + newPassword: strongPassword, + }); + + expect(await Users.comparePassword(strongPassword, updatedUser.password)).toBeTruthy(); + }); + + test('Check password', async () => { + expect.assertions(1); + const weakPassword = '123456'; + + try { + await Users.checkPassword(weakPassword); + } catch (e) { + expect(e.message).toBe( + 'Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters', + ); + } + }); }); diff --git a/src/__tests__/userMutations.test.ts b/src/__tests__/userMutations.test.ts index 8f0b21978..a7777f94b 100644 --- a/src/__tests__/userMutations.test.ts +++ b/src/__tests__/userMutations.test.ts @@ -1,7 +1,8 @@ import * as bcrypt from 'bcryptjs'; import * as faker from 'faker'; import * as moment from 'moment'; -import utils from '../data/utils'; +import * as sinon from 'sinon'; +import utils, * as allUtils from '../data/utils'; import { graphqlRequest } from '../db/connection'; import { brandFactory, channelFactory, userFactory, usersGroupFactory } from '../db/factories'; import { Brands, Channels, Users } from '../db/models'; @@ -43,12 +44,13 @@ describe('User mutations', () => { let _brand; let context; + const strongPassword = 'Password123'; const commonParamDefs = ` $username: String! $email: String! $details: UserDetails - $links: UserLinks + $links: JSON $channelIds: [String] `; @@ -60,6 +62,12 @@ describe('User mutations', () => { channelIds: $channelIds `; + const usersCreateOwnerMutation = ` + mutation usersCreateOwner($email: String! $password: String! $firstName: String! $subscribeEmail: Boolean!) { + usersCreateOwner(email: $email password: $password firstName: $firstName subscribeEmail: $subscribeEmail) + } + `; + beforeEach(async () => { // Creating test data _user = await userFactory({}); @@ -77,6 +85,74 @@ describe('User mutations', () => { await Channels.deleteMany({}); }); + test('Create owner (Access denied)', async () => { + process.env.HTTPS = 'false'; + + try { + await graphqlRequest( + usersCreateOwnerMutation, + 'usersCreateOwner', + { + email: 'owner1@gmail.com', + password: 'pass', + firstName: 'Firstname', + subscribeEmail: false, + }, + { user: {} }, + ); + } catch (e) { + expect(e[0].message).toBe('Access denied'); + } + }); + + test('Create owner', async () => { + process.env.HTTPS = 'false'; + + await Users.deleteMany({}); + + const response = await graphqlRequest( + usersCreateOwnerMutation, + 'usersCreateOwner', + { + email: 'owner2@gmail.com', + password: 'Pass@123', + firstName: 'Firstname', + subscribeEmail: false, + }, + { user: {} }, + ); + + expect(response).toBe('success'); + }); + + test('Create owner (Subscribe email)', async () => { + process.env.HTTPS = 'false'; + process.env.NODE_ENV = 'production'; + + await Users.deleteMany({}); + + const mock = sinon.stub(allUtils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const response = await graphqlRequest( + usersCreateOwnerMutation, + 'usersCreateOwner', + { + email: 'owner3@gmail.com', + password: 'Pass@123', + firstName: 'Firstname', + subscribeEmail: true, + }, + { user: {} }, + ); + + mock.restore(); + process.env.NODE_ENV = 'test'; + + expect(response).toBe('success'); + }); + test('Login', async () => { process.env.HTTPS = 'false'; @@ -147,7 +223,7 @@ describe('User mutations', () => { const params = { token, - newPassword: 'newPassword', + newPassword: strongPassword, }; await graphqlRequest(mutation, 'resetPassword', params); @@ -176,7 +252,7 @@ describe('User mutations', () => { const group = await usersGroupFactory(); const params = { - entries: [{ email: 'test@example.com', groupId: group._id }], + entries: [{ email: 'test@example.com', password: strongPassword, groupId: group._id }], }; await graphqlRequest(mutation, 'usersInvite', params, { user: _admin }); @@ -202,7 +278,6 @@ describe('User mutations', () => { content: invitationUrl, domain: MAIN_APP_DOMAIN, }, - isCustom: true, }, }); @@ -237,33 +312,12 @@ describe('User mutations', () => { content: invitationUrl, domain: MAIN_APP_DOMAIN, }, - isCustom: true, }, }); spyEmail.mockRestore(); }); - test('usersSeenOnBoard', async () => { - const mutation = ` - mutation usersSeenOnBoard { - usersSeenOnBoard { - _id - } - } - `; - - await graphqlRequest(mutation, 'usersSeenOnBoard', {}, context); - const userObj = await Users.findOne({ _id: _user._id }); - - if (!userObj) { - throw new Error('User not found'); - } - - // send email call - expect(userObj.hasSeenOnBoard).toBeTruthy(); - }); - test('usersConfirmInvitation', async () => { await userFactory({ email: 'test@example.com', @@ -283,18 +337,14 @@ describe('User mutations', () => { const params = { token: '123', - password: '123', - passwordConfirmation: '123', + password: strongPassword, + passwordConfirmation: strongPassword, }; await graphqlRequest(mutation, 'usersConfirmInvitation', params); const userObj = await Users.findOne({ email: 'test@example.com' }); - if (!userObj) { - throw new Error('User not found'); - } - // send email call expect(userObj).toBeDefined(); }); @@ -319,25 +369,14 @@ describe('User mutations', () => { position description } - links { - linkedIn - twitter - facebook - github - youtube - website - } + links } } `; - const user = await graphqlRequest(mutation, 'usersEdit', { _id: _user._id, ...doc }, { user: _admin }); - - const channel = await Channels.findOne({ _id: _channel._id }); + let user = await graphqlRequest(mutation, 'usersEdit', { _id: _user._id, ...doc }, { user: _admin }); - if (!channel) { - throw new Error('Channel not found'); - } + let channel = await Channels.getChannel(_channel._id); expect(channel.memberIds).toContain(user._id); expect(user.username).toBe(doc.username); @@ -353,6 +392,17 @@ describe('User mutations', () => { expect(user.links.github).toBe(doc.links.github); expect(user.links.youtube).toBe(doc.links.youtube); expect(user.links.website).toBe(doc.links.website); + + // if channelIds is empty + user = await graphqlRequest( + mutation, + 'usersEdit', + { _id: _user._id, ...doc, channelIds: undefined }, + { user: _admin }, + ); + channel = await Channels.getChannel(_channel._id); + + expect(channel.memberIds).not.toContain(user._id); }); test('Edit user profile', async () => { @@ -361,7 +411,7 @@ describe('User mutations', () => { $username: String! $email: String! $details: UserDetails - $links: UserLinks + $links: JSON $password: String! ) { usersEditProfile( @@ -380,14 +430,7 @@ describe('User mutations', () => { position description } - links { - linkedIn - twitter - facebook - github - youtube - website - } + links } } `; @@ -407,6 +450,22 @@ describe('User mutations', () => { expect(user.links.github).toBe(args.links.github); expect(user.links.youtube).toBe(args.links.youtube); expect(user.links.website).toBe(args.links.website); + + // if password is empty + args.password = ''; + try { + await graphqlRequest(mutation, 'usersEditProfile', args, context); + } catch (e) { + expect(e[0].message).toBe('Invalid password. Try again'); + } + + // if password is not match + args.password = 'updated'; + try { + await graphqlRequest(mutation, 'usersEditProfile', args, context); + } catch (e) { + expect(e[0].message).toBe('Invalid password. Try again'); + } }); test('Change user password', async () => { @@ -431,16 +490,12 @@ describe('User mutations', () => { 'usersChangePassword', { currentPassword: 'pass', - newPassword: 'pass1', + newPassword: strongPassword, }, context, ); - const user = await Users.findOne({ _id: _user._id }); - - if (!user) { - throw new Error('User not found'); - } + const user = await Users.getUser(_user._id); expect(user.password).not.toBe(previousPassword); }); @@ -457,15 +512,16 @@ describe('User mutations', () => { await Users.updateOne({ _id: _user._id }, { $unset: { registrationToken: 1, isOwner: false } }); - await graphqlRequest(mutation, 'usersSetActiveStatus', { _id: _user._id }, { user: _admin }); + const response = await graphqlRequest(mutation, 'usersSetActiveStatus', { _id: _user._id }, { user: _admin }); - const deactivedUser = await Users.findOne({ _id: _user._id }); + expect(response.isActive).toBe(false); - if (!deactivedUser) { - throw new Error('User not found'); + // if deactivate yourself + try { + await graphqlRequest(mutation, 'usersSetActiveStatus', { _id: _admin._id }, { user: _admin }); + } catch (e) { + expect(e[0].message).toBe('You can not delete yourself'); } - - expect(deactivedUser.isActive).toBe(false); }); test('Config user email signature', async () => { @@ -502,4 +558,55 @@ describe('User mutations', () => { expect(user.getNotificationByEmail).toBeDefined(); }); + + test('Logout', async () => { + const mutation = ` + mutation logout { + logout + } + `; + + const res = { + clearCookie: () => { + return 'clearCookie'; + }, + }; + + const response = await graphqlRequest(mutation, 'logout', {}, { res }); + + expect(response).toBe('loggedout'); + }); + + test('Reset member password', async () => { + const previousPassword = _user.password; + + const mutation = ` + mutation usersResetMemberPassword( + $_id: String! + $newPassword: String! + ) { + usersResetMemberPassword( + _id: $_id + newPassword: $newPassword + ) { + _id + } + } + `; + + const user = await graphqlRequest( + mutation, + 'usersResetMemberPassword', + { _id: _user.id, newPassword: strongPassword }, + context, + ); + // if not newPassword + try { + await graphqlRequest(mutation, 'usersResetMemberPassword', { _id: _user.id, newPassword: '' }, context); + } catch (e) { + expect(e[0].message).toBe('Password is required.'); + } + + expect(user.password).not.toBe(previousPassword); + }); }); diff --git a/src/__tests__/userQueries.test.ts b/src/__tests__/userQueries.test.ts index 17c45ddb7..9f7d35948 100644 --- a/src/__tests__/userQueries.test.ts +++ b/src/__tests__/userQueries.test.ts @@ -1,6 +1,6 @@ import { graphqlRequest } from '../db/connection'; -import { conversationFactory, userFactory } from '../db/factories'; -import { Conversations, Users } from '../db/models'; +import { brandFactory, conversationFactory, onboardHistoryFactory, userFactory } from '../db/factories'; +import { Conversations, OnboardingHistories, Users } from '../db/models'; import './setup.ts'; @@ -9,9 +9,80 @@ describe('userQueries', () => { // Clearing test data await Users.deleteMany({}); await Conversations.deleteMany({}); + await OnboardingHistories.deleteMany({}); }); - test('Users', async () => { + test('Test users()', async () => { + // Creating test data + const brand = await brandFactory(); + const user1 = await userFactory({ email: 'example@email.com', brandIds: [brand._id] }); + const user2 = await userFactory(); + const user3 = await userFactory({ isActive: false }); + await userFactory({ registrationToken: 'token' }); + + const paramDefs = ` + $page: Int, + $perPage: Int, + $searchValue: String, + $requireUsername: Boolean, + $isActive: Boolean, + $ids: [String], + $status: String, + $brandIds: [String] + `; + + const paramValues = ` + page: $page, + perPage: $perPage, + searchValue: $searchValue, + requireUsername: $requireUsername, + isActive: $isActive, + ids: $ids, + status: $status, + brandIds: $brandIds + `; + + const qry = ` + query users(${paramDefs}) { + users(${paramValues}) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'users'); + + // 1 in graphRequest + above active 3 + expect(response.length).toBe(4); + + response = await graphqlRequest(qry, 'users', { searchValue: 'example@email.com' }); + + expect(response[0]._id).toBe(user1._id); + + response = await graphqlRequest(qry, 'users', { requireUsername: true }); + + // 3 in graphRequest + above active 3 + expect(response.length).toBe(6); + + response = await graphqlRequest(qry, 'users', { isActive: false }); + + expect(response[0]._id).toBe(user3._id); + + response = await graphqlRequest(qry, 'users', { ids: [user1.id, user2._id] }); + + expect(response.length).toBe(2); + + response = await graphqlRequest(qry, 'users', { status: 'status' }); + + // 6 in graphRequest + above 2 + expect(response.length).toBe(8); + + response = await graphqlRequest(qry, 'users', { brandIds: [brand._id] }); + + expect(response.length).toBe(1); + }); + + test('All users', async () => { // Creating test data await userFactory({}); await userFactory({}); @@ -19,54 +90,57 @@ describe('userQueries', () => { await userFactory({ isActive: false }); const qry = ` - query users($page: Int $perPage: Int) { - users(page: $page perPage: $perPage) { + query allUsers($isActive: Boolean) { + allUsers(isActive: $isActive) { _id - username - email - details { - avatar - fullName - position - location - description - } - links { - linkedIn - twitter - facebook - github - youtube - website - } - emailSignatures - getNotificationByEmail } } `; - const response = await graphqlRequest(qry, 'users', { - page: 1, - perPage: 20, - }); + let response = await graphqlRequest(qry, 'allUsers'); + // 1 in graphRequest + above 4 + expect(response.length).toBe(5); + + response = await graphqlRequest(qry, 'allUsers', { isActive: true }); + + // 2 in graphRequest + above active 3 expect(response.length).toBe(5); }); test('User detail', async () => { - const user = await userFactory({}); - const qry = ` query userDetail($_id: String) { userDetail(_id: $_id) { _id + status + brands { _id } + permissionActions + configs + configsConstants + onboardingHistory { _id } } } `; - const response = await graphqlRequest(qry, 'userDetail', { _id: user._id }); + // checking not verified + let user = await userFactory({ isOwner: true, registrationToken: 'registrationToken' }); + // to improve test coverage + await onboardHistoryFactory({ userId: user._id, isCompleted: false }); + + let response = await graphqlRequest(qry, 'userDetail', { _id: user._id }, { user }); + + expect(response._id).toBe(user._id); + expect(response.status).toBe('Not verified'); + + // checking brand ids + const brand = await brandFactory(); + user = await userFactory({ isOwner: false, brandIds: [brand._id] }); + response = await graphqlRequest(qry, 'userDetail', { _id: user._id }, { user }); expect(response._id).toBe(user._id); + expect(response.status).toBe('Verified'); + expect(response.brands[0]._id).toBe(brand._id); }); test('Get total count of users', async () => { @@ -76,31 +150,41 @@ describe('userQueries', () => { await userFactory({ isActive: false }); const qry = ` - query usersTotalCount { - usersTotalCount + query usersTotalCount($isActive: Boolean) { + usersTotalCount(isActive: $isActive) } `; const response = await graphqlRequest(qry, 'usersTotalCount'); - // 1 in graphRequest + above 3 - expect(response).toBe(4); + // 1 in graphRequest + above active 2 + expect(response).toBe(3); }); test('Current user', async () => { const user = await userFactory({}); + // to improve test coverage + await onboardHistoryFactory({ userId: user._id, isCompleted: true }); + const qry = ` query currentUser { currentUser { _id + onboardingHistory { + _id + } } } `; - const response = await graphqlRequest(qry, 'currentUser', {}, { user }); + let response = await graphqlRequest(qry, 'currentUser', {}, { user }); expect(response._id).toBe(user._id); + + response = await graphqlRequest(qry, 'currentUser', {}, { user: { isActive: false } }); + + expect(response).toBe(null); }); test('User conversations', async () => { diff --git a/src/__tests__/webhookDb.test.ts b/src/__tests__/webhookDb.test.ts new file mode 100644 index 000000000..9113d65fc --- /dev/null +++ b/src/__tests__/webhookDb.test.ts @@ -0,0 +1,95 @@ +import { webhookFactory } from '../db/factories'; +import { Webhooks } from '../db/models'; + +import './setup.ts'; + +describe('Test webhooks model', () => { + let _webhook; + let _webhook2; + + beforeEach(async () => { + // Creating test data + _webhook = await webhookFactory({}); + + _webhook2 = await webhookFactory({}); + }); + + afterEach(async () => { + // Clearing test data + await Webhooks.deleteMany({}); + }); + + test('Get webhook', async () => { + try { + await Webhooks.getWebHook('fakeId'); + } catch (e) { + expect(e.message).toBe('Webhook not found'); + } + + const response = await Webhooks.getWebHook(_webhook._id); + + expect(response).toBeDefined(); + }); + + test('Get webhooks', async () => { + const response = await Webhooks.getWebHooks(); + + expect(response.length).toEqual(2); + }); + + test('Create webhook check valid url', async () => { + expect.assertions(1); + try { + await Webhooks.createWebhook({ token: _webhook.token, url: 'http://alskdjalksjd.com', actions: [] }); + } catch (e) { + expect(e.message).toEqual('Url is not valid. Enter valid url with ssl cerfiticate'); + } + }); + + test('Update webhook check valid url', async () => { + expect.assertions(1); + try { + await Webhooks.updateWebhook(_webhook2._id, { + url: 'http://alskdjalksjd.com', + token: _webhook.token, + actions: _webhook.actions, + }); + } catch (e) { + expect(e.message).toEqual('Url is not valid. Enter valid url with ssl cerfiticate'); + } + }); + + test('Create Webhook', async () => { + const webhookObj = await Webhooks.createWebhook({ + url: 'https://test.com', + actions: _webhook.actions, + }); + + expect(webhookObj.url).toEqual('https://test.com'); + expect(webhookObj.actions.length).toBeGreaterThan(0); + expect(webhookObj.token).toBeDefined(); + }); + + test('Update Webhook', async () => { + const webhookObj = await Webhooks.updateWebhook(_webhook._id, { + url: 'https://test.com', + actions: _webhook.actions, + }); + + expect(webhookObj).toBeDefined(); + expect(webhookObj.url).toEqual('https://test.com'); + expect(webhookObj.actions.length).toBeGreaterThan(0); + }); + + test('Remove Webhook', async () => { + const isDeleted = await Webhooks.removeWebhooks(_webhook.id); + + expect(isDeleted).toBeTruthy(); + }); + + test('Update webhook status', async () => { + const response = await Webhooks.updateStatus(_webhook2._id, 'available'); + + expect(response.status).toEqual('available'); + }); +}); diff --git a/src/__tests__/webhookMutations.test.ts b/src/__tests__/webhookMutations.test.ts new file mode 100644 index 000000000..f54355f36 --- /dev/null +++ b/src/__tests__/webhookMutations.test.ts @@ -0,0 +1,137 @@ +import * as sinon from 'sinon'; +import * as utils from '../data/utils'; +import { graphqlRequest } from '../db/connection'; +import { userFactory, webhookFactory } from '../db/factories'; +import { Users, Webhooks } from '../db/models'; +import { WEBHOOK_ACTIONS } from '../db/models/definitions/constants'; + +import './setup.ts'; + +describe('Test webhooks mutations', () => { + let _webhook; + let _user; + let doc; + let context; + + const commonParamDefs = ` + $actions: [WebhookActionInput] + $url: String! +`; + + const commonParams = ` + actions: $actions, + url: $url, +`; + + beforeEach(async () => { + // Creating test data + _webhook = await webhookFactory({}); + _user = await userFactory({}); + + context = { user: _user }; + + doc = { + url: `${_webhook.url}1`, + actions: WEBHOOK_ACTIONS, + }; + }); + + afterEach(async () => { + // Clearing test data + await Webhooks.deleteMany({}); + await Users.deleteMany({}); + }); + + test('Add webhook', async () => { + const mutation = ` + mutation webhooksAdd(${commonParamDefs}) { + webhooksAdd(${commonParams}) { + url + actions{ + label + type + action + } + token + } + } + `; + + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const webhook = await graphqlRequest(mutation, 'webhooksAdd', doc, context); + + expect(webhook.url).toBe(doc.url); + expect(webhook.actions.length).toBeGreaterThan(1); + expect(webhook.token).toBeDefined(); + + mock.restore(); + }); + + test('Add webhook with request error', async () => { + const mutation = ` + mutation webhooksAdd(${commonParamDefs}) { + webhooksAdd(${commonParams}) { + url + actions{ + label + type + action + } + status + token + } + } + `; + + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.reject('error'); + }); + + const webhook = await graphqlRequest(mutation, 'webhooksAdd', doc, context); + + mock.restore(); + + expect(webhook.url).toBe(doc.url); + expect(webhook.actions.length).toBeGreaterThan(1); + expect(webhook.token).toBeDefined(); + expect(webhook.status).toBe('unavailable'); + }); + + test('Edit webhook', async () => { + const mutation = ` + mutation webhooksEdit($_id: String! ${commonParamDefs}){ + webhooksEdit(_id: $_id ${commonParams}) { + _id + url + actions{ + label + type + action + } + token + } + } + `; + + const webhook = await graphqlRequest(mutation, 'webhooksEdit', { _id: _webhook._id, ...doc }, context); + + expect(webhook._id).toBe(_webhook._id); + expect(webhook.url).toBe(doc.url); + expect(webhook.actions.length).toBeGreaterThan(1); + }); + + test('Remove webhook', async () => { + const mutation = ` + mutation webhooksRemove($_id: String!) { + webhooksRemove(_id: $_id) + } + `; + + await graphqlRequest(mutation, 'webhooksRemove', { _id: _webhook._id }, context); + + expect(await Webhooks.find({ _id: _webhook._id })).toEqual([]); + }); +}); diff --git a/src/__tests__/webhookQueries.test.ts b/src/__tests__/webhookQueries.test.ts new file mode 100644 index 000000000..5f0a3004f --- /dev/null +++ b/src/__tests__/webhookQueries.test.ts @@ -0,0 +1,67 @@ +import { graphqlRequest } from '../db/connection'; +import { webhookFactory } from '../db/factories'; +import { Webhooks } from '../db/models'; + +import './setup.ts'; + +describe('webhookQueries', () => { + afterEach(async () => { + // Clearing test data + await Webhooks.deleteMany({}); + }); + + test('get webhooks', async () => { + // Creating test data + await webhookFactory({}); + + const qry = ` + query webhooks { + webhooks { + _id + url + token + actions { + label + type + action + } + } + } + `; + + const response = await graphqlRequest(qry, 'webhooks'); + + expect(response.length).toBe(1); + }); + + test('Webhook detail', async () => { + const webhook = await webhookFactory({}); + + const qry = ` + query webhookDetail($_id: String!) { + webhookDetail(_id: $_id) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'webhookDetail', { _id: webhook._id }); + + expect(response._id).toBe(webhook._id); + }); + + test('Webhook total count', async () => { + // Creating test data + await webhookFactory({}); + + const qry = ` + query webhooks { + webhooksTotalCount + } + `; + + const response = await graphqlRequest(qry, 'webhooksTotalCount'); + + expect(response).toBe(1); + }); +}); diff --git a/src/__tests__/widgetMutations.test.ts b/src/__tests__/widgetMutations.test.ts new file mode 100644 index 000000000..14a809e53 --- /dev/null +++ b/src/__tests__/widgetMutations.test.ts @@ -0,0 +1,885 @@ +import * as faker from 'faker'; +import * as Random from 'meteor-random'; +import * as sinon from 'sinon'; +import widgetMutations, { getMessengerData } from '../data/resolvers/mutations/widgets'; +import * as utils from '../data/utils'; +import { graphqlRequest } from '../db/connection'; +import { + brandFactory, + conversationFactory, + conversationMessageFactory, + customerFactory, + engageMessageFactory, + fieldFactory, + formFactory, + integrationFactory, + knowledgeBaseArticleFactory, + messengerAppFactory, + userFactory, +} from '../db/factories'; +import { + Brands, + ConversationMessages, + Conversations, + Customers, + FormSubmissions, + Integrations, + KnowledgeBaseArticles, + MessengerApps, +} from '../db/models'; +import { IBrandDocument } from '../db/models/definitions/brands'; +import { CONVERSATION_OPERATOR_STATUS, CONVERSATION_STATUSES, MESSAGE_TYPES } from '../db/models/definitions/constants'; +import { ICustomerDocument } from '../db/models/definitions/customers'; +import { IIntegrationDocument } from '../db/models/definitions/integrations'; +import './setup.ts'; + +describe('messenger connect', () => { + let _brand: IBrandDocument; + let _integration: IIntegrationDocument; + let _customer: ICustomerDocument; + + beforeEach(async () => { + // Creating test data + _brand = await brandFactory(); + _integration = await integrationFactory({ + brandId: _brand._id, + kind: 'messenger', + }); + _customer = await customerFactory({ + integrationId: _integration._id, + primaryEmail: 'test@gmail.com', + emails: ['test@gmail.com'], + primaryPhone: '96221050', + deviceTokens: ['111'], + }); + }); + + afterEach(async () => { + // Clearing test data + await Brands.deleteMany({}); + await Integrations.deleteMany({}); + await Customers.deleteMany({}); + await MessengerApps.deleteMany({}); + }); + + test('brand not found', async () => { + try { + await widgetMutations.widgetsMessengerConnect({}, { brandCode: 'invalidCode' }); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + }); + + test('brand not found', async () => { + const brand = await brandFactory({}); + + try { + await widgetMutations.widgetsMessengerConnect({}, { brandCode: brand.code || '' }); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + }); + + test('returns proper integrationId', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + await messengerAppFactory({ + kind: 'knowledgebase', + name: 'kb', + credentials: { + integrationId: _integration._id, + topicId: 'topicId', + }, + }); + + await messengerAppFactory({ + kind: 'lead', + name: 'lead', + credentials: { + integrationId: _integration._id, + formCode: 'formCode', + }, + }); + + const { integrationId, brand, messengerData } = await widgetMutations.widgetsMessengerConnect( + {}, + { brandCode: _brand.code || '', email: faker.internet.email() }, + ); + + expect(integrationId).toBe(_integration._id); + expect(brand.code).toBe(_brand.code); + expect(messengerData.formCode).toBe('formCode'); + expect(messengerData.knowledgeBaseTopicId).toBe('topicId'); + mock.restore(); + }); + + test('creates new customer', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + const email = 'newCustomer@gmail.com'; + const now = new Date(); + + const { customerId } = await widgetMutations.widgetsMessengerConnect( + {}, + { brandCode: _brand.code || '', email, companyData: { name: 'company' }, deviceToken: '111' }, + ); + + expect(customerId).toBeDefined(); + + const customer = await Customers.findById(customerId); + + if (!customer) { + throw new Error('customer not found'); + } + + expect(customer._id).toBeDefined(); + expect(customer.primaryEmail).toBe(email); + expect(customer.emails).toContain(email); + expect(customer.integrationId).toBe(_integration._id); + expect((customer.deviceTokens || []).length).toBe(1); + expect(customer.deviceTokens).toContain('111'); + expect(customer.createdAt >= now).toBeTruthy(); + expect(customer.sessionCount).toBe(1); + mock.restore(); + }); + + test('updates existing customer', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const now = new Date(); + + const { customerId } = await widgetMutations.widgetsMessengerConnect( + {}, + { + brandCode: _brand.code || '', + email: _customer.primaryEmail, + isUser: true, + deviceToken: '222', + }, + ); + + expect(customerId).toBeDefined(); + + const customer = await Customers.findById(customerId); + + if (!customer) { + throw new Error('customer not found'); + } + + expect(customer).toBeDefined(); + expect(customer.integrationId).toBe(_integration._id); + expect(customer.createdAt < now).toBeTruthy(); + + // must be updated + expect((customer.deviceTokens || []).length).toBe(2); + expect(customer.deviceTokens).toContain('111'); + expect(customer.deviceTokens).toContain('222'); + mock.restore(); + }); +}); + +describe('insertMessage()', () => { + let _integration: IIntegrationDocument; + let _customer: ICustomerDocument; + let _integrationBot: IIntegrationDocument; + + beforeEach(async () => { + // Creating test data + _integration = await integrationFactory({ + brandId: Random.id(), + kind: 'messenger', + }); + _integrationBot = await integrationFactory({ + brandId: Random.id(), + kind: 'messenger', + messengerData: { + botEndpointUrl: 'botEndpointUrl', + }, + }); + _customer = await customerFactory({ integrationId: _integration._id }); + }); + + afterEach(async () => { + // Clearing test data + await Integrations.deleteMany({}); + await Customers.deleteMany({}); + }); + + test('without conversationId', async () => { + const now = new Date(); + + const message = await widgetMutations.widgetsInsertMessage( + {}, + { + contentType: MESSAGE_TYPES.TEXT, + integrationId: _integration._id, + customerId: _customer._id, + message: faker.lorem.sentence(), + }, + ); + + // check message ========== + expect(message).toBeDefined(); + expect(message.createdAt >= now).toBeTruthy(); + + // check conversation ========= + const conversation = await Conversations.findById(message.conversationId); + + if (!conversation) { + throw new Error('conversation is not found'); + } + + expect(conversation.status).toBe(CONVERSATION_STATUSES.OPEN); + expect((conversation.readUserIds || []).length).toBe(0); + + // check customer ========= + const customer = await Customers.findOne({ _id: _customer._id }); + + if (!customer) { + throw new Error('customer is not found'); + } + + expect(customer.isOnline).toBeTruthy(); + }); + + test('with conversationId', async () => { + const conversation = await conversationFactory({}); + + const message = await widgetMutations.widgetsInsertMessage( + {}, + { + contentType: MESSAGE_TYPES.TEXT, + integrationId: _integration._id, + customerId: _customer._id, + message: 'withConversationId', + conversationId: conversation._id, + }, + ); + + expect(message.content).toBe('withConversationId'); + }); + + test('Widget bot message with conversationId', async () => { + const conversation = await conversationFactory({ operatorStatus: CONVERSATION_OPERATOR_STATUS.BOT }); + + const sendRequestMock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve({ + responses: [ + { + type: 'text', + text: 'Bot message', + }, + ], + }); + }); + + const message = await widgetMutations.widgetsInsertMessage( + {}, + { + contentType: MESSAGE_TYPES.TEXT, + integrationId: _integrationBot._id, + message: 'User message', + customerId: _customer._id, + conversationId: conversation._id, + }, + ); + + expect(message.content).toBe('User message'); + + const botMessage = await ConversationMessages.findOne({ + conversationId: conversation._id, + botData: { $exists: true }, + }); + + if (botMessage) { + expect(botMessage.botData).toEqual([ + { + type: 'text', + text: 'Bot message', + }, + ]); + } else { + fail('Bot message not found'); + } + + sendRequestMock.restore(); + }); + + test('Bot message without conversationId', async () => { + const sendRequestMock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve({ + responses: [ + { + type: 'text', + text: 'Bot message', + }, + ], + }); + }); + + const message = await widgetMutations.widgetsInsertMessage( + {}, + { + contentType: MESSAGE_TYPES.TEXT, + integrationId: _integrationBot._id, + message: 'User message', + customerId: _customer._id, + }, + ); + + expect(message.content).toBe('User message'); + + const botMessage = await ConversationMessages.findOne({ + conversationId: message.conversationId, + botData: { $exists: true }, + }); + + if (botMessage) { + expect(botMessage.botData).toEqual([ + { + type: 'text', + text: 'Bot message', + }, + ]); + } else { + fail('Bot message not found'); + } + + sendRequestMock.restore(); + }); + + test('Bot widget post request', async () => { + const conversation1 = await conversationFactory({ operatorStatus: CONVERSATION_OPERATOR_STATUS.BOT }); + const conversation2 = await conversationFactory({ operatorStatus: CONVERSATION_OPERATOR_STATUS.BOT }); + + const sendRequestMock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve({ + responses: [ + { + type: 'text', + text: 'Response of quick reply', + }, + ], + }); + }); + + const botMessage1 = await widgetMutations.widgetBotRequest( + {}, + { + integrationId: _integrationBot._id, + conversationId: conversation1._id, + customerId: _customer._id, + message: 'Reply message', + payload: 'Response of reply', + type: 'postback', + }, + ); + + const message1 = await ConversationMessages.findOne({ + conversationId: conversation1._id, + botData: { $exists: false }, + }); + + if (message1) { + expect(message1.content).toBe('Reply message'); + } else { + fail('Message not found'); + } + + expect(botMessage1.botData).toEqual([ + { + type: 'text', + text: 'Response of quick reply', + }, + ]); + + const botMessage2 = await widgetMutations.widgetBotRequest( + {}, + { + integrationId: _integrationBot._id, + conversationId: conversation2._id, + customerId: _customer._id, + message: 'Reply message 2', + payload: 'Response of reply', + type: 'say_something', + }, + ); + + const message2 = await ConversationMessages.findOne({ + conversationId: conversation2._id, + botData: { $exists: false }, + }); + + if (message2) { + expect(message2.content).toBe('Reply message 2'); + } else { + fail('Message not found'); + } + + expect(botMessage2.botData).toEqual([ + { + type: 'text', + text: 'Response of reply', + }, + ]); + + sendRequestMock.restore(); + }); +}); + +describe('saveBrowserInfo()', () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + afterEach(async () => { + // Clearing test data + await Integrations.deleteMany({}); + await Customers.deleteMany({}); + await Brands.deleteMany({}); + }); + + test('not found', async () => { + let customer = await customerFactory({}); + + // integration not found + try { + await widgetMutations.widgetsSaveBrowserInfo( + {}, + { + customerId: customer._id, + browserInfo: {}, + }, + ); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + + const integration = await integrationFactory({}); + customer = await customerFactory({ integrationId: integration._id }); + + try { + await widgetMutations.widgetsSaveBrowserInfo( + {}, + { + customerId: customer._id, + browserInfo: {}, + }, + ); + } catch (e) { + expect(e.message).toBe('Brand not found'); + } + }); + + test('success', async () => { + const user = await userFactory({}); + const brand = await brandFactory({}); + const integration = await integrationFactory({ brandId: brand._id }); + + const customer = await customerFactory({ integrationId: integration._id }); + + await engageMessageFactory({ + userId: user._id, + messenger: { + brandId: brand._id, + content: 'engageMessage', + rules: [ + { + text: 'text', + kind: 'currentPageUrl', + condition: 'is', + value: '/page', + }, + ], + }, + kind: 'visitorAuto', + method: 'messenger', + isLive: true, + }); + + const response = await widgetMutations.widgetsSaveBrowserInfo( + {}, + { + customerId: customer._id, + browserInfo: { url: '/page' }, + }, + ); + + expect(response && response.content).toBe('engageMessage'); + }); + + mock.restore(); +}); + +describe('rest', () => { + test('widgetsSaveCustomerGetNotified', async () => { + let customer = await customerFactory({}); + + customer = await widgetMutations.widgetsSaveCustomerGetNotified( + {}, + { + customerId: customer._id, + type: 'email', + value: 'email', + }, + ); + + expect(customer.visitorContactInfo && customer.visitorContactInfo.email).toBe('email'); + }); + + test('widgetsSendTypingInfo', async () => { + const conversation = await conversationFactory({}); + + const response = await widgetMutations.widgetsSendTypingInfo( + {}, + { + conversationId: conversation._id, + }, + ); + + expect(response).toBe('ok'); + }); + + test('widgetsReadConversationMessages', async () => { + const user = await userFactory({}); + const conversation = await conversationFactory({}); + + const message = await conversationMessageFactory({ + conversationId: conversation._id, + userId: user._id, + isCustomerRead: false, + }); + + expect(message.isCustomerRead).toBe(false); + + await widgetMutations.widgetsReadConversationMessages( + {}, + { + conversationId: conversation._id, + }, + ); + + const updatedMessage = await ConversationMessages.findOne({ _id: message._id }); + + expect(updatedMessage && updatedMessage.isCustomerRead).toBe(true); + }); + + test('getMessengerData', async () => { + const integration = await integrationFactory({ + languageCode: 'en', + messengerData: { + messages: { + en: { + welcome: 'welcome', + }, + }, + }, + }); + + const response = await getMessengerData(integration); + + expect(response.messages && response.messages.welcome).toBe('welcome'); + }); +}); + +describe('knowledgebase', () => { + test('widgetsKnowledgebaseIncReactionCount', async () => { + const article = await knowledgeBaseArticleFactory({ + reactionChoices: ['wow'], + }); + + await widgetMutations.widgetsKnowledgebaseIncReactionCount( + {}, + { + articleId: article._id, + reactionChoice: 'wow', + }, + ); + + const updatedArticle = await KnowledgeBaseArticles.findOne({ _id: article._id }); + + expect(updatedArticle && updatedArticle.reactionCounts && updatedArticle.reactionCounts.wow).toBe(1); + }); +}); + +describe('lead', () => { + afterEach(async () => { + // Clearing test data + await Integrations.deleteMany({}); + await Customers.deleteMany({}); + await Conversations.deleteMany({}); + await ConversationMessages.deleteMany({}); + await FormSubmissions.deleteMany({}); + }); + + test('widgetsLeadIncreaseViewCount', async () => { + const form = await formFactory({}); + const integration = await integrationFactory({ formId: form._id }); + + await widgetMutations.widgetsLeadIncreaseViewCount( + {}, + { + formId: form._id, + }, + ); + + const updatedInteg = await Integrations.findOne({ _id: integration._id }); + + expect(updatedInteg && updatedInteg.leadData && updatedInteg.leadData.viewCount).toBe(1); + }); + + test('leadConnect: not found', async () => { + // invalid configuration + try { + await widgetMutations.widgetsLeadConnect( + {}, + { + brandCode: 'code', + formCode: 'code', + }, + ); + } catch (e) { + expect(e.message).toBe('Invalid configuration'); + } + + const brand = await brandFactory({}); + const form = await formFactory({}); + + try { + await widgetMutations.widgetsLeadConnect( + {}, + { + brandCode: brand.code || '', + formCode: form.code || '', + }, + ); + } catch (e) { + expect(e.message).toBe('Integration not found'); + } + }); + + test('leadConnect: success', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const brand = await brandFactory({}); + const form = await formFactory({}); + + const integration = await integrationFactory({ + brandId: brand._id, + formId: form._id, + leadData: { + loadType: 'embedded', + }, + }); + + const response = await widgetMutations.widgetsLeadConnect( + {}, + { + brandCode: brand.code || '', + formCode: form.code || '', + }, + ); + + expect(response && response.integration._id).toBe(integration._id); + expect(response && response.form._id).toBe(form._id); + + mock.restore(); + }); + + test('leadConnect: Already filled', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const brand = await brandFactory({}); + const form = await formFactory({}); + + const integration = await integrationFactory({ + brandId: brand._id, + formId: form._id, + leadData: { + loadType: 'embedded', + isRequireOnce: true, + }, + }); + + const conversation = await conversationFactory({ customerId: '123123', integrationId: integration._id }); + + const response = await widgetMutations.widgetsLeadConnect( + {}, + { + brandCode: brand.code || '', + formCode: form.code || '', + cachedCustomerId: '123123', + }, + ); + expect(conversation).toBeDefined(); + expect(response).toBeNull(); + mock.restore(); + }); + + test('saveLead: form not found', async () => { + try { + await widgetMutations.widgetsSaveLead( + {}, + { + integrationId: '_id', + formId: '_id', + submissions: [{ _id: 'id', value: null }], + browserInfo: {}, + }, + ); + } catch (e) { + expect(e.message).toBe('Form not found'); + } + }); + + test('saveLead: invalid', async () => { + const form = await formFactory({}); + + const requiredField = await fieldFactory({ + contentTypeId: form._id, + isRequired: true, + }); + + const integration = await integrationFactory({ formId: form._id }); + + const response = await widgetMutations.widgetsSaveLead( + {}, + { + integrationId: integration._id, + formId: form._id, + submissions: [{ _id: requiredField._id, value: null }], + browserInfo: { + currentPageUrl: '/page', + }, + }, + ); + + expect(response && response.status).toBe('error'); + }); + + test('saveLead: success', async () => { + const mock = sinon.stub(utils, 'sendRequest').callsFake(() => { + return Promise.resolve('success'); + }); + + const form = await formFactory({}); + + const emailField = await fieldFactory({ + type: 'email', + contentTypeId: form._id, + validation: 'text', + isRequired: true, + }); + + const firstNameField = await fieldFactory({ + type: 'firstName', + contentTypeId: form._id, + validation: 'text', + isRequired: true, + }); + + const lastNameField = await fieldFactory({ + type: 'lastName', + contentTypeId: form._id, + validation: 'text', + isRequired: true, + }); + + const phoneField = await fieldFactory({ + type: 'phone', + contentTypeId: form._id, + isRequired: true, + }); + + const checkField = await fieldFactory({ + type: 'check', + contentTypeId: form._id, + validation: 'text', + options: ['check1', 'check2'], + }); + + const radioField = await fieldFactory({ + type: 'radio', + contentTypeId: form._id, + validation: 'text', + options: ['radio1', 'radio2'], + }); + + const integration = await integrationFactory({ formId: form._id }); + + const response = await widgetMutations.widgetsSaveLead( + {}, + { + integrationId: integration._id, + formId: form._id, + submissions: [ + { _id: emailField._id, type: 'email', value: 'email@yahoo.com' }, + { _id: firstNameField._id, type: 'firstName', value: 'firstName' }, + { _id: lastNameField._id, type: 'lastName', value: 'lastName' }, + { _id: phoneField._id, type: 'phone', value: '+88998833' }, + { _id: radioField._id, type: 'radio', value: 'radio2' }, + { _id: checkField._id, type: 'check', value: 'check1, check2' }, + ], + browserInfo: { + currentPageUrl: '/page', + }, + }, + ); + + expect(response && response.status).toBe('ok'); + + expect(await Conversations.find().countDocuments()).toBe(1); + expect(await ConversationMessages.find().countDocuments()).toBe(1); + expect(await Customers.find().countDocuments()).toBe(1); + expect(await FormSubmissions.find().countDocuments()).toBe(1); + + const message = await ConversationMessages.findOne(); + const formData = message ? message.formWidgetData : {}; + + if (!message || !formData) { + throw new Error('Message not found'); + } + + expect(formData[0].value).toBe('email@yahoo.com'); + expect(formData[1].value).toBe('firstName'); + expect(formData[2].value).toBe('lastName'); + expect(formData[3].value).toBe('+88998833'); + expect(formData[4].value).toBe('radio2'); + expect(formData[5].value).toBe('check1, check2'); + mock.restore(); + }); + + test('widgetsSendEmail', async () => { + const emailParams = { + toEmails: ['test-mail@gmail.com'], + fromEmail: 'admin@erxes.io', + title: 'Thank you for submitting.', + content: 'We have received your request', + }; + + const spyEmail = jest.spyOn(widgetMutations, 'widgetsSendEmail'); + + const mutation = ` + mutation widgetsSendEmail($toEmails: [String], $fromEmail: String, $title: String, $content: String) { + widgetsSendEmail(toEmails: $toEmails, fromEmail: $fromEmail, title: $title, content: $content) + } + `; + + spyEmail.mockImplementation(() => Promise.resolve()); + + const response = await graphqlRequest(mutation, 'widgetsSendEmail', emailParams); + + expect(response).toBe(null); + + spyEmail.mockRestore(); + }); +}); diff --git a/src/__tests__/widgetQueries.test.ts b/src/__tests__/widgetQueries.test.ts new file mode 100644 index 000000000..4ec69ade0 --- /dev/null +++ b/src/__tests__/widgetQueries.test.ts @@ -0,0 +1,266 @@ +import { graphqlRequest } from '../db/connection'; + +import { isMessengerOnline } from '../data/resolvers/queries/widgets'; +import { + brandFactory, + conversationFactory, + conversationMessageFactory, + customerFactory, + integrationFactory, + knowledgeBaseArticleFactory, + knowledgeBaseCategoryFactory, + knowledgeBaseTopicFactory, + userFactory, +} from '../db/factories'; +import { Conversations, Customers, Integrations } from '../db/models'; +import './setup.ts'; + +describe('widgetQueries', () => { + afterEach(async () => { + // Clearing test data + await Conversations.deleteMany({}); + await Customers.deleteMany({}); + await Integrations.deleteMany({}); + }); + + test('Conversations', async () => { + // Creating test data + const integration = await integrationFactory({}); + const customer = await customerFactory({ integrationId: integration._id }); + + await conversationFactory({ integrationId: integration._id }); + await conversationFactory({ customerId: customer._id, integrationId: integration._id }); + await conversationFactory({ customerId: customer._id, integrationId: integration._id }); + + const qry = ` + query widgetsConversations($integrationId: String!, $customerId: String!) { + widgetsConversations(integrationId: $integrationId, customerId: $customerId) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'widgetsConversations', { + integrationId: integration._id, + customerId: customer._id, + }); + + expect(response.length).toBe(2); + }); + + test('Conversation detail', async () => { + // Creating test data + const user = await userFactory({}); + + const integration = await integrationFactory({ + kind: 'messenger', + messengerData: { supporterIds: [user._id] }, + }); + + const conversation = await conversationFactory({ integrationId: integration._id }); + + const qry = ` + query widgetsConversationDetail($_id: String, $integrationId: String!) { + widgetsConversationDetail(_id: $_id, integrationId: $integrationId) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'widgetsConversationDetail', { _id: '_id', integrationId: '_id ' }); + expect(response).toBe(null); + + response = await graphqlRequest(qry, 'widgetsConversationDetail', { _id: conversation._id, integrationId: '_id ' }); + expect(response).toBe(null); + + response = await graphqlRequest(qry, 'widgetsConversationDetail', { + _id: '_id', + integrationId: conversation.integrationId, + }); + expect(response).not.toBeNull(); + + response = await graphqlRequest(qry, 'widgetsConversationDetail', { + _id: conversation._id, + integrationId: conversation.integrationId, + }); + + expect(response._id).toBe(conversation._id); + }); + + test('getMessengerIntegration', async () => { + // Creating test data + const brand = await brandFactory({}); + const integration = await integrationFactory({ kind: 'messenger', brandId: brand._id }); + + const qry = ` + query widgetsGetMessengerIntegration($brandCode: String!) { + widgetsGetMessengerIntegration(brandCode: $brandCode) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'widgetsGetMessengerIntegration', { + brandCode: brand.code, + }); + + expect(response._id).toBe(integration._id); + }); + + test('widgetsMessengerSupporters', async () => { + // Creating test data + const user = await userFactory({}); + + const integration = await integrationFactory({ + kind: 'messenger', + messengerData: { supporterIds: [user._id] }, + }); + + const qry = ` + query widgetsMessengerSupporters($integrationId: String!) { + widgetsMessengerSupporters(integrationId: $integrationId) { + supporters { + _id + } + isOnline + serverTime + } + } + `; + + try { + await graphqlRequest(qry, 'widgetsMessengerSupporters', { integrationId: '_id' }); + } catch (e) { + expect(e[0].message).toBe('Integration not found'); + } + + const response = await graphqlRequest(qry, 'widgetsMessengerSupporters', { integrationId: integration._id }); + + expect(response.supporters.length).toBe(1); + }); + + test('widgetsTotalUnreadCount', async () => { + // Creating test data + const user = await userFactory({}); + const integration = await integrationFactory({ kind: 'messenger' }); + const customer = await customerFactory({}); + + const conv1 = await conversationFactory({ integrationId: integration._id, customerId: customer._id }); + await conversationMessageFactory({ + conversationId: conv1._id, + userId: user._id, + isCustomerRead: true, + internal: false, + }); + + const conv2 = await conversationFactory({ integrationId: integration._id, customerId: customer._id }); + await conversationMessageFactory({ + conversationId: conv2._id, + userId: user._id, + isCustomerRead: false, + internal: false, + }); + + const qry = ` + query widgetsTotalUnreadCount($integrationId: String!, $customerId: String!) { + widgetsTotalUnreadCount(integrationId: $integrationId, customerId: $customerId) + } + `; + + const response = await graphqlRequest(qry, 'widgetsTotalUnreadCount', { + integrationId: integration._id, + customerId: customer._id, + }); + + expect(response).toBe(1); + }); + + test('widgetsUnreadCount', async () => { + // Creating test data + const user = await userFactory({}); + const integration = await integrationFactory({ kind: 'messenger' }); + const customer = await customerFactory({}); + + const conv1 = await conversationFactory({ integrationId: integration._id, customerId: customer._id }); + await conversationMessageFactory({ + conversationId: conv1._id, + userId: user._id, + isCustomerRead: false, + internal: false, + }); + + const qry = ` + query widgetsUnreadCount($conversationId: String!) { + widgetsUnreadCount(conversationId: $conversationId) + } + `; + + const response = await graphqlRequest(qry, 'widgetsUnreadCount', { conversationId: conv1._id }); + + expect(response).toBe(1); + }); + + test('widgetsMessages', async () => { + // Creating test data + const conv = await conversationFactory({}); + await conversationMessageFactory({ conversationId: conv._id, internal: false }); + + const qry = ` + query widgetsMessages($conversationId: String!) { + widgetsMessages(conversationId: $conversationId) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'widgetsMessages', { conversationId: conv._id }); + + expect(response.length).toBe(1); + }); + + test('widgetsKnowledgeBaseTopicDetail', async () => { + const user = await userFactory({}); + const topic = await knowledgeBaseTopicFactory({ userId: user._id }); + + const qry = ` + query widgetsKnowledgeBaseTopicDetail($_id: String!) { + widgetsKnowledgeBaseTopicDetail(_id: $_id) { + _id + } + } + `; + + const response = await graphqlRequest(qry, 'widgetsKnowledgeBaseTopicDetail', { _id: topic._id }); + + expect(response._id).toBe(topic._id); + }); + + test('widgetsKnowledgeBaseArticles', async () => { + // Creating test data + const topic = await knowledgeBaseTopicFactory({}); + const category = await knowledgeBaseCategoryFactory({ topicIds: [topic._id] }); + await knowledgeBaseArticleFactory({ categoryIds: [category._id], status: 'publish' }); + + const qry = ` + query widgetsKnowledgeBaseArticles($topicId: String!, $searchString: String) { + widgetsKnowledgeBaseArticles(topicId: $topicId, searchString: $searchString) { + _id + } + } + `; + + let response = await graphqlRequest(qry, 'widgetsKnowledgeBaseArticles', { topicId: '_id' }); + expect(response.length).toBe(0); + + response = await graphqlRequest(qry, 'widgetsKnowledgeBaseArticles', { topicId: topic._id }); + expect(response.length).toBe(1); + }); + + test('isMessengerOnline', async () => { + const integration = await integrationFactory({}); + + const response = await isMessengerOnline(integration); + + expect(response).toBe(false); + }); +}); diff --git a/src/apolloClient.ts b/src/apolloClient.ts index ebe346854..02d4aeca4 100644 --- a/src/apolloClient.ts +++ b/src/apolloClient.ts @@ -1,16 +1,18 @@ import { ApolloServer, PlaygroundConfig } from 'apollo-server-express'; +import * as cookie from 'cookie'; import * as dotenv from 'dotenv'; +import * as jwt from 'jsonwebtoken'; +import { EngagesAPI, IntegrationsAPI } from './data/dataSources'; import resolvers from './data/resolvers'; import typeDefs from './data/schema'; -import { getEnv } from './data/utils'; -import { Conversations, Customers } from './db/models'; +import { Conversations, Customers, Users } from './db/models'; +import memoryStorage from './inmemoryStorage'; import { graphqlPubsub } from './pubsub'; -import { get, getArray, set, setArray } from './redisClient'; // load environment variables dotenv.config(); -const NODE_ENV = getEnv({ name: 'NODE_ENV' }); +const { NODE_ENV, USE_BRAND_RESTRICTIONS } = process.env; let playground: PlaygroundConfig = false; @@ -28,45 +30,113 @@ if (NODE_ENV !== 'production') { }; } +const generateDataSources = () => { + return { + EngagesAPI: new EngagesAPI(), + IntegrationsAPI: new IntegrationsAPI(), + }; +}; + const apolloServer = new ApolloServer({ typeDefs, resolvers, + dataSources: generateDataSources, playground, uploads: false, - context: ({ req, res }) => { + context: ({ req, res, connection }) => { + let user = req && req.user ? req.user : null; + + if (!req) { + if (connection && connection.context && connection.context.user) { + user = connection.context.user; + } + + return { + dataSources: generateDataSources(), + user, + }; + } + + const requestInfo = { + secure: req.secure, + cookies: req.cookies, + }; + + if (USE_BRAND_RESTRICTIONS !== 'true') { + return { + brandIdSelector: {}, + singleBrandIdSelector: {}, + userBrandIdsSelector: {}, + docModifier: doc => doc, + commonQuerySelector: {}, + user, + res, + requestInfo, + }; + } + + let scopeBrandIds = JSON.parse(req.cookies.scopeBrandIds || '[]'); + let brandIds = []; + let brandIdSelector = {}; + let commonQuerySelector = {}; + let commonQuerySelectorElk; + let userBrandIdsSelector = {}; + let singleBrandIdSelector = {}; + + if (user) { + brandIds = user.brandIds || []; + + if (scopeBrandIds.length === 0) { + scopeBrandIds = brandIds; + } + + if (!user.isOwner) { + brandIdSelector = { _id: { $in: scopeBrandIds } }; + commonQuerySelector = { scopeBrandIds: { $in: scopeBrandIds } }; + commonQuerySelectorElk = { terms: { scopeBrandIds } }; + userBrandIdsSelector = { brandIds: { $in: scopeBrandIds } }; + singleBrandIdSelector = { brandId: { $in: scopeBrandIds } }; + } + } + return { - user: req && req.user, + brandIdSelector, + singleBrandIdSelector, + docModifier: doc => ({ ...doc, scopeBrandIds }), + commonQuerySelector, + commonQuerySelectorElk, + userBrandIdsSelector, + user, res, + requestInfo, }; }, subscriptions: { keepAlive: 10000, path: '/subscriptions', - onConnect(_connectionParams, webSocket) { + onConnect(_connectionParams, webSocket: any, connectionContext: any) { webSocket.on('message', async message => { - const parsedMessage = JSON.parse(message).id || {}; + const parsedMessage = JSON.parse(message.toString()).id || {}; if (parsedMessage.type === 'messengerConnected') { - // get status from redis - const connectedClients = await getArray('connectedClients'); - const clients = await getArray('clients'); - webSocket.messengerData = parsedMessage.value; const customerId = webSocket.messengerData.customerId; - if (!connectedClients.includes(customerId)) { - connectedClients.push(customerId); - await setArray('connectedClients', connectedClients); + // get status from inmemory storage + const inConnectedClients = await memoryStorage().inArray('connectedClients', customerId); + const inClients = await memoryStorage().inArray('clients', customerId); + + if (!inConnectedClients) { + await memoryStorage().addToArray('connectedClients', customerId); } // Waited for 1 minute to reconnect in disconnect hook and disconnect hook // removed this customer from connected clients list. So it means this customer // is back online - if (!clients.includes(customerId)) { - clients.push(customerId); - await setArray('clients', clients); + if (!inClients) { + await memoryStorage().addToArray('clients', customerId); // mark as online await Customers.markCustomerAsActive(customerId); @@ -81,15 +151,28 @@ const apolloServer = new ApolloServer({ } } }); + + let user; + + try { + const cookies = cookie.parse(connectionContext.request.headers.cookie); + + const jwtContext = jwt.verify(cookies['auth-token'], Users.getSecret()); + + user = jwtContext.user; + } catch (e) { + user = null; + } + + return { + user, + }; }, - async onDisconnect(webSocket) { + async onDisconnect(webSocket: any) { const messengerData = webSocket.messengerData; if (messengerData) { - // get status from redis - let connectedClients = await getArray('connectedClients'); - const customerId = messengerData.customerId; const integrationId = messengerData.integrationId; @@ -97,27 +180,24 @@ const apolloServer = new ApolloServer({ // If client refreshes his browser, It will trigger disconnect, connect hooks. // So to determine this issue. We are marking as disconnected here and waiting // for 1 minute to reconnect. - connectedClients.splice(connectedClients.indexOf(customerId), 1); - await setArray('connectedClients', connectedClients); + await memoryStorage().removeFromArray('connectedClients', customerId); setTimeout(async () => { - // get status from redis - connectedClients = await getArray('connectedClients'); - const clients = await getArray('clients'); - const customerLastStatus = await get(`customer_last_status_${customerId}`); + // get status from inmemory storage + const inNewConnectedClients = await memoryStorage().inArray('connectedClients', customerId); + const customerLastStatus = await memoryStorage().get(`customer_last_status_${customerId}`); - if (connectedClients.includes(customerId)) { + if (inNewConnectedClients) { return; } - clients.splice(clients.indexOf(customerId), 1); - await setArray('clients', clients); + await memoryStorage().removeFromArray('clients', customerId); // mark as offline await Customers.markCustomerAsNotActive(customerId); if (customerLastStatus !== 'left') { - set(`customer_last_status_${customerId}`, 'left'); + memoryStorage().set(`customer_last_status_${customerId}`, 'left'); // customer has left + time const conversationMessages = await Conversations.changeCustomerStatus('left', customerId, integrationId); diff --git a/src/commands/aftertest.ts b/src/commands/aftertest.ts deleted file mode 100644 index 019a030dc..000000000 --- a/src/commands/aftertest.ts +++ /dev/null @@ -1,43 +0,0 @@ -import * as dotenv from 'dotenv'; -import mongoose = require('mongoose'); - -mongoose.Promise = global.Promise; - -// load environment variables -dotenv.config(); - -const TEST_MONGO_URL = process.env.TEST_MONGO_URL || 'mongodb://localhost/test'; - -// prevent deprecated warning related findAndModify -// https://github.com/Automattic/mongoose/issues/6880 -mongoose.set('useFindAndModify', false); - -const removeDbs = async () => { - await mongoose.connect( - TEST_MONGO_URL.replace('test', `erxes-test-${Math.random()}`).replace('.', ''), - { useNewUrlParser: true, useCreateIndex: true }, - ); - - const result = await mongoose.connection.db.admin().command({ - listDatabases: 1, - nameOnly: true, - filter: { name: /^erxes-test/ }, - }); - - const promises: any[] = []; - - for (const { name } of result.databases) { - const db = await mongoose.connect( - TEST_MONGO_URL.replace('test', name), - { useNewUrlParser: true, useCreateIndex: true }, - ); - - promises.push(db.connection.dropDatabase()); - } - - return Promise.all(promises); -}; - -removeDbs().then(() => { - process.exit(); -}); diff --git a/src/commands/createGooglePubsubTopics.ts b/src/commands/createGooglePubsubTopics.ts deleted file mode 100644 index 36cafeacd..000000000 --- a/src/commands/createGooglePubsubTopics.ts +++ /dev/null @@ -1,84 +0,0 @@ -import * as PubSub from '@google-cloud/pubsub'; -import * as dotenv from 'dotenv'; -import * as fs from 'fs'; -import * as ora from 'ora'; -import * as path from 'path'; - -// load environment variables -dotenv.config(); - -const { - PUBSUB_TYPE, - GOOGLE_APPLICATION_CREDENTIALS, -}: { - PUBSUB_TYPE?: string; - GOOGLE_APPLICATION_CREDENTIALS?: string; -} = process.env; - -const topics = [ - 'activityLogsChanged', - 'conversationAdminMessageInserted', - 'conversationChanged', - 'conversationClientMessageInserted', - 'conversationMessageInserted', - 'customerConnectionChanged', - 'widgetNotification', -]; - -const spinnerOptions = { - prefixText: 'Creating google pubsub topics \n', -}; - -const spinner = ora(spinnerOptions); - -// Create google pubsub's topics for subscriptions -const start = async () => { - spinner.start(); - - if (PUBSUB_TYPE !== 'GOOGLE') { - throw new Error('Pubsub type is not configured for Google'); - } - - const checkHasConfigFile = fs.existsSync(path.join(__dirname, '..', '../google_cred.json')); - - if (!checkHasConfigFile) { - throw new Error('Google credentials file not found!'); - } - - const serviceAccount = require('../../google_cred.json'); - - const googleClient = PubSub({ - projectId: serviceAccount.project_id, - keyFilename: GOOGLE_APPLICATION_CREDENTIALS, - }); - - /* - * Schema - * [[ Topic: { name, pubsub, parent, request } ]] - */ - const [pubsubTopics = []] = await googleClient.getTopics(); - const existingTopics: string[] = []; - - if (pubsubTopics.length > 0) { - for (const topic of pubsubTopics) { - const name = topic.name.split('/'); - const lastName = name.pop(); - - existingTopics.push(lastName); - } - } - - const filteredTopics = topics.filter(topic => !existingTopics.includes(topic)); - - if (filteredTopics.length === 0) { - return spinner.succeed('You already have topics'); - } - - for (const topic of filteredTopics) { - await googleClient.createTopic(topic); - } - - spinner.succeed('Successfully created google pubsub topics.'); -}; - -start(); diff --git a/src/commands/customCommand.ts b/src/commands/customCommand.ts new file mode 100644 index 000000000..e97211e3a --- /dev/null +++ b/src/commands/customCommand.ts @@ -0,0 +1,49 @@ +import * as dotenv from 'dotenv'; +import { connect } from '../db/connection'; +import { Customers } from '../db/models'; + +dotenv.config(); + +const command = async () => { + await connect(); + + const argv = process.argv; + const limit = argv.pop(); + + const selector = { + relatedIntegrationIds: { $exists: false }, + integrationId: { $exists: true, $ne: null }, + profileScore: { $gt: 0 }, + }; + + const customers = await Customers.find(selector).limit(limit ? parseInt(limit, 10) : 10000); + + console.log(`Limit: ${limit}, length: ${customers.length}`); + + const bulkOptions: any[] = []; + + for (const customer of customers) { + if (!customer.integrationId) { + continue; + } + + bulkOptions.push({ + updateOne: { + filter: { + _id: customer._id, + }, + update: { + $set: { + relatedIntegrationIds: [customer.integrationId], + }, + }, + }, + }); + } + + await Customers.bulkWrite(bulkOptions); + + process.exit(); +}; + +command(); diff --git a/src/commands/engageSubscriptions.ts b/src/commands/engageSubscriptions.ts deleted file mode 100644 index 1f867aa14..000000000 --- a/src/commands/engageSubscriptions.ts +++ /dev/null @@ -1,78 +0,0 @@ -import * as dotenv from 'dotenv'; -import { getEnv } from '../data/utils'; -import { getApi } from '../trackers/engageTracker'; - -const start = () => { - // load environment variables - dotenv.config(); - - const AWS_SES_CONFIG_SET = getEnv({ name: 'AWS_SES_CONFIG_SET' }); - const AWS_ENDPOINT = getEnv({ name: 'AWS_ENDPOINT' }); - - let topicArn = ''; - - // Automatically creating aws configs - getApi('sns') - // Create Topic - .createTopic({ Name: AWS_SES_CONFIG_SET }) - .promise() - // Subscribing to the topic - .then(result => { - topicArn = result.TopicArn; - - return getApi('sns') - .subscribe({ - TopicArn: topicArn, - Protocol: 'https', - Endpoint: `${AWS_ENDPOINT}/service/engage/tracker`, - }) - .promise(); - }) - // Creating configuration set - .then(() => { - console.log('Successfully subscribed to the topic'); - - return getApi('ses') - .createConfigurationSet({ - ConfigurationSet: { - Name: AWS_SES_CONFIG_SET, - }, - }) - .promise(); - }) - .catch(error => { - console.log(error.message); - }) - // Creating event destination for configuration set - .then(() => { - console.log('Successfully created config set'); - - return getApi('ses') - .createConfigurationSetEventDestination({ - ConfigurationSetName: AWS_SES_CONFIG_SET, - EventDestination: { - MatchingEventTypes: [ - 'send', - 'reject', - 'bounce', - 'complaint', - 'delivery', - 'open', - 'click', - 'renderingFailure', - ], - Name: AWS_SES_CONFIG_SET, - Enabled: true, - SNSDestination: { - TopicARN: topicArn, - }, - }, - }) - .promise(); - }) - .catch(error => { - console.log(error.message); - }); -}; - -start(); diff --git a/src/commands/generateVersion.ts b/src/commands/generateVersion.ts deleted file mode 100644 index 0923ba011..000000000 --- a/src/commands/generateVersion.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as fs from 'fs'; -import * as gitRepoInfo from 'git-repo-info'; -import * as path from 'path'; - -const start = () => { - const projectPath = process.cwd(); - const packageVersion = require(path.join(projectPath, 'package.json')).version; - const info = gitRepoInfo(); - const versionInfo = { packageVersion, ...info }; - - fs.writeFile('./dist/private/version.json', JSON.stringify(versionInfo), () => { - console.log('saved'); - }); -}; - -start(); diff --git a/src/commands/initProject.ts b/src/commands/initProject.ts index 4f0a7a07e..a26d32640 100644 --- a/src/commands/initProject.ts +++ b/src/commands/initProject.ts @@ -3,10 +3,19 @@ import { Users } from '../db/models'; connect() .then(async () => { + // generate random password + const generator = require('generate-password'); + const newPwd = generator.generate({ + length: 10, + numbers: true, + lowercase: true, + uppercase: true, + }); + // create admin user - const user = await Users.createUser({ + const user = await Users.create({ username: 'admin', - password: 'erxes', + password: await Users.generatePassword(newPwd), email: 'admin@erxes.io', isOwner: true, details: { @@ -14,7 +23,7 @@ connect() }, }); - await Users.updateOne({ _id: user._id }, { $set: { isOwner: true } }); + console.log('\x1b[32m%s\x1b[0m', 'Your new password: ' + newPwd); return Users.findOne({ _id: user._id }); }) diff --git a/src/commands/loadGrowthHackData.ts b/src/commands/loadGrowthHackData.ts new file mode 100644 index 000000000..41f2e0541 --- /dev/null +++ b/src/commands/loadGrowthHackData.ts @@ -0,0 +1,17 @@ +import * as shelljs from 'shelljs'; +import { getEnv } from '../data/utils'; + +const main = async () => { + const MONGO_URL = getEnv({ name: 'MONGO_URL' }); + + const result = await shelljs.exec(`mongorestore --uri ${MONGO_URL} --db erxes ./src/initialData/growthHack`, { + silent: true, + }); + const output = result.stderr + result.stdout; + + console.log(output); + + process.exit(); +}; + +main(); diff --git a/src/commands/loadInitialData.ts b/src/commands/loadInitialData.ts new file mode 100644 index 000000000..bb0eaed07 --- /dev/null +++ b/src/commands/loadInitialData.ts @@ -0,0 +1,47 @@ +import * as dotenv from 'dotenv'; +import * as shelljs from 'shelljs'; +import { getEnv } from '../data/utils'; +import { connect } from '../db/connection'; +import { Users } from '../db/models'; + +dotenv.config(); + +const main = async () => { + const MONGO_URL = getEnv({ name: 'MONGO_URL' }); + + const connection = await connect(); + + const dbName = connection.connection.db.databaseName; + console.log(`drop and create database: ${dbName}`); + + await connection.connection.dropDatabase(); + + const result = await shelljs.exec(`mongorestore --uri "${MONGO_URL}" --db ${dbName} ./src/initialData/common`, { + silent: true, + }); + const output = result.stderr + result.stdout; + + console.log(output); + + console.log(`success, imported initial data to: ${dbName}`); + + const generator = require('generate-password'); + const newPwd = generator.generate({ + length: 10, + numbers: true, + lowercase: true, + uppercase: true, + }); + + const pwdHash = await Users.generatePassword(newPwd); + + await shelljs.exec(`mongo "${MONGO_URL}" --eval 'db.users.update({}, { $set: {password: "${pwdHash}" } })'`, { + silent: true, + }); + + console.log('\x1b[32m%s\x1b[0m', 'Your new password: ' + newPwd); + + process.exit(); +}; + +main(); diff --git a/src/commands/loadPermissionData.ts b/src/commands/loadPermissionData.ts new file mode 100644 index 000000000..5f5f4001a --- /dev/null +++ b/src/commands/loadPermissionData.ts @@ -0,0 +1,18 @@ +import * as shelljs from 'shelljs'; +import { getEnv } from '../data/utils'; + +const main = async () => { + const MONGO_URL = getEnv({ name: 'MONGO_URL' }); + + const result = await shelljs.exec(`mongorestore --uri "${MONGO_URL}" --db erxes ./src/initialData/permission`, { + silent: true, + }); + + const output = result.stderr + result.stdout; + + console.log(output); + + process.exit(); +}; + +main(); diff --git a/src/commands/loadTestData.ts b/src/commands/loadTestData.ts new file mode 100644 index 000000000..39ecf4752 --- /dev/null +++ b/src/commands/loadTestData.ts @@ -0,0 +1,959 @@ +import * as dotenv from 'dotenv'; +import * as faker from 'faker'; +import * as fs from 'fs'; +import { disconnect } from 'mongoose'; +import * as shelljs from 'shelljs'; +import * as XlsxStreamReader from 'xlsx-stream-reader'; +import { checkFieldNames } from '../data/modules/fields/utils'; +import widgetMutations from '../data/resolvers/mutations/widgets'; +import { getEnv } from '../data/utils'; +import { connect } from '../db/connection'; +import { + Boards, + Brands, + Channels, + Companies, + Conformities, + Conversations, + Customers, + Deals, + EmailTemplates, + EngageMessages, + Fields, + Forms, + ImportHistory, + Integrations, + KnowledgeBaseArticles, + KnowledgeBaseCategories, + KnowledgeBaseTopics, + Pipelines, + PipelineTemplates, + ProductCategories, + Products, + Segments, + Stages, + Tags, + Tasks, + Tickets, + Users, + UsersGroups, + Configs, +} from '../db/models'; +import { IPipelineStage } from '../db/models/definitions/boards'; +import { LEAD_LOAD_TYPES, MESSAGE_TYPES, TAG_TYPES } from '../db/models/definitions/constants'; +import { debugWorkers } from '../debuggers'; +import { initMemoryStorage } from '../inmemoryStorage'; +import { clearEmptyValues, generatePronoun, updateDuplicatedValue } from '../workers/utils'; +import memoryStorage from './../inmemoryStorage'; +dotenv.config(); + +export const icons = [ + { value: 'alarm', label: 'alarm' }, + { value: 'briefcase', label: 'briefcase' }, + { value: 'earthgrid', label: 'earthgrid' }, + { value: 'compass', label: 'compass' }, + { value: 'idea', label: 'idea' }, + { value: 'diamond', label: 'diamond' }, + { value: 'piggybank', label: 'piggybank' }, + { value: 'piechart', label: 'piechart' }, + { value: 'scale', label: 'scale' }, + { value: 'megaphone', label: 'megaphone' }, + { value: 'tools', label: 'tools' }, + { value: 'umbrella', label: 'umbrella' }, + { value: 'bar-chart', label: 'bar-chart' }, + { value: 'star', label: 'star' }, + { value: 'head-1', label: 'head-1' }, + { value: 'settings', label: 'settings' }, + { value: 'users', label: 'users' }, + { value: 'paintpalette', label: 'paintpalette' }, + { value: 'flag', label: 'flag' }, + { value: 'phone-call', label: 'phone-call' }, + { value: 'laptop', label: 'laptop' }, + { value: 'home', label: 'home' }, + { value: 'puzzle', label: 'puzzle' }, + { value: 'medal', label: 'medal' }, + { value: 'like', label: 'like' }, + { value: 'book', label: 'book' }, + { value: 'clipboard', label: 'clipboard' }, + { value: 'computer', label: 'computer' }, + { value: 'paste', label: 'paste' }, + { value: 'folder-1', label: 'folder' }, +]; + +const main = async () => { + const MONGO_URL = getEnv({ name: 'MONGO_URL' }); + + await shelljs.exec(`mongo "${MONGO_URL}" --eval 'db.killOp()'`, { + silent: true, + }); + + const connection = await connect(); + + initMemoryStorage(); + + const dbName = connection.connection.db.databaseName; + + console.log(`drop and create database: ${dbName}`); + + await connection.connection.dropDatabase(); + + const userGroup = await UsersGroups.create({ name: 'admin' }); + const groups = ['support', 'development', 'management']; + + console.log('Creating: UserGroups'); + + groups.forEach(async group => { + await UsersGroups.create({ name: group }); + }); + + console.log('Finished: UserGroups'); + + const brand = await Brands.create({ name: faker.random.word(), description: faker.lorem.lines() }); + + const generator = require('generate-password'); + + const newPwd = generator.generate({ + length: 10, + numbers: true, + lowercase: true, + uppercase: true, + strict: true, + }); + + const userDoc = { + createdAt: faker.date.recent(), + username: faker.internet.userName(), + password: newPwd, + isOwner: true, + email: 'admin@erxes.io', + getNotificationByEmail: true, + details: { + avatar: faker.image.avatar(), + fullName: faker.name.findName(), + shortName: faker.name.firstName(), + position: faker.name.jobTitle(), + location: faker.address.streetAddress(), + description: faker.name.title(), + operatorPhone: faker.phone.phoneNumber(), + }, + links: { + link: faker.internet.url(), + }, + brandIds: [brand.id], + groupIds: [userGroup.id], + isActive: true, + }; + + console.log('Creating: Users'); + + const admin = await Users.createUser(userDoc); + + for (let i = 0; i < 10; i++) { + const randomGroup = await UsersGroups.aggregate([{ $sample: { size: 1 } }]); + const fakeUserDoc = { + createdAt: faker.date.recent(), + username: faker.internet.userName(), + password: newPwd, + isOwner: false, + email: faker.internet.email(), + getNotificationByEmail: true, + details: { + avatar: faker.image.avatar(), + fullName: faker.name.findName(), + shortName: faker.name.firstName(), + position: faker.name.jobTitle(), + location: faker.address.streetAddress(), + description: faker.name.title(), + operatorPhone: faker.phone.phoneNumber(), + }, + links: { + link: faker.internet.url(), + }, + brandIds: [brand.id], + groupIds: [randomGroup[0].id], + isActive: true, + }; + await Users.createUser(fakeUserDoc); + } + console.log('Finished: Users'); + + console.log('Creating: Channels'); + const channel = await Channels.createChannel( + { name: faker.random.word(), description: faker.lorem.sentence(), memberIds: [admin._id] }, + admin._id, + ); + + console.log('Finished: Channels'); + + console.log('Creating: Messenger Integration'); + + const integration = await Integrations.createMessengerIntegration( + { + kind: 'messenger', + languageCode: 'en', + channelIds: [channel._id], + name: faker.random.word(), + brandId: brand._id, + }, + admin._id, + ); + + await Channels.updateMany({ _id: { $in: [channel._id] } }, { $push: { integrationIds: integration._id } }); + + console.log('Finished: Messenger Integration'); + + // popup + + console.log('Creating: Popups'); + + const form = await createForms('lead', admin); + + let loadType = LEAD_LOAD_TYPES.ALL[Math.floor(Math.random() * LEAD_LOAD_TYPES.ALL.length)]; + + if (loadType.length === 0) { + loadType = LEAD_LOAD_TYPES.DROPDOWN; + } + + await Tags.createTag({ name: 'happy', type: TAG_TYPES.CUSTOMER, colorCode: '#4BBF6B' }); + await Tags.createTag({ name: 'angry', type: TAG_TYPES.CUSTOMER, colorCode: '#CD5A91' }); + await Tags.createTag({ name: 'other', type: TAG_TYPES.CUSTOMER, colorCode: '#F7CE53' }); + + await Tags.createTag({ name: 'happy', type: TAG_TYPES.CONVERSATION, colorCode: '#4BBF6B' }); + await Tags.createTag({ name: 'angry', type: TAG_TYPES.CONVERSATION, colorCode: '#CD5A91' }); + await Tags.createTag({ name: 'other', type: TAG_TYPES.CONVERSATION, colorCode: '#F7CE53' }); + + await Configs.createOrUpdateConfig({ code: 'UPLOAD_SERVICE_TYPE', value: ['local'] }); + + await Integrations.createLeadIntegration( + { + languageCode: 'en', + formId: form._id, + kind: 'lead', + brandId: brand._id, + name: faker.random.word(), + leadData: { + loadType, + successAction: 'redirect', + redirectUrl: faker.internet.url(), + thankContent: faker.lorem.sentence(), + }, + }, + admin._id, + ); + + console.log('Finished: Popups'); + + // Knowledgebase + console.log('Creating: KnowledgeBase'); + + const kbTopic = await KnowledgeBaseTopics.createDoc( + { + brandId: brand._id, + title: 'Get expert help from Erxes', + description: faker.lorem.sentence(), + languageCode: 'en', + color: faker.internet.color(), + backgroundImage: faker.image.abstract(), + }, + admin._id, + ); + + for (let i = 0; i < 3; i++) { + const kbCategory = await KnowledgeBaseCategories.createDoc( + { + title: faker.random.word(), + description: faker.lorem.sentence(), + icon: + icons[ + faker.random.number({ + min: 0, + max: 29, + }) + ].value, + topicIds: [kbTopic._id], + }, + admin._id, + ); + + for (let j = 0; j < 4; j++) { + await KnowledgeBaseArticles.createDoc( + { + title: faker.lorem.sentence(), + summary: faker.lorem.sentence(), + content: faker.lorem.paragraphs(), + reactionChoices: [ + 'https://erxes.s3.amazonaws.com/icons/sad.svg', + 'https://erxes.s3.amazonaws.com/icons/neutral.svg', + 'https://erxes.s3.amazonaws.com/icons/grinning.svg', + 'https://erxes.s3.amazonaws.com/icons/like.svg', + 'https://erxes.s3.amazonaws.com/icons/dislike.svg', + ], + status: 'publish', + categoryIds: [kbCategory._id], + }, + admin._id, + ); + } + } + console.log('Finished: Knowledgebase'); + + // Contacts + + console.log('Creating: Contacts'); + + for (let i = 0; i < 10; i++) { + await Customers.createVisitor(); + } + + const xlsDatas = [ + { fileName: 'fakeCompanies.xlsx', type: 'company' }, + { fileName: 'fakeCustomers.xlsx', type: 'customer' }, + ]; + + for (const data of xlsDatas) { + const { properties, result, type, importHistoryId } = await readXlsFile(data.fileName, data.type, admin); + + await insertToDB({ + user: admin, + scopeBrandIds: [brand._id], + result, + contentType: type, + properties, + importHistoryId, + }); + } + + console.log('Finished: Contacts'); + + // Sales PipeLine + console.log('Creating: Sales & Pipelines'); + + const productCategory = await ProductCategories.createProductCategory({ + name: 'Vehicles', + code: 'code001', + description: faker.lorem.sentence(), + order: '0', + }); + + for (let i = 0; i < 10; i++) { + await Products.createProduct({ + name: faker.random.word(), + categoryId: productCategory._id, + unitPrice: faker.random.number({ min: 100000, max: 1000000 }), + type: 'product', + description: faker.lorem.sentence(), + sku: faker.random.number(), + code: faker.random.number(), + }); + } + + const dealStages = await populateStages('deal'); + for (let i = 0; i < 3; i++) { + const board = await Boards.createBoard({ name: faker.random.word(), type: 'deal', userId: admin._id }); + + for (let j = 0; j < 2; j++) { + await Pipelines.createPipeline({ name: faker.random.word(), type: 'deal', boardId: board._id }, dealStages); + } + } + + console.log('Finished: Sales & PipeLines'); + + // Conversation + + console.log('Creating: Conversations'); + + for (let i = 0; i < 5; i++) { + const randomCustomer = await Customers.aggregate([{ $sample: { size: 1 } }]); + + if (randomCustomer[0]) { + memoryStorage().set(`customer_last_status_${randomCustomer[0]._id}`, 'left'); + await widgetMutations.widgetsInsertMessage( + {}, + { + contentType: MESSAGE_TYPES.TEXT, + integrationId: integration._id, + customerId: randomCustomer[0]._id || '', + message: faker.lorem.sentence(), + }, + ); + } + } + + console.log('Finished: Conversations'); + + // Ticket + + console.log('Creating: Tickets'); + + const randomConversation = await Conversations.findOne(); + const ticketBoard = await Boards.createBoard({ name: faker.random.word(), type: 'ticket', userId: admin._id }); + const ticketStages = await populateStages('ticket'); + + for (let j = 0; j < 2; j++) { + await Pipelines.createPipeline( + { name: faker.random.word(), type: 'ticket', boardId: ticketBoard._id }, + ticketStages, + ); + } + + const selectedTicketStage = await Stages.findOne({ type: 'ticket' }); + + await Tickets.createTicket({ + name: faker.random.word(), + userId: admin._id, + initialStageId: selectedTicketStage?._id, + sourceConversationId: randomConversation?._id, + stageId: selectedTicketStage?._id || '', + }); + + console.log('Finished: Tickets'); + + // Task + + console.log('Created: Tasks'); + + const randomUser = await Users.aggregate([{ $sample: { size: 1 } }]); + const taskBoard = await Boards.createBoard({ name: faker.random.word(), type: 'task', userId: admin._id }); + const taskStages = await populateStages('task'); + + for (let j = 0; j < 2; j++) { + await Pipelines.createPipeline({ name: faker.random.word(), type: 'task', boardId: taskBoard._id }, taskStages); + } + + const selectedTaskStage = await Stages.findOne({ type: 'task' }); + + await Tasks.createTask({ + name: faker.random.word(), + userId: admin._id, + initialStageId: selectedTaskStage?._id, + assignedUserIds: [randomUser[0]._id || admin._id], + stageId: selectedTicketStage?._id || '', + }); + + console.log('Finished: Tasks'); + + // Segment + console.log('Creating: Segments'); + + const template = await EmailTemplates.create({ + name: faker.random.word(), + content: `

${faker.lorem.sentences()}

\n`, + }); + + const segment = await Segments.createSegment({ + name: 'Happy customers', + description: faker.lorem.sentence(), + contentType: 'customer', + color: faker.internet.color(), + subOf: '', + conditions: [], + }); + + const docAutoMessage = { + kind: 'visitorAuto', + title: 'Visitor auto message', + fromUserId: randomUser[0]._id, + segmentIds: [segment._id], + brandIds: [brand._id], + tagIds: [], + isLive: false, + isDraft: true, + }; + + const docAutoEmail = { + kind: 'auto', + title: 'Auto email every friday', + fromUserId: randomUser[0]._id, + segmentIds: [segment._id], + brandIds: [brand._id], + tagIds: [], + isLive: false, + isDraft: true, + email: { + subject: faker.lorem.sentence(), + sender: faker.internet.email(), + replyTo: faker.internet.email(), + content: faker.lorem.paragraphs(), + attachments: [], + templateId: template._id, + }, + scheduleDate: { + type: '5', + month: '', + day: '', + }, + method: 'email', + }; + + await EngageMessages.createEngageMessage(docAutoMessage); + await EngageMessages.createEngageMessage(docAutoEmail); + + console.log('Finished: Engages'); + + // Growth Hack + + console.log('Creating: Growth Hack'); + + const growthForm = await createForms('growthHack', admin); + const growthBoard = await Boards.createBoard({ name: faker.random.word(), type: 'growthHack', userId: admin._id }); + + const pipelineTemplate = await PipelineTemplates.createPipelineTemplate( + { + name: faker.random.word(), + description: faker.lorem.sentence(), + type: 'growthHack', + }, + [ + { + _id: faker.unique, + name: faker.random.word(), + formId: growthForm._id, + }, + ], + ); + + const growthHackDock = { + name: faker.random.word(), + startDate: faker.date.past(), + endDate: faker.date.future(), + visibility: 'public', + type: 'growthHack', + boardId: growthBoard._id, + memberIds: [], + bgColor: faker.internet.color(), + templateId: pipelineTemplate._id, + hackScoringType: 'rice', + metric: 'monthly-active-users', + }; + await Pipelines.createPipeline(growthHackDock); + + console.log('Finished: Growth Hack'); + + await disconnect(); + + console.log('admin email: admin@erxes.io'); + console.log('admin password: ', newPwd); + + process.exit(); +}; + +const createXlsStream = async (fileName: string): Promise<{ fieldNames: string[]; datas: any[] }> => { + return new Promise(async (resolve, reject) => { + let rowCount = 0; + + const usedSheets: any[] = []; + + const xlsxReader = XlsxStreamReader(); + + try { + const stream = fs.createReadStream(`./src/initialData/xls/${fileName}`); + + stream.pipe(xlsxReader); + + xlsxReader.on('worksheet', workSheetReader => { + if (workSheetReader > 1) { + return workSheetReader.skip(); + } + + workSheetReader.on('row', row => { + if (rowCount > 100000) { + return reject(new Error('You can only import 100000 rows one at a time')); + } + + if (row.values.length > 0) { + usedSheets.push(row.values); + rowCount++; + } + }); + + workSheetReader.process(); + }); + + xlsxReader.on('end', () => { + const compactedRows: any = []; + + for (const row of usedSheets) { + if (row.length > 0) { + row.shift(); + + compactedRows.push(row); + } + } + + const fieldNames = usedSheets[0]; + + // Removing column + compactedRows.shift(); + + return resolve({ fieldNames, datas: compactedRows }); + }); + + xlsxReader.on('error', error => { + return reject(error); + }); + } catch (e) { + reject(e); + } + }); +}; + +const readXlsFile = async (fileName: string, type: string, user: any) => { + try { + let fieldNames: string[] = []; + let datas: any[] = []; + let result: any = {}; + + result = await createXlsStream(fileName); + + fieldNames = result.fieldNames; + datas = result.datas; + + if (datas.length === 0) { + throw new Error('Please import at least one row of data'); + } + + const properties = await checkFieldNames(type, fieldNames); + + const importHistoryId = await ImportHistory.create({ + contentType: type, + total: datas.length, + userId: user._id, + date: Date.now(), + }); + + return { properties, result: datas, type, importHistoryId }; + } catch (e) { + debugWorkers(e.message); + throw e; + } +}; + +const insertToDB = async xlsData => { + const { user, scopeBrandIds, result, contentType, properties, importHistoryId } = xlsData; + let create: any = null; + let model: any = null; + + const isBoardItem = (): boolean => contentType === 'deal' || contentType === 'task' || contentType === 'ticket'; + + switch (contentType) { + case 'company': + create = Companies.createCompany; + model = Companies; + break; + case 'customer': + create = Customers.createCustomer; + model = Customers; + break; + case 'lead': + create = Customers.createCustomer; + model = Customers; + break; + case 'product': + create = Products.createProduct; + model = Products; + break; + case 'deal': + create = Deals.createDeal; + break; + case 'task': + create = Tasks.createTask; + break; + case 'ticket': + create = Tickets.createTicket; + break; + default: + break; + } + + for (const fieldValue of result) { + // Import history result statistics + const inc: { success: number; failed: number; percentage: number } = { + success: 0, + failed: 0, + percentage: 100, + }; + + // Collecting errors + const errorMsgs: string[] = []; + + const doc: any = { + scopeBrandIds, + customFieldsData: [], + }; + + let colIndex: number = 0; + let boardName: string = ''; + let pipelineName: string = ''; + let stageName: string = ''; + + // Iterating through detailed properties + for (const property of properties) { + const value = (fieldValue[colIndex] || '').toString(); + + switch (property.type) { + case 'customProperty': + { + doc.customFieldsData.push({ + field: property.id, + value: fieldValue[colIndex], + }); + } + break; + + case 'customData': + { + doc[property.name] = value; + } + break; + + case 'ownerEmail': + { + const userEmail = value; + + const owner = await Users.findOne({ email: userEmail }).lean(); + + doc[property.name] = owner ? owner._id : ''; + } + break; + + case 'pronoun': + { + doc.sex = generatePronoun(value); + } + break; + + case 'companiesPrimaryNames': + { + doc.companiesPrimaryNames = value.split(','); + } + break; + + case 'customersPrimaryEmails': + doc.customersPrimaryEmails = value.split(','); + break; + + case 'state': + doc.state = value; + break; + + case 'boardName': + boardName = value; + break; + + case 'pipelineName': + pipelineName = value; + break; + + case 'stageName': + stageName = value; + break; + + case 'tag': + { + const tagName = value; + + const tag = await Tags.findOne({ name: new RegExp(`.*${tagName}.*`, 'i') }).lean(); + + doc[property.name] = tag ? [tag._id] : []; + } + break; + + case 'basic': + { + doc[property.name] = value; + + if (property.name === 'primaryName' && value) { + doc.names = [value]; + } + + if (property.name === 'primaryEmail' && value) { + doc.emails = [value]; + } + + if (property.name === 'primaryPhone' && value) { + doc.phones = [value]; + } + + if (property.name === 'phones' && value) { + doc.phones = value.split(','); + } + + if (property.name === 'emails' && value) { + doc.emails = value.split(','); + } + + if (property.name === 'names' && value) { + doc.names = value.split(','); + } + + if (property.name === 'isComplete') { + doc.isComplete = Boolean(value); + } + } + break; + } // end property.type switch + + colIndex++; + } // end properties for loop + + if ((contentType === 'customer' || contentType === 'lead') && !doc.emailValidationStatus) { + doc.emailValidationStatus = 'unknown'; + } + + if ((contentType === 'customer' || contentType === 'lead') && !doc.phoneValidationStatus) { + doc.phoneValidationStatus = 'unknown'; + } + + // set board item created user + if (isBoardItem()) { + doc.userId = user._id; + + if (boardName && pipelineName && stageName) { + const board = await Boards.findOne({ name: boardName, type: contentType }); + const pipeline = await Pipelines.findOne({ boardId: board && board._id, name: pipelineName }); + const stage = await Stages.findOne({ pipelineId: pipeline && pipeline._id, name: stageName }); + + doc.stageId = stage && stage._id; + } + } + + await create(doc, user) + .then(async cocObj => { + if (doc.companiesPrimaryNames && doc.companiesPrimaryNames.length > 0 && contentType !== 'company') { + const companyIds: string[] = []; + + for (const primaryName of doc.companiesPrimaryNames) { + let company = await Companies.findOne({ primaryName }).lean(); + + if (company) { + companyIds.push(company._id); + } else { + company = await Companies.createCompany({ primaryName }); + companyIds.push(company._id); + } + } + + for (const _id of companyIds) { + await Conformities.addConformity({ + mainType: contentType === 'lead' ? 'customer' : contentType, + mainTypeId: cocObj._id, + relType: 'company', + relTypeId: _id, + }); + } + } + + if (doc.customersPrimaryEmails && doc.customersPrimaryEmails.length > 0 && contentType !== 'customer') { + const customers = await Customers.find({ primaryEmail: { $in: doc.customersPrimaryEmails } }, { _id: 1 }); + const customerIds = customers.map(customer => customer._id); + + for (const _id of customerIds) { + await Conformities.addConformity({ + mainType: contentType === 'lead' ? 'customer' : contentType, + mainTypeId: cocObj._id, + relType: 'customer', + relTypeId: _id, + }); + } + } + + await ImportHistory.updateOne({ _id: importHistoryId }, { $push: { ids: [cocObj._id] } }); + + // Increasing success count + inc.success++; + }) + .catch(async (e: Error) => { + const updatedDoc = clearEmptyValues(doc); + + // Increasing failed count and pushing into error message + + switch (e.message) { + case 'Duplicated email': + inc.success++; + await updateDuplicatedValue(model, 'primaryEmail', updatedDoc); + break; + case 'Duplicated phone': + inc.success++; + await updateDuplicatedValue(model, 'primaryPhone', updatedDoc); + break; + case 'Duplicated name': + inc.success++; + await updateDuplicatedValue(model, 'primaryName', updatedDoc); + break; + default: + inc.failed++; + errorMsgs.push(e.message); + break; + } + }); + + await ImportHistory.updateOne({ _id: importHistoryId }, { $inc: inc, $push: { errorMsgs } }); + + let importHistory = await ImportHistory.findOne({ _id: importHistoryId }); + + if (!importHistory) { + throw new Error('Could not find import history'); + } + + if (importHistory.failed + importHistory.success === importHistory.total) { + await ImportHistory.updateOne({ _id: importHistoryId }, { $set: { status: 'Done', percentage: 100 } }); + + importHistory = await ImportHistory.findOne({ _id: importHistoryId }); + } + + if (!importHistory) { + throw new Error('Could not find import history'); + } + } +}; + +const populateStages = async type => { + const stages: IPipelineStage[] = []; + + for (let i = 0; i < 5; i++) { + const stage: IPipelineStage = { _id: faker.unique, name: faker.random.word(), type, pipelineId: '' }; + stages.push(stage); + } + return stages; +}; + +const createForms = async (type: string, user: any) => { + const form = await Forms.createForm( + { + title: faker.random.word(), + description: faker.lorem.sentence(), + buttonText: faker.random.word(), + type, + }, + user._id, + ); + + const validations = ['datetime', 'date', 'email', 'number', 'phone']; + + let order = 0; + + for (const validation of validations) { + let text = faker.random.word(); + + if (validation === 'email') { + text = 'email'; + } else if (validation === 'phone') { + text = 'phone number'; + } + + await Fields.createField({ + contentTypeId: form._id, + contentType: 'form', + type: 'input', + validation, + text, + description: faker.random.word(), + order, + }); + order++; + } + + return form; +}; + +main(); diff --git a/src/commands/runEsCommand.ts b/src/commands/runEsCommand.ts new file mode 100644 index 000000000..056ed2591 --- /dev/null +++ b/src/commands/runEsCommand.ts @@ -0,0 +1,33 @@ +import { client, getMappings } from '../elasticsearch'; + +const argv = process.argv; + +/* + * yarn run runEsCommand deleteByQuery '{"index":"erxes_office__events","body":{"query":{"match":{"customerId":"CX2BFBGDEHFehNT8y"}}}}' + */ +const main = async () => { + if (argv.length === 4) { + const body = argv.pop() || '{}'; + const action = argv.pop(); + + try { + if (action === 'getMapping') { + const mappingResponse = await getMappings(JSON.parse(body).index); + return console.log(JSON.stringify(mappingResponse)); + } + + const response = await client[action](JSON.parse(body)); + console.log(JSON.stringify(response)); + } catch (e) { + console.log(e); + } + } +}; + +main() + .then(() => { + console.log('done ...'); + }) + .catch(e => { + console.log(e.message); + }); diff --git a/src/commands/trackTelemetry.ts b/src/commands/trackTelemetry.ts new file mode 100644 index 000000000..3265d8295 --- /dev/null +++ b/src/commands/trackTelemetry.ts @@ -0,0 +1,12 @@ +import * as telemetry from 'erxes-telemetry'; + +const command = async () => { + const argv = process.argv; + const message = argv.pop(); + + telemetry.trackCli('installation_status', { message }); + + process.exit(); +}; + +command(); diff --git a/src/cronJobs/activityLogs.ts b/src/cronJobs/activityLogs.ts index 87f98ae34..0c7576b50 100644 --- a/src/cronJobs/activityLogs.ts +++ b/src/cronJobs/activityLogs.ts @@ -1,20 +1,30 @@ +import * as dotenv from 'dotenv'; import * as schedule from 'node-schedule'; -import QueryBuilder from '../data/modules/segments/queryBuilder'; -import { ActivityLogs, Customers, Segments } from '../db/models'; +import { fetchBySegments } from '../data/modules/segments/queryBuilder'; +import { connect } from '../db/connection'; +import { ActivityLogs, Companies, Customers, Segments } from '../db/models'; /** * Send conversation messages to customer */ +dotenv.config(); + export const createActivityLogsFromSegments = async () => { + await connect(); const segments = await Segments.find({}); for (const segment of segments) { - const selector = await QueryBuilder.segments(segment); - const customers = await Customers.find(selector); + const ids = await fetchBySegments(segment); + + const customers = await Customers.find({ _id: { $in: ids } }, { _id: 1 }); + const customerIds = customers.map(c => c._id); + + const companies = await Companies.find({ _id: { $in: ids } }, { _id: 1 }); + const companyIds = companies.map(c => c._id); + + await ActivityLogs.createSegmentLog(segment, customerIds, 'customer'); - for (const customer of customers) { - await ActivityLogs.createSegmentLog(segment, customer); - } + await ActivityLogs.createSegmentLog(segment, companyIds, 'company'); } }; diff --git a/src/cronJobs/conversations.ts b/src/cronJobs/conversations.ts index 1fe446af2..887523ecb 100644 --- a/src/cronJobs/conversations.ts +++ b/src/cronJobs/conversations.ts @@ -1,9 +1,10 @@ import * as moment from 'moment'; import * as schedule from 'node-schedule'; import * as _ from 'underscore'; -import utils from '../data/utils'; +import utils, { IEmailParams } from '../data/utils'; import { Brands, ConversationMessages, Conversations, Customers, Integrations, Users } from '../db/models'; import { IMessageDocument } from '../db/models/definitions/conversationMessages'; +import { debugCrons } from '../debuggers'; /** * Send conversation messages to customer @@ -12,84 +13,116 @@ export const sendMessageEmail = async () => { // new or open conversations const conversations = await Conversations.newOrOpenConversation(); + debugCrons(`Found ${conversations.length} conversations`); + for (const conversation of conversations) { - const customer = await Customers.findOne({ _id: conversation.customerId }); + const customer = await Customers.findOne({ _id: conversation.customerId }).lean(); + const integration = await Integrations.findOne({ _id: conversation.integrationId, }); if (!integration) { - return; + continue; } - const brand = await Brands.findOne({ _id: integration.brandId }); - - if (!customer || !customer.primaryEmail) { - return; + if (!customer || !(customer.emails && customer.emails.length > 0)) { + continue; } + const brand = await Brands.findOne({ _id: integration.brandId }).lean(); + if (!brand) { - return; + continue; } // user's last non answered question - const question: IMessageDocument | any = (await ConversationMessages.getNonAsnweredMessage(conversation._id)) || {}; + const question: IMessageDocument = await ConversationMessages.getNonAsnweredMessage(conversation._id); + + const adminMessages = await ConversationMessages.getAdminMessages(conversation._id); + + if (adminMessages.length < 1) { + continue; + } // generate admin unread answers const answers: any = []; - const adminMessages = await ConversationMessages.getAdminMessages(conversation._id); - for (const message of adminMessages) { - const answer = message; + const answer = { + ...message.toJSON(), + createdAt: new Date(moment(message.createdAt).format('DD MMM YY, HH:mm')), + }; + + const usr = await Users.findOne({ _id: message.userId }).lean(); + + if (usr) { + answer.user = usr; + answer.user.avatar = usr.details.avatar; + answer.user.fullName = usr.details.fullName; + } + + if (message.attachments.length !== 0) { + for (const attachment of message.attachments) { + answer.content = answer.content.concat(`

${attachment.name}

`); + } + } // add user object to answer - answers.push({ - ...answer.toJSON(), - user: await Users.findOne({ _id: message.userId }), - createdAt: new Date(moment(answer.createdAt).format('DD MMM YY, HH:mm')), - }); + answers.push(answer); } - if (answers.length < 1) { - return; - } + customer.name = Customers.getCustomerName(customer); - // template data - const data: any = { + const data = { customer, - question: { - ...question.toJSON(), - createdAt: new Date(moment(question.createdAt).format('DD MMM YY, HH:mm')), - }, + question: {}, answers, brand, }; - // add user's signature - const user = await Users.findOne({ _id: answers[0].userId }); - - if (user && user.emailSignatures) { - const signature = await _.find(user.emailSignatures, s => brand._id === s.brandId); - - if (signature) { - data.signature = signature.signature; + if (question) { + const questionData = { + ...question.toJSON(), + createdAt: new Date(moment(question.createdAt).format('DD MMM YY, HH:mm')), + }; + + if (question.attachments.length !== 0) { + for (const attachment of question.attachments) { + questionData.content = questionData.content.concat( + `

${attachment.name}

`, + ); + } } + + data.question = questionData; } - // send email - utils.sendEmail({ - toEmails: [customer.primaryEmail], + const email = customer.primaryEmail || customer.emails[0]; + + const emailOptions: IEmailParams = { + toEmails: [email], title: `Reply from "${brand.name}"`, - template: { + }; + + const emailConfig = brand.emailConfig; + + if (emailConfig && emailConfig.type === 'custom') { + emailOptions.customHtml = emailConfig.template; + emailOptions.customHtmlData = data; + } else { + emailOptions.template = { name: 'conversationCron', - isCustom: true, data, - }, - }); + }; + } + + // send email + + await utils.sendEmail(emailOptions); // mark sent messages as read - ConversationMessages.markSentAsReadMessages(conversation._id); + await ConversationMessages.markSentAsReadMessages(conversation._id); } }; @@ -109,6 +142,8 @@ export default { * └───────────────────────── second (0 - 59, OPTIONAL) */ // every 10 minutes -schedule.scheduleJob('*/10 * * * *', () => { - sendMessageEmail(); +schedule.scheduleJob('*/10 * * * *', async () => { + debugCrons('Ran conversation crons'); + + await sendMessageEmail(); }); diff --git a/src/cronJobs/deals.ts b/src/cronJobs/deals.ts index 9a9d33f00..fae46a15a 100644 --- a/src/cronJobs/deals.ts +++ b/src/cronJobs/deals.ts @@ -1,38 +1,61 @@ import * as moment from 'moment'; import * as schedule from 'node-schedule'; import utils from '../data/utils'; -import { Deals, Pipelines, Stages } from '../db/models'; -import { NOTIFICATION_TYPES } from '../db/models/definitions/constants'; +import { Deals, Pipelines, Stages, Tasks, Tickets, Users } from '../db/models'; /** - * Send notification Deals dueDate + * Send notification Deals, Tasks and Tickets dueDate */ export const sendNotifications = async () => { const now = new Date(); + const collections = { + deal: Deals, + task: Tasks, + ticket: Tickets, + all: ['deal', 'task', 'ticket'], + }; - const deals = await Deals.find({ - closeDate: { - $gte: now, - $lte: moment(now) - .add(24, 'hour') - .toDate(), - }, - }); - - for (const deal of deals) { - const stage = await Stages.getStage(deal.stageId || ''); - const pipeline = await Pipelines.getPipeline(stage.pipelineId || ''); - - const content = `Reminder: '${deal.name}' deal is due in upcoming`; - - utils.sendNotification({ - notifType: NOTIFICATION_TYPES.DEAL_DUE_DATE, - title: content, - content, - link: `/deal/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}`, - // exclude current user - receivers: deal.assignedUserIds || [], + for (const type of collections.all) { + const objects = await collections[type].find({ + closeDate: { + $gte: now, + $lte: moment() + .add(2, 'days') + .toDate(), + }, }); + + for (const object of objects) { + const stage = await Stages.getStage(object.stageId || ''); + const pipeline = await Pipelines.getPipeline(stage.pipelineId || ''); + + const user = await Users.findOne({ _id: object.modifiedBy }); + + if (!user) { + return; + } + + const diffMinute = Math.floor((object.closeDate.getTime() - now.getTime()) / 60000); + + if (Math.abs(diffMinute - (object.reminderMinute || 0)) < 5) { + const content = `${object.name} ${type} is due in upcoming`; + + const url = type === 'ticket' ? `/inbox/${type}/board` : `${type}/board`; + + utils.sendNotification({ + notifType: `${type}DueDate`, + title: content, + content, + action: `Reminder:`, + link: `${url}?id=${pipeline.boardId}&pipelineId=${pipeline._id}&itemId=${object._id}`, + createdUser: user, + // exclude current user + contentType: type, + contentTypeId: object._id, + receivers: object.assignedUserIds || [], + }); + } + } } }; @@ -51,7 +74,8 @@ export default { * │ └──────────────────── minute (0 - 59) * └───────────────────────── second (0 - 59, OPTIONAL) */ -// every day in 23:45:00 -schedule.scheduleJob('0 45 23 * * *', () => { + +// every 5 minutes +schedule.scheduleJob('*/5 * * * *', () => { sendNotifications(); }); diff --git a/src/cronJobs/engages.ts b/src/cronJobs/engages.ts index a9dbcca4a..2cd3b9e13 100644 --- a/src/cronJobs/engages.ts +++ b/src/cronJobs/engages.ts @@ -1,118 +1,127 @@ -import * as moment from 'moment'; import * as schedule from 'node-schedule'; import { send } from '../data/resolvers/mutations/engageUtils'; import { EngageMessages } from '../db/models'; -import { IEngageMessageDocument, IScheduleDate } from '../db/models/definitions/engages'; - -interface IEngageSchedules { - id: string; - job: any; -} - -// Track runtime cron job instances -export const ENGAGE_SCHEDULES: IEngageSchedules[] = []; - -/** - * Update or Remove selected engage message - * @param _id - Engage id - * @param update - Action type - */ -export const updateOrRemoveSchedule = async ({ _id }: { _id: string }, update?: boolean) => { - const selectedIndex = ENGAGE_SCHEDULES.findIndex(engage => engage.id === _id); - - if (selectedIndex === -1) { - return; - } +import { debugCrons } from '../debuggers'; - // Remove selected job instance and update tracker - ENGAGE_SCHEDULES[selectedIndex].job.cancel(); - ENGAGE_SCHEDULES.splice(selectedIndex, 1); +const findMessages = (selector = {}) => { + return EngageMessages.find({ + kind: { $in: ['auto', 'visitorAuto'] }, + isLive: true, + ...selector, + }); +}; - if (!update) { - return; +const runJobs = async messages => { + for (const message of messages) { + await send(message); } +}; - const message = await EngageMessages.findOne({ _id }); +const checkEveryMinuteJobs = async () => { + const messages = await findMessages({ 'scheduleDate.type': 'minute' }); + await runJobs(messages); +}; - if (!message) { - return; - } +const checkHourMinuteJobs = async () => { + debugCrons('Checking every hour jobs ....'); - return createSchedule(message); + const messages = await findMessages({ 'scheduleDate.type': 'hour' }); + + debugCrons(`Found every hour messages ${messages.length}`); + + await runJobs(messages); }; -/** - * Create cron job for an engage message - */ -export const createSchedule = (message: IEngageMessageDocument) => { - const { scheduleDate } = message; +const checkDayJobs = async () => { + debugCrons('Checking every day jobs ....'); - if (scheduleDate) { - const rule = createScheduleRule(scheduleDate); + // every day messages =========== + const everyDayMessages = await findMessages({ 'scheduleDate.type': 'day' }); + await runJobs(everyDayMessages); - const job = schedule.scheduleJob(rule, () => { - send(message); - }); + debugCrons(`Found every day messages ${everyDayMessages.length}`); - // Collect cron job instances - ENGAGE_SCHEDULES.push({ id: message._id, job }); - } -}; + const now = new Date(); + const day = now.getDate(); + const month = now.getMonth() + 1; + const year = now.getFullYear(); -/** - * Create cron job schedule rule - */ -export const createScheduleRule = (scheduleDate: IScheduleDate) => { - if (!scheduleDate || (!scheduleDate.type && !scheduleDate.time)) { - return '0 45 23 * * *'; - } + // every nth day messages ======= + const everyNthDayMessages = await findMessages({ 'scheduleDate.type': day.toString() }); + await runJobs(everyNthDayMessages); - if (!scheduleDate.time) { - return '0 45 23 * * *'; - } + debugCrons(`Found every nth day messages ${everyNthDayMessages.length}`); - const time = moment(new Date(scheduleDate.time)); + // every month messages ======== + let everyMonthMessages = await findMessages({ 'scheduleDate.type': 'month' }); - const hour = time.hour() || '*'; - const minute = time.minute() || '0'; - const month = scheduleDate.month || '*'; + everyMonthMessages = everyMonthMessages.filter(message => { + const { lastRunAt, scheduleDate } = message; - let dayOfWeek = '*'; - let day: string | number = '*'; + if (!lastRunAt) { + return true; + } - // Schedule type day of week [0-6] - if (scheduleDate.type && scheduleDate.type.length === 1) { - dayOfWeek = scheduleDate.type || '*'; - } + // ignore if last run month is this month + if (lastRunAt.getMonth() === month) { + return false; + } - if (scheduleDate.type === 'month' || scheduleDate.type === 'year') { - day = scheduleDate.day || '*'; - } + return scheduleDate && scheduleDate.day === day.toString(); + }); - /* - * * * * * * - ┬ ┬ ┬ ┬ ┬ ┬ - │ │ │ │ │ │ - │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) - │ │ │ │ └───── month (1 - 12) - │ │ │ └────────── day of month (1 - 31) - │ │ └─────────────── hour (0 - 23) - │ └──────────────────── minute (0 - 59) - └───────────────────────── second (0 - 59, OPTIONAL) - */ - - return `${minute} ${hour} ${day} ${month} ${dayOfWeek}`; -}; + debugCrons(`Found every month messages ${everyMonthMessages.length}`); -const initCronJob = async () => { - const messages = await EngageMessages.find({ - kind: { $in: ['auto', 'visitorAuto'] }, - isLive: true, + await runJobs(everyMonthMessages); + + await EngageMessages.updateMany( + { _id: { $in: everyMonthMessages.map(m => m._id) } }, + { $set: { lastRunAt: new Date() } }, + ); + + // every year messages ======== + let everyYearMessages = await findMessages({ 'scheduleDate.type': 'year' }); + + everyYearMessages = everyYearMessages.filter(message => { + const { lastRunAt, scheduleDate } = message; + + if (!lastRunAt) { + return true; + } + + // ignore if last run year is this year + if (lastRunAt.getFullYear() === year) { + return false; + } + + if (scheduleDate && scheduleDate.month !== month.toString()) { + return false; + } + + return scheduleDate && scheduleDate.day === day.toString(); }); - for (const message of messages) { - createSchedule(message); - } + debugCrons(`Found every year messages ${everyYearMessages.length}`); + + await runJobs(everyYearMessages); + + await EngageMessages.updateMany( + { _id: { $in: everyYearMessages.map(m => m._id) } }, + { $set: { lastRunAt: new Date() } }, + ); }; -initCronJob(); +// every minute at 1sec +schedule.scheduleJob('1 * * * * *', async () => { + await checkEveryMinuteJobs(); +}); + +// every hour at 10min:10sec +schedule.scheduleJob('10 10 * * * *', async () => { + await checkHourMinuteJobs(); +}); + +// every day at 11hour:20min:20sec +schedule.scheduleJob('20 20 11 * * *', async () => { + checkDayJobs(); +}); diff --git a/src/cronJobs/index.ts b/src/cronJobs/index.ts index a0d6cef89..a5ca0e895 100644 --- a/src/cronJobs/index.ts +++ b/src/cronJobs/index.ts @@ -1,72 +1,38 @@ -import * as bodyParser from 'body-parser'; import * as dotenv from 'dotenv'; import * as express from 'express'; import { connect } from '../db/connection'; -import { debugCrons, debugRequest, debugResponse } from '../debuggers'; +import { debugCrons } from '../debuggers'; +import { initMemoryStorage } from '../inmemoryStorage'; +import { initBroker } from '../messageBroker'; import './activityLogs'; import './conversations'; import './deals'; -import { createSchedule, updateOrRemoveSchedule } from './engages'; +import './engages'; +import './integrations'; +import './robot'; // load environment variables dotenv.config(); -// connect to mongo database -connect(); - const app = express(); // for health check -app.get('/status', async (_req, res) => { +app.get('/health', async (_req, res) => { res.end('ok'); }); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); - -app.post('/create-schedule', async (req, res, next) => { - debugRequest(debugCrons, req); - - const { message } = req.body; - - try { - await createSchedule(JSON.parse(message)); - } catch (e) { - debugCrons(`Error while proccessing createSchedule ${e.message}`); - return next(e); - } - - debugResponse(debugCrons, req); - - return res.json({ status: 'ok ' }); -}); - -app.post('/update-or-remove-schedule', async (req, res, next) => { - debugRequest(debugCrons, req); +const { PORT_CRONS = 3600 } = process.env; - const { _id, update } = req.body; - - try { - await updateOrRemoveSchedule(_id, update); - } catch (e) { - debugCrons(`Error while proccessing createSchedule ${e.message}`); - return next(e); - } - - debugResponse(debugCrons, req); - - return res.json({ status: 'ok ' }); -}); - -// Error handling middleware -app.use((error, _req, res, _next) => { - console.error(error.stack); - res.status(500).send(error.message); -}); +app.listen(PORT_CRONS, () => { + // connect to mongo database + connect().then(async () => { + initMemoryStorage(); -const { PORT_CRONS } = process.env; + initBroker(app).catch(e => { + debugCrons(`Error ocurred during broker init ${e.message}`); + }); + }); -app.listen(PORT_CRONS, () => { debugCrons(`Cron Server is now running on ${PORT_CRONS}`); }); diff --git a/src/cronJobs/integrations.ts b/src/cronJobs/integrations.ts new file mode 100644 index 000000000..a4834951c --- /dev/null +++ b/src/cronJobs/integrations.ts @@ -0,0 +1,17 @@ +import * as schedule from 'node-schedule'; +import messageBroker from '../messageBroker'; + +/** + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * │ │ │ │ │ | + * │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) + * │ │ │ │ └───── month (1 - 12) + * │ │ │ └────────── day of month (1 - 31) + * │ │ └─────────────── hour (0 - 23) + * │ └──────────────────── minute (0 - 59) + * └───────────────────────── second (0 - 59, OPTIONAL) + */ +schedule.scheduleJob('0 0 * * *', () => { + return messageBroker().sendMessage('erxes-api:integrations-notification', { type: 'cronjob' }); +}); diff --git a/src/cronJobs/robot.ts b/src/cronJobs/robot.ts new file mode 100644 index 000000000..c37aa6cd6 --- /dev/null +++ b/src/cronJobs/robot.ts @@ -0,0 +1,44 @@ +import * as schedule from 'node-schedule'; +import { Users } from '../db/models'; +import { OnboardingHistories } from '../db/models/Robot'; +import { debugCrons } from '../debuggers'; +import messageBroker from '../messageBroker'; + +const checkOnboarding = async () => { + const users = await Users.find({}).lean(); + + for (const user of users) { + const status = await OnboardingHistories.userStatus(user._id); + + if (status === 'completed') { + continue; + } + + messageBroker().sendMessage('callPublish', { + name: 'onboardingChanged', + data: { + onboardingChanged: { + userId: user._id, + type: status, + }, + }, + }); + } +}; + +/** + * * * * * * * + * ┬ ┬ ┬ ┬ ┬ ┬ + * │ │ │ │ │ | + * │ │ │ │ │ └ day of week (0 - 7) (0 or 7 is Sun) + * │ │ │ │ └───── month (1 - 12) + * │ │ │ └────────── day of month (1 - 31) + * │ │ └─────────────── hour (0 - 23) + * │ └──────────────────── minute (0 - 59) + * └───────────────────────── second (0 - 59, OPTIONAL) + */ +schedule.scheduleJob('0 45 23 * * *', () => { + debugCrons('Checked onboarding'); + + checkOnboarding(); +}); diff --git a/src/data/constants.ts b/src/data/constants.ts index 030330bc8..a95487817 100644 --- a/src/data/constants.ts +++ b/src/data/constants.ts @@ -1,13 +1,6 @@ export const EMAIL_CONTENT_CLASS = 'erxes-email-content'; export const EMAIL_CONTENT_PLACEHOLDER = `
`; -export const INTEGRATION_KIND_CHOICES = { - MESSENGER: 'messenger', - FORM: 'form', - FACEBOOK: 'facebook', - ALL: ['messenger', 'form', 'facebook'], -}; - export const MESSAGE_KINDS = { AUTO: 'auto', VISITOR_AUTO: 'visitorAuto', @@ -32,8 +25,9 @@ export const FORM_FIELDS = { BLANK: '', NUMBER: 'number', DATE: 'date', + DATETIME: 'datetime', EMAIL: 'email', - ALL: ['', 'number', 'date', 'email'], + ALL: ['', 'number', 'date', 'datetime', 'email'], }, }; @@ -41,7 +35,17 @@ export const FIELD_CONTENT_TYPES = { FORM: 'form', CUSTOMER: 'customer', COMPANY: 'company', - ALL: ['form', 'customer', 'company'], + PRODUCT: 'product', + ALL: ['form', 'customer', 'company', 'product'], +}; + +export const EXTEND_FIELDS = { + CUSTOMER: [ + { name: 'tag', label: 'Tag' }, + { name: 'ownerEmail', label: 'Owner' }, + { name: 'companiesPrimaryNames', label: 'Companies' }, + ], + PRODUCT: [{ name: 'categoryCode', label: 'Category Code' }], }; export const COC_LEAD_STATUS_TYPES = [ @@ -71,39 +75,10 @@ export const COC_LIFECYCLE_STATE_TYPES = [ export const FIELDS_GROUPS_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', - ALL: ['customer', 'company'], + PRODUCT: 'product', + ALL: ['customer', 'company', 'product'], }; -export const CUSTOMER_BASIC_INFOS = [ - 'firstName', - 'lastName', - 'primaryEmail', - 'primaryPhone', - 'ownerId', - 'position', - 'department', - 'leadStatus', - 'lifecycleState', - 'hasAuthority', - 'description', - 'doNotDisturb', -]; - -export const COMPANY_BASIC_INFOS = [ - 'primaryName', - 'size', - 'industry', - 'website', - 'plan', - 'primaryEmail', - 'primaryPhone', - 'leadStatus', - 'lifecycleState', - 'businessType', - 'description', - 'doNotDisturb', -]; - export const INSIGHT_BASIC_INFOS = { count: 'Customer count', messageCount: 'Conversation message count', @@ -253,4 +228,83 @@ export const NOTIFICATION_MODULES = [ }, ], }, + { + name: 'customers', + description: 'Customers', + types: [ + { + name: 'customerMention', + text: 'Mention on customer note', + }, + ], + }, + { + name: 'companies', + description: 'Companies', + types: [ + { + name: 'companyMention', + text: 'Mention on company note', + }, + ], + }, ]; + +export const MODULE_NAMES = { + BOARD: 'board', + BOARD_DEAL: 'dealBoards', + BOARD_TASK: 'taskBoards', + BOARD_TICKET: 'ticketBoards', + BOARD_GH: 'growthHackBoards', + PIPELINE_DEAL: 'dealPipelines', + PIPELINE_TASK: 'taskPipelines', + PIPELINE_TICKET: 'ticketPipelines', + PIPELINE_GH: 'growthHackPipelines', + CHECKLIST: 'checklist', + CHECKLIST_ITEM: 'checkListItem', + BRAND: 'brand', + CHANNEL: 'channel', + COMPANY: 'company', + CUSTOMER: 'customer', + DEAL: 'deal', + EMAIL_TEMPLATE: 'emailTemplate', + IMPORT_HISTORY: 'importHistory', + PRODUCT: 'product', + PRODUCT_CATEGORY: 'product-category', + RESPONSE_TEMPLATE: 'responseTemplate', + TAG: 'tag', + TASK: 'task', + TICKET: 'ticket', + PERMISSION: 'permission', + USER: 'user', + KB_TOPIC: 'knowledgeBaseTopic', + KB_CATEGORY: 'knowledgeBaseCategory', + KB_ARTICLE: 'knowledgeBaseArticle', + USER_GROUP: 'userGroup', + INTERNAL_NOTE: 'internalNote', + PIPELINE_LABEL: 'pipelineLabel', + PIPELINE_TEMPLATE: 'pipelineTemplate', + GROWTH_HACK: 'growthHack', + INTEGRATION: 'integration', + SEGMENT: 'segment', + ENGAGE: 'engage', + SCRIPT: 'script', + FIELD: 'field', + WEBHOOK: 'webhook', +}; + +export const RABBITMQ_QUEUES = { + PUT_LOG: 'putLog', + RPC_API_TO_INTEGRATIONS: 'rpc_queue:api_to_integrations', + RPC_API_TO_WORKERS: 'rpc_queue:api_to_workers', + WORKERS: 'workers', +}; + +export const AUTO_BOT_MESSAGES = { + NO_RESPONSE: 'No reply', + CHANGE_OPERATOR: 'The team will reply in message', +}; + +export const BOT_MESSAGE_TYPES = { + SAY_SOMETHING: 'say_something', +}; diff --git a/src/data/dataSources/engages.ts b/src/data/dataSources/engages.ts new file mode 100644 index 000000000..e426b7e59 --- /dev/null +++ b/src/data/dataSources/engages.ts @@ -0,0 +1,85 @@ +import { HTTPCache, RESTDataSource } from 'apollo-datasource-rest'; +import { debugBase } from '../../debuggers'; +import { getSubServiceDomain } from '../utils'; + +export default class EngagesAPI extends RESTDataSource { + constructor() { + super(); + + const ENGAGES_API_DOMAIN = getSubServiceDomain({ name: 'ENGAGES_API_DOMAIN' }); + + this.baseURL = ENGAGES_API_DOMAIN; + this.httpCache = new HTTPCache(); + } + + public didEncounterError(e) { + const error = e.extensions || {}; + const { response } = error; + const { body } = response || { body: e.message }; + + if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND') { + throw new Error('Engages api is not running'); + } + + throw new Error(body); + } + + public async engagesConfigDetail() { + return this.get(`/configs/detail`); + } + + public async engagesUpdateConfigs(configsMap) { + return this.post(`/configs/save`, configsMap); + } + + public async engagesSendTestEmail(params) { + return this.post(`/configs/send-test-email`, params); + } + + public engagesGetVerifiedEmails() { + return this.get(`/configs/get-verified-emails`); + } + + public engagesVerifyEmail(params) { + return this.post(`/configs/verify-email`, params); + } + + public engagesRemoveVerifiedEmail(params) { + return this.post(`/configs/remove-verified-email`, params); + } + + public async engagesStats(engageMessageId) { + try { + const response = await this.get(`/deliveryReports/statsList/${engageMessageId}`); + return response; + } catch (e) { + debugBase(e.message); + return {}; + } + } + + public async engageReportsList(params) { + return this.get(`/deliveryReports/reportsList`, params); + } + + public async engagesLogs(engageMessageId) { + try { + const response = await this.get(`/deliveryReports/logs/${engageMessageId}`); + return response; + } catch (e) { + debugBase(e.message); + return []; + } + } + + public async engagesSmsStats(engageMessageId) { + try { + const response = await this.get(`/deliveryReports/smsStats/${engageMessageId}`); + + return response; + } catch (e) { + debugBase(e.message); + return {}; + } + } +} // end class diff --git a/src/data/dataSources/index.ts b/src/data/dataSources/index.ts new file mode 100644 index 000000000..77f6b23fc --- /dev/null +++ b/src/data/dataSources/index.ts @@ -0,0 +1,4 @@ +import EngagesAPI from './engages'; +import IntegrationsAPI from './integrations'; + +export { EngagesAPI, IntegrationsAPI }; diff --git a/src/data/dataSources/integrations.ts b/src/data/dataSources/integrations.ts new file mode 100644 index 000000000..a421d1695 --- /dev/null +++ b/src/data/dataSources/integrations.ts @@ -0,0 +1,89 @@ +import { HTTPCache, RESTDataSource } from 'apollo-datasource-rest'; +import { getSubServiceDomain } from '../utils'; + +export default class IntegrationsAPI extends RESTDataSource { + constructor() { + super(); + + const INTEGRATIONS_API_DOMAIN = getSubServiceDomain({ name: 'INTEGRATIONS_API_DOMAIN' }); + + this.baseURL = INTEGRATIONS_API_DOMAIN; + this.httpCache = new HTTPCache(); + } + + public willSendRequest(request) { + const { user } = this.context || {}; + + if (user) { + request.headers.set('userId', user._id); + } + } + + public didEncounterError(e) { + const error = e.extensions || {}; + const { response } = error; + const { body } = response || { body: e.message }; + + if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND') { + throw new Error('Integrations api is not running'); + } + + throw new Error(body); + } + + public async createIntegration(kind, params) { + return this.post(`/${kind}/create-integration`, params); + } + + public async removeIntegration(params) { + return this.post('/integrations/remove', params); + } + + public async removeAccount(params) { + return this.post('/accounts/remove', params); + } + + public async replyChatfuel(params) { + return this.post('/chatfuel/reply', params); + } + + public async sendEmail(kind, params) { + return this.post(`/${kind}/send`, params); + } + + public async deleteDailyVideoChatRoom(name) { + return this.delete(`/daily/rooms/${name}`); + } + + public async createDailyVideoChatRoom(params) { + return this.post('/daily/room', params); + } + + public async fetchApi(path, params) { + return this.get(path, params); + } + + public async replyTwitterDm(params) { + return this.post('/twitter/reply', params); + } + + public async replySmooch(params) { + return this.post('/smooch/reply', params); + } + + public async replyWhatsApp(params) { + return this.post('/whatsapp/reply', params); + } + + public async updateConfigs(configsMap) { + return this.post('/update-configs', { configsMap }); + } + + public async createProductBoardNote(params) { + return this.post('/productBoard/create-note', params); + } + + public async sendSms(params) { + return this.post('/telnyx/send-sms', params); + } +} diff --git a/src/data/logUtils.ts b/src/data/logUtils.ts new file mode 100644 index 000000000..89fdffd46 --- /dev/null +++ b/src/data/logUtils.ts @@ -0,0 +1,1344 @@ +import * as _ from 'underscore'; +import { IPipelineDocument } from '../db/models/definitions/boards'; +import { IChannelDocument } from '../db/models/definitions/channels'; +import { ICompanyDocument } from '../db/models/definitions/companies'; +import { ACTIVITY_CONTENT_TYPES } from '../db/models/definitions/constants'; +import { ICustomerDocument } from '../db/models/definitions/customers'; +import { IDealDocument, IProductDocument } from '../db/models/definitions/deals'; +import { IEngageMessage, IEngageMessageDocument } from '../db/models/definitions/engages'; +import { IGrowthHackDocument } from '../db/models/definitions/growthHacks'; +import { IIntegrationDocument } from '../db/models/definitions/integrations'; +import { ICategoryDocument, ITopicDocument } from '../db/models/definitions/knowledgebase'; +import { IPipelineTemplateDocument } from '../db/models/definitions/pipelineTemplates'; +import { IScriptDocument } from '../db/models/definitions/scripts'; +import { ITaskDocument } from '../db/models/definitions/tasks'; +import { ITicketDocument } from '../db/models/definitions/tickets'; +import { IUserDocument } from '../db/models/definitions/users'; +import { + Boards, + Brands, + Checklists, + Companies, + Customers, + Deals, + Forms, + GrowthHacks, + Integrations, + KnowledgeBaseArticles, + KnowledgeBaseCategories, + KnowledgeBaseTopics, + PipelineLabels, + Pipelines, + ProductCategories, + Products, + Segments, + Stages, + Tags, + Tasks, + Tickets, + Users, + UsersGroups, +} from '../db/models/index'; +import messageBroker from '../messageBroker'; +import { MODULE_NAMES, RABBITMQ_QUEUES } from './constants'; +import { getSubServiceDomain, registerOnboardHistory, sendRequest, sendToWebhook } from './utils'; + +export type LogDesc = { + [key: string]: any; +} & { name: any }; + +interface ILogNameParams { + idFields: string[]; + foreignKey: string; + prevList?: LogDesc[]; +} + +interface ILogParams extends ILogNameParams { + collection: any; + nameFields: string[]; +} + +interface IContentTypeParams { + contentType: string; + contentTypeId: string; +} + +/** + * @param object - Previous state of the object + * @param newData - Requested update data + * @param updatedDocument - State after any updates to the object + */ +export interface ILogDataParams { + type: string; + description?: string; + object: any; + newData?: object; + extraDesc?: object[]; + updatedDocument?: any; +} + +interface IFinalLogParams extends ILogDataParams { + action: string; +} + +export interface ILogQueryParams { + start?: string; + end?: string; + userId?: string; + action?: string; + page?: number; + perPage?: number; + type?: string; +} + +interface IDescriptions { + description?: string; + extraDesc?: LogDesc[]; +} + +interface IDescriptionParams { + action: string; + type: string; + obj: any; + updatedDocument?: any; +} + +type BoardItemDocument = IDealDocument | ITaskDocument | ITicketDocument | IGrowthHackDocument; + +const LOG_ACTIONS = { + CREATE: 'create', + UPDATE: 'update', + DELETE: 'delete', +}; + +// used in internalNotes mutations +const findContentItemName = async (contentType: string, contentTypeId: string): Promise => { + let name: string = ''; + + if (contentType === MODULE_NAMES.DEAL) { + const deal = await Deals.getDeal(contentTypeId); + + if (deal && deal.name) { + name = deal.name; + } + } + if (contentType === MODULE_NAMES.CUSTOMER) { + const customer = await Customers.getCustomer(contentTypeId); + + if (customer) { + name = Customers.getCustomerName(customer); + } + } + if (contentType === MODULE_NAMES.COMPANY) { + const company = await Companies.getCompany(contentTypeId); + + if (company) { + name = Companies.getCompanyName(company); + } + } + if (contentType === MODULE_NAMES.TASK) { + const task = await Tasks.getTask(contentTypeId); + + if (task && task.name) { + name = task.name; + } + } + if (contentType === MODULE_NAMES.TICKET) { + const ticket = await Tickets.getTicket(contentTypeId); + + if (ticket && ticket.name) { + name = ticket.name; + } + } + if (contentType === MODULE_NAMES.GROWTH_HACK) { + const gh = await GrowthHacks.getGrowthHack(contentTypeId); + + if (gh && gh.name) { + name = gh.name; + } + } + if (contentType === MODULE_NAMES.USER) { + const user = await Users.getUser(contentTypeId); + + if (user) { + name = user.username || user.email || ''; + } + } + if (contentType === MODULE_NAMES.PRODUCT) { + const product = await Products.getProduct({ _id: contentTypeId }); + + if (product) { + name = product.name; + } + } + + return name; +}; + +const gatherUsernames = async (params: ILogNameParams): Promise => { + const { idFields, foreignKey, prevList } = params; + + return gatherNames({ + collection: Users, + idFields, + foreignKey, + prevList, + nameFields: ['email', 'username'], + }); +}; + +const gatherIntegrationNames = async (params: ILogNameParams): Promise => { + const { idFields, foreignKey, prevList } = params; + + return gatherNames({ + collection: Integrations, + idFields, + foreignKey, + prevList, + nameFields: ['name'], + }); +}; + +export const gatherTagNames = async (params: ILogNameParams): Promise => { + const { idFields, foreignKey, prevList } = params; + + return gatherNames({ + collection: Tags, + idFields, + foreignKey, + prevList, + nameFields: ['name'], + }); +}; + +const gatherBrandNames = async (params: ILogNameParams): Promise => { + const { idFields, foreignKey, prevList } = params; + + return gatherNames({ + collection: Brands, + idFields, + foreignKey, + prevList, + nameFields: ['name'], + }); +}; + +/** + * Finds name field from given collection + * @param params.collection Collection to find + * @param params.idFields Id fields saved in collection + * @param params.foreignKey Name of id fields + * @param params.prevList Array to save found id with name + * @param params.nameFields List of values to be mapped to id field + */ +const gatherNames = async (params: ILogParams): Promise => { + const { collection, idFields, foreignKey, prevList, nameFields = [] } = params; + let options: LogDesc[] = []; + + if (prevList && prevList.length > 0) { + options = prevList; + } + + const uniqueIds = _.compact(_.uniq(idFields)); + + for (const id of uniqueIds) { + const item = await collection.findOne({ _id: id }); + let name: string = `item with id "${id}" has been deleted`; + + if (item) { + for (const n of nameFields) { + if (item[n]) { + name = item[n]; + } + } + } + + options.push({ [foreignKey]: id, name }); + } + + return options; +}; + +const findItemName = async ({ contentType, contentTypeId }: IContentTypeParams): Promise => { + let item: any; + let name: string = ''; + + if (contentType === ACTIVITY_CONTENT_TYPES.DEAL) { + item = await Deals.findOne({ _id: contentTypeId }); + } + + if (contentType === ACTIVITY_CONTENT_TYPES.TASK) { + item = await Tasks.findOne({ _id: contentTypeId }); + } + + if (contentType === ACTIVITY_CONTENT_TYPES.TICKET) { + item = await Tickets.findOne({ _id: contentTypeId }); + } + + if (contentType === ACTIVITY_CONTENT_TYPES.GROWTH_HACK) { + item = await GrowthHacks.getGrowthHack(contentTypeId); + } + + if (item && item.name) { + name = item.name; + } + + return name; +}; + +const gatherCompanyFieldNames = async (doc: ICompanyDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.parentCompanyId) { + options = await gatherNames({ + collection: Companies, + idFields: [doc.parentCompanyId], + foreignKey: 'parentCompanyId', + prevList: options, + nameFields: ['primaryName'], + }); + } + + if (doc.ownerId) { + options = await gatherUsernames({ + idFields: [doc.ownerId], + foreignKey: 'ownerId', + prevList: options, + }); + } + + if (doc.mergedIds && doc.mergedIds.length > 0) { + options = await gatherNames({ + collection: Companies, + idFields: doc.mergedIds, + foreignKey: 'mergedIds', + prevList: options, + nameFields: ['primaryName'], + }); + } + + if (doc.tagIds && doc.tagIds.length > 0) { + options = await gatherTagNames({ + idFields: doc.tagIds, + foreignKey: 'tagIds', + prevList: options, + }); + } + + return options; +}; + +const gatherCustomerFieldNames = async (doc: ICustomerDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.ownerId) { + options = await gatherUsernames({ idFields: [doc.ownerId], foreignKey: 'ownerId', prevList: options }); + } + + if (doc.integrationId) { + options = await gatherIntegrationNames({ + idFields: [doc.integrationId], + foreignKey: 'integrationId', + prevList: options, + }); + } + + if (doc.tagIds && doc.tagIds.length > 0) { + options = await gatherTagNames({ + idFields: doc.tagIds, + foreignKey: 'tagIds', + prevList: options, + }); + } + + if (doc.mergedIds) { + options = await gatherNames({ + collection: Customers, + idFields: doc.mergedIds, + foreignKey: 'mergedIds', + prevList: options, + nameFields: ['firstName'], + }); + } + + return options; +}; + +const gatherBoardItemFieldNames = async (doc: BoardItemDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.userId) { + options = await gatherUsernames({ + idFields: [doc.userId], + foreignKey: 'userId', + prevList: options, + }); + } + + if (doc.assignedUserIds && doc.assignedUserIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.assignedUserIds, + foreignKey: 'assignedUserIds', + prevList: options, + }); + } + + if (doc.watchedUserIds && doc.watchedUserIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.watchedUserIds, + foreignKey: 'watchedUserIds', + prevList: options, + }); + } + + if (doc.labelIds && doc.labelIds.length > 0) { + options = await gatherNames({ + collection: PipelineLabels, + idFields: doc.labelIds, + foreignKey: 'labelIds', + prevList: options, + nameFields: ['name'], + }); + } + + options = await gatherNames({ + collection: Stages, + idFields: [doc.stageId], + foreignKey: 'stageId', + prevList: options, + nameFields: ['name'], + }); + + if (doc.initialStageId) { + options = await gatherNames({ + collection: Stages, + idFields: [doc.initialStageId], + foreignKey: 'initialStageId', + prevList: options, + nameFields: ['name'], + }); + } + + if (doc.modifiedBy) { + options = await gatherUsernames({ + idFields: [doc.modifiedBy], + foreignKey: 'modifiedBy', + prevList: options, + }); + } + + return options; +}; + +const gatherDealFieldNames = async (doc: IDealDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + options = await gatherBoardItemFieldNames(doc, options); + + if (doc.productsData && doc.productsData.length > 0) { + options = await gatherNames({ + collection: Products, + idFields: doc.productsData.map(p => p.productId), + foreignKey: 'productId', + prevList: options, + nameFields: ['name'], + }); + } + + return options; +}; + +const gatherEngageFieldNames = async ( + doc: IEngageMessageDocument | IEngageMessage, + prevList?: LogDesc[], +): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.segmentIds && doc.segmentIds.length > 0) { + options = await gatherNames({ + collection: Segments, + idFields: doc.segmentIds, + foreignKey: 'segmentIds', + prevList: options, + nameFields: ['name'], + }); + } + + if (doc.brandIds && doc.brandIds.length > 0) { + options = await gatherBrandNames({ + idFields: doc.brandIds, + foreignKey: 'brandIds', + prevList: options, + }); + } + + if (doc.tagIds && doc.tagIds.length > 0) { + options = await gatherTagNames({ + idFields: doc.tagIds, + foreignKey: 'tagIds', + prevList: options, + }); + } + + if (doc.fromUserId) { + options = await gatherUsernames({ + idFields: [doc.fromUserId], + foreignKey: 'fromUserId', + prevList: options, + }); + } + + if (doc.messenger && doc.messenger.brandId) { + options = await gatherBrandNames({ + idFields: [doc.messenger.brandId], + foreignKey: 'brandId', + prevList: options, + }); + } + + return options; +}; + +const gatherChannelFieldNames = async (doc: IChannelDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.userId) { + options = await gatherUsernames({ + idFields: [doc.userId], + foreignKey: 'userId', + prevList: options, + }); + } + + if (doc.memberIds && doc.memberIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.memberIds, + foreignKey: 'memberIds', + prevList: options, + }); + } + + if (doc.integrationIds && doc.integrationIds.length > 0) { + options = await gatherIntegrationNames({ + idFields: doc.integrationIds, + foreignKey: 'integrationIds', + prevList: options, + }); + } + + return options; +}; + +const gatherGHFieldNames = async (doc: IGrowthHackDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + options = await gatherBoardItemFieldNames(doc, options); + + if (doc.votedUserIds && doc.votedUserIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.votedUserIds, + foreignKey: 'votedUserIds', + prevList: options, + }); + } + + return options; +}; + +const gatherIntegrationFieldNames = async (doc: IIntegrationDocument, prevList?: LogDesc[]) => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.createdUserId) { + options = await gatherUsernames({ + idFields: [doc.createdUserId], + foreignKey: 'createdUserId', + prevList: options, + }); + } + + if (doc.brandId) { + options = await gatherBrandNames({ + idFields: [doc.brandId], + foreignKey: 'brandId', + prevList: options, + }); + } + + if (doc.tagIds && doc.tagIds.length > 0) { + options = await gatherTagNames({ + idFields: doc.tagIds, + foreignKey: 'tagIds', + prevList: options, + }); + } + + if (doc.formId) { + options = await gatherNames({ + collection: Forms, + idFields: [doc.formId], + foreignKey: 'formId', + prevList: options, + nameFields: ['title'], + }); + } + + return options; +}; + +const gatherKbTopicFieldNames = async (doc: ITopicDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + options = await gatherUsernames({ + idFields: [doc.createdBy], + foreignKey: 'createdBy', + prevList: options, + }); + + options = await gatherUsernames({ + idFields: [doc.modifiedBy], + foreignKey: 'modifiedBy', + prevList: options, + }); + + if (doc.brandId) { + options = await gatherBrandNames({ + idFields: [doc.brandId], + foreignKey: 'brandId', + prevList: options, + }); + } + + if (doc.categoryIds && doc.categoryIds.length > 0) { + // categories are removed alongside + const categories = await KnowledgeBaseCategories.find({ _id: { $in: doc.categoryIds } }, { title: 1 }); + + for (const cat of categories) { + options.push({ + categoryIds: cat._id, + name: cat.title, + }); + } + } + + return options; +}; + +const gatherKbCategoryFieldNames = async (doc: ICategoryDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + const articles = await KnowledgeBaseArticles.find({ _id: { $in: doc.articleIds } }, { title: 1 }); + + options = await gatherUsernames({ + idFields: [doc.createdBy], + foreignKey: 'createdBy', + prevList: options, + }); + + options = await gatherUsernames({ + idFields: [doc.modifiedBy], + foreignKey: 'modifiedBy', + prevList: options, + }); + + if (articles.length > 0) { + for (const article of articles) { + options.push({ articleIds: article._id, name: article.title }); + } + } + + return options; +}; + +const gatherProductFieldNames = async (doc: IProductDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.tagIds && doc.tagIds.length > 0) { + options = await gatherTagNames({ + idFields: doc.tagIds, + foreignKey: 'tagIds', + prevList: options, + }); + } + + if (doc.categoryId) { + options = await gatherNames({ + collection: ProductCategories, + idFields: [doc.categoryId], + foreignKey: 'categoryId', + prevList: options, + nameFields: ['name'], + }); + } + + return options; +}; + +const gatherScriptFieldNames = async (doc: IScriptDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + if (doc.messengerId) { + options = await gatherIntegrationNames({ + idFields: [doc.messengerId], + foreignKey: 'messengerId', + prevList: options, + }); + } + + if (doc.kbTopicId) { + options = await gatherNames({ + collection: KnowledgeBaseTopics, + idFields: [doc.kbTopicId], + foreignKey: 'kbTopicId', + prevList: options, + nameFields: ['title'], + }); + } + + if (doc.leadIds && doc.leadIds.length > 0) { + options = await gatherIntegrationNames({ + idFields: doc.leadIds, + foreignKey: 'leadIds', + prevList: options, + }); + } + + return options; +}; + +const gatherPipelineFieldNames = async (doc: IPipelineDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + options = await gatherNames({ + collection: Boards, + idFields: [doc.boardId], + foreignKey: 'boardId', + nameFields: ['name'], + prevList: options, + }); + + if (doc.userId) { + options = await gatherUsernames({ + idFields: [doc.userId], + foreignKey: 'userId', + prevList: options, + }); + } + + if (doc.excludeCheckUserIds && doc.excludeCheckUserIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.excludeCheckUserIds, + foreignKey: 'excludeCheckUserIds', + prevList: options, + }); + } + + if (doc.memberIds && doc.memberIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.memberIds, + foreignKey: 'memberIds', + prevList: options, + }); + } + + if (doc.watchedUserIds && doc.watchedUserIds.length > 0) { + options = await gatherUsernames({ + idFields: doc.watchedUserIds, + foreignKey: 'watchedUserIds', + prevList: options, + }); + } + + return options; +}; + +const gatherPipelineTemplateFieldNames = async ( + doc: IPipelineTemplateDocument, + prevList?: LogDesc[], +): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + options = await gatherUsernames({ + idFields: [doc.createdBy], + foreignKey: 'createdBy', + prevList: options, + }); + + if (doc.stages && doc.stages.length > 0) { + options = await gatherNames({ + collection: Forms, + idFields: doc.stages.map(s => s.formId), + foreignKey: 'formId', + prevList: options, + nameFields: ['title'], + }); + } + + return options; +}; + +const gatherUserFieldNames = async (doc: IUserDocument, prevList?: LogDesc[]): Promise => { + let options: LogDesc[] = []; + + if (prevList) { + options = prevList; + } + + // show only user group names of users for now + options = await gatherNames({ + collection: UsersGroups, + idFields: doc.groupIds || [], + foreignKey: 'groupIds', + nameFields: ['name'], + prevList: options, + }); + + return options; +}; + +const gatherDescriptions = async (params: IDescriptionParams): Promise => { + const { action, type, obj, updatedDocument } = params; + + let extraDesc: LogDesc[] = []; + let description: string = ''; + + switch (type) { + case MODULE_NAMES.BRAND: + case MODULE_NAMES.BOARD_DEAL: + case MODULE_NAMES.BOARD_GH: + case MODULE_NAMES.BOARD_TASK: + case MODULE_NAMES.BOARD_TICKET: + if (obj.userId) { + extraDesc = await gatherUsernames({ idFields: [obj.userId], foreignKey: 'userId' }); + } + + description = `"${obj.name}" has been ${action}d`; + + break; + case MODULE_NAMES.PIPELINE_DEAL: + case MODULE_NAMES.PIPELINE_GH: + case MODULE_NAMES.PIPELINE_TASK: + case MODULE_NAMES.PIPELINE_TICKET: + extraDesc = await gatherPipelineFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherPipelineFieldNames(updatedDocument, extraDesc); + } + + description = `"${obj.name}" has been ${action}d`; + + break; + case MODULE_NAMES.CHANNEL: + extraDesc = await gatherChannelFieldNames(obj); + description = `"${obj.name}" has been ${action}d`; + + if (updatedDocument) { + extraDesc = await gatherChannelFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.CHECKLIST: + const itemName = await findItemName({ contentType: obj.contentType, contentTypeId: obj.contentTypeId }); + + extraDesc = await gatherUsernames({ idFields: [obj.createdUserId], foreignKey: 'createdUserId' }); + + extraDesc.push({ contentTypeId: obj.contentTypeId, name: itemName }); + + if (action === LOG_ACTIONS.CREATE) { + description = `"${obj.title}" has been created in ${obj.contentType.toUpperCase()} "${itemName}"`; + } + if (action === LOG_ACTIONS.UPDATE) { + description = `"${obj.title}" saved in ${obj.contentType.toUpperCase()} "${itemName}" has been edited`; + } + if (action === LOG_ACTIONS.DELETE) { + description = `"${obj.title}" from ${obj.contentType.toUpperCase()} "${itemName}" has been removed`; + } + + break; + case MODULE_NAMES.CHECKLIST_ITEM: + const checklist = await Checklists.getChecklist(obj.checklistId); + + extraDesc = await gatherUsernames({ idFields: [obj.createdUserId], foreignKey: 'createdUserid' }); + + extraDesc.push({ checklistId: checklist._id, name: checklist.title }); + + if (action === LOG_ACTIONS.CREATE) { + description = `"${obj.content}" has been added to "${checklist.title}"`; + } + if (action === LOG_ACTIONS.UPDATE) { + description = `"${obj.content}" has been edited /checked/`; + } + if (action === LOG_ACTIONS.DELETE) { + description = `"${obj.content}" has been removed from "${checklist.title}"`; + } + + break; + case MODULE_NAMES.COMPANY: + extraDesc = await gatherCompanyFieldNames(obj); + description = `"${obj.primaryName}" has been ${action}d`; + + if (updatedDocument) { + extraDesc = await gatherCompanyFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.CUSTOMER: + description = `"${obj.firstName}" has been ${action}d`; + + extraDesc = await gatherCustomerFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherCustomerFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.DEAL: + description = `"${obj.name}" has been ${action}d`; + extraDesc = await gatherDealFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherDealFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.EMAIL_TEMPLATE: + description = `"${obj.name}" has been ${action}d`; + + break; + case MODULE_NAMES.ENGAGE: + description = `"${obj.title}" has been ${action}d`; + extraDesc = await gatherEngageFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherEngageFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.GROWTH_HACK: + description = `"${obj.name}" has been ${action}d`; + + extraDesc = await gatherGHFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherGHFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.IMPORT_HISTORY: + description = `${obj._id}-${obj.date} has been removed`; + + extraDesc = await gatherUsernames({ + idFields: [obj.userId], + foreignKey: 'userId', + prevList: extraDesc, + }); + + const param = { + idFields: obj.ids, + foreignKey: 'ids', + prevList: extraDesc, + }; + + switch (obj.contentType) { + case MODULE_NAMES.COMPANY: + extraDesc = await gatherNames({ ...param, collection: Companies, nameFields: ['primaryName'] }); + break; + case MODULE_NAMES.CUSTOMER: + extraDesc = await gatherNames({ ...param, collection: Customers, nameFields: ['firstName'] }); + break; + case MODULE_NAMES.PRODUCT: + extraDesc = await gatherNames({ ...param, collection: Products, nameFields: ['name'] }); + break; + default: + break; + } + + break; + case MODULE_NAMES.INTEGRATION: + description = `"${obj.name}" has been ${action}d`; + + extraDesc = await gatherIntegrationFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherIntegrationFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.INTERNAL_NOTE: + description = `Note of type ${obj.contentType} has been ${action}d`; + + extraDesc = [ + { + contentTypeId: obj.contentTypeId, + name: await findContentItemName(obj.contentType, obj.contentTypeId), + }, + ]; + + extraDesc = await gatherUsernames({ + idFields: [obj.createdUserId], + foreignKey: 'createdUserId', + prevList: extraDesc, + }); + + break; + case MODULE_NAMES.KB_TOPIC: + description = `"${obj.title}" has been ${action}d`; + + extraDesc = await gatherKbTopicFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherKbTopicFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.KB_CATEGORY: + description = `"${obj.title}" has been ${action}d`; + + extraDesc = await gatherKbCategoryFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherKbCategoryFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.KB_ARTICLE: + description = `"${obj.title}" has been ${action}d`; + + extraDesc = await gatherUsernames({ idFields: [obj.createdBy], foreignKey: 'createdBy' }); + + if (obj.modifiedBy) { + extraDesc = await gatherUsernames({ + idFields: [obj.modifiedBy], + foreignKey: 'modifiedBy', + prevList: extraDesc, + }); + } + + if (updatedDocument && updatedDocument.modifiedBy) { + extraDesc = await gatherUsernames({ + idFields: [updatedDocument.modifiedBy], + foreignKey: 'modifiedBy', + prevList: extraDesc, + }); + } + + break; + case MODULE_NAMES.PERMISSION: + description = `Permission of module "${obj.module}", action "${obj.action}" assigned to `; + + if (obj.groupId) { + const group = await UsersGroups.getGroup(obj.groupId); + + description = `${description} user group "${group.name}" `; + + extraDesc.push({ groupId: obj.groupId, name: group.name }); + } + + if (obj.userId) { + const permUser = await Users.getUser(obj.userId); + + description = `${description} user "${permUser.email}" has been ${action}d`; + + extraDesc.push({ userId: obj.userId, name: permUser.username || permUser.email }); + } + + break; + case MODULE_NAMES.PIPELINE_LABEL: + description = `"${obj.name}" has been ${action}d`; + + const pipeline = await Pipelines.findOne({ _id: obj.pipelineId }); + + extraDesc = await gatherUsernames({ idFields: [obj.createdBy], foreignKey: 'createdBy' }); + + if (pipeline) { + extraDesc.push({ pipelineId: pipeline._id, name: pipeline.name }); + } + + break; + case MODULE_NAMES.PIPELINE_TEMPLATE: + extraDesc = await gatherPipelineTemplateFieldNames(obj); + + description = `"${obj.name}" has been created`; + + if (updatedDocument) { + extraDesc = await gatherPipelineTemplateFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.PRODUCT: + description = `${obj.name} has been ${action}d`; + + extraDesc = await gatherProductFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherProductFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.PRODUCT_CATEGORY: + description = `"${obj.name}" has been ${action}d`; + + const parentIds: string[] = []; + + if (obj.parentId) { + parentIds.push(obj.parentId); + } + + if (updatedDocument && updatedDocument.parentId !== obj.parentId) { + parentIds.push(updatedDocument.parentId); + } + + if (parentIds.length > 0) { + extraDesc = await gatherNames({ + collection: ProductCategories, + idFields: parentIds, + foreignKey: 'parentId', + nameFields: ['name'], + }); + } + + break; + case MODULE_NAMES.RESPONSE_TEMPLATE: + description = `"${obj.name}" has been created`; + + const brandIds: string[] = []; + + if (obj.brandId) { + brandIds.push(obj.brandId); + } + + if (updatedDocument && updatedDocument.brandId && updatedDocument.brandId !== obj.brandId) { + brandIds.push(updatedDocument.brandId); + } + + if (brandIds.length > 0) { + extraDesc = await gatherBrandNames({ idFields: brandIds, foreignKey: 'brandId' }); + } + + break; + case MODULE_NAMES.SCRIPT: + description = `"${obj.name}" has been ${action}d`; + + extraDesc = await gatherScriptFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherScriptFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.SEGMENT: + const parents: string[] = []; + + if (obj.subOf) { + parents.push(obj.subOf); + } + + if (updatedDocument && updatedDocument.subOf && updatedDocument.subOf !== obj.subOf) { + parents.push(updatedDocument.subOf); + } + + if (parents.length > 0) { + extraDesc = await gatherNames({ + collection: Segments, + idFields: parents, + foreignKey: 'subOf', + nameFields: ['name'], + }); + } + + description = `"${obj.name}" has been ${action}d`; + + break; + case MODULE_NAMES.TASK: + description = `"${obj.name}" has been ${action}d`; + + extraDesc = await gatherBoardItemFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherBoardItemFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.TICKET: + description = `"${obj.name}" has been ${action}`; + + extraDesc = await gatherBoardItemFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherBoardItemFieldNames(updatedDocument, extraDesc); + } + + break; + case MODULE_NAMES.USER: + description = `"${obj.username || obj.email}" has been ${action}`; + + extraDesc = await gatherUserFieldNames(obj); + + if (updatedDocument) { + extraDesc = await gatherUserFieldNames(updatedDocument, extraDesc); + } + + break; + default: + break; + } + + return { extraDesc, description }; +}; + +/** + * Prepares a create log request to log server + * @param params Log document params + * @param user User information from mutation context + */ +export const putCreateLog = async (params: ILogDataParams, user: IUserDocument) => { + await registerOnboardHistory({ type: `${params.type}Create`, user }); + + const descriptions = await gatherDescriptions({ action: LOG_ACTIONS.CREATE, type: params.type, obj: params.object }); + + await sendToWebhook(LOG_ACTIONS.CREATE, params.type, params); + + return putLog( + { + ...params, + action: LOG_ACTIONS.CREATE, + extraDesc: descriptions.extraDesc, + description: params.description || descriptions.description, + }, + user, + ); +}; + +/** + * Prepares a create log request to log server + * @param params Log document params + * @param user User information from mutation context + */ +export const putUpdateLog = async (params: ILogDataParams, user: IUserDocument) => { + const descriptions = await gatherDescriptions({ + action: LOG_ACTIONS.UPDATE, + type: params.type, + obj: params.object, + updatedDocument: params.updatedDocument, + }); + + await sendToWebhook(LOG_ACTIONS.UPDATE, params.type, params); + + return putLog( + { + ...params, + action: LOG_ACTIONS.UPDATE, + description: params.description || descriptions.description, + extraDesc: descriptions.extraDesc, + }, + user, + ); +}; + +/** + * Prepares a create log request to log server + * @param params Log document params + * @param user User information from mutation context + */ +export const putDeleteLog = async (params: ILogDataParams, user: IUserDocument) => { + const descriptions = await gatherDescriptions({ + action: LOG_ACTIONS.DELETE, + type: params.type, + obj: params.object, + }); + + await sendToWebhook(LOG_ACTIONS.DELETE, params.type, params); + + return putLog( + { + ...params, + action: LOG_ACTIONS.DELETE, + extraDesc: descriptions.extraDesc, + description: params.description || descriptions.description, + }, + user, + ); +}; + +const putLog = async (params: IFinalLogParams, user: IUserDocument) => { + try { + return messageBroker().sendMessage(RABBITMQ_QUEUES.PUT_LOG, { + ...params, + createdBy: user._id, + unicode: user.username || user.email || user._id, + object: JSON.stringify(params.object), + newData: JSON.stringify(params.newData), + extraDesc: JSON.stringify(params.extraDesc), + }); + } catch (e) { + return e.message; + } +}; + +/** + * Sends a request to logs api + * @param {Object} param0 Request + */ +export const fetchLogs = (params: ILogQueryParams) => { + const LOGS_DOMAIN = getSubServiceDomain({ name: 'LOGS_API_DOMAIN' }); + + return sendRequest( + { url: `${LOGS_DOMAIN}/logs`, method: 'get', body: { params: JSON.stringify(params) } }, + 'Failed to connect to logs api. Check whether LOGS_API_DOMAIN env is missing or logs api is not running', + ); +}; diff --git a/src/data/modules/coc/companies.ts b/src/data/modules/coc/companies.ts index b8528970a..d748077a6 100644 --- a/src/data/modules/coc/companies.ts +++ b/src/data/modules/coc/companies.ts @@ -1,28 +1,7 @@ -import { Customers, Integrations, Segments } from '../../../db/models'; -import { STATUSES } from '../../../db/models/definitions/constants'; -import QueryBuilder from '../segments/queryBuilder'; - -export interface IListArgs { - page?: number; - perPage?: number; - segment?: string; - tag?: string; - ids?: string[]; - searchValue?: string; - lifecycleState?: string; - leadStatus?: string; - sortField?: string; - sortDirection?: number; - brand?: string; -} - -interface IIn { - $in: string[]; -} - -interface IBrandFilter { - _id: IIn; -} +import * as _ from 'underscore'; +import { Companies, Conformities, Customers, Integrations } from '../../../db/models'; +import { IConformityQueryParams } from '../../resolvers/queries/types'; +import { CommonBuilder } from './utils'; type TSortBuilder = { primaryName: number } | { [index: string]: number }; @@ -39,76 +18,69 @@ export const sortBuilder = (params: IListArgs): TSortBuilder => { return sortParams; }; -/* - * Brand filter - */ -export const brandFilter = async (brandId: string): Promise => { - const integrations = await Integrations.find({ brandId }, { _id: 1 }); - const integrationIds = integrations.map(i => i._id); - - const customers = await Customers.find({ integrationId: { $in: integrationIds } }, { companyIds: 1 }); - - let companyIds: any = []; +export interface IListArgs extends IConformityQueryParams { + segment?: string; + tag?: string; + ids?: string[]; + searchValue?: string; + brand?: string; + sortField?: string; + sortDirection?: number; +} - for (const customer of customers) { - companyIds = [...companyIds, ...(customer.companyIds || [])]; +export class Builder extends CommonBuilder { + constructor(params: IListArgs, context) { + super('companies', params, context); } - return { _id: { $in: companyIds } }; -}; - -export const filter = async (params: IListArgs) => { - let selector: any = { - status: { $ne: STATUSES.DELETED }, - }; - - // Filter by segments - if (params.segment) { - const segment = await Segments.findOne({ _id: params.segment }); - const query = await QueryBuilder.segments(segment); - - Object.assign(selector, query); + // filter by brand + public async brandFilter(brandId: string): Promise { + const integrations = await Integrations.findIntegrations({ brandId }, { _id: 1 }); + const integrationIds = integrations.map(i => i._id); + + const customers = await Customers.find({ integrationId: { $in: integrationIds } }, { companyIds: 1 }); + + const customerIds = await customers.map(customer => customer._id); + const companyIds = await Conformities.filterConformity({ + mainType: 'customer', + mainTypeIds: customerIds, + relType: 'company', + }); + + this.positiveList.push({ + terms: { + _id: companyIds || [], + }, + }); } - if (params.searchValue) { - const fields = [ - { names: { $in: [new RegExp(`.*${params.searchValue}.*`, 'i')] } }, - { primaryEmail: new RegExp(`.*${params.searchValue}.*`, 'i') }, - { primaryPhone: new RegExp(`.*${params.searchValue}.*`, 'i') }, - { emails: { $in: [new RegExp(`.*${params.searchValue}.*`, 'i')] } }, - { phones: { $in: [new RegExp(`.*${params.searchValue}.*`, 'i')] } }, - { website: new RegExp(`.*${params.searchValue}.*`, 'i') }, - { industry: new RegExp(`.*${params.searchValue}.*`, 'i') }, - { plan: new RegExp(`.*${params.searchValue}.*`, 'i') }, - ]; - - selector = { $or: fields }; - } + public async findAllMongo(limit: number) { + const selector = { + ...this.context.commonQuerySelector, + status: { $ne: 'deleted' }, + }; - // Filter by tag - if (params.tag) { - selector.tagIds = params.tag; - } + const companies = await Companies.find(selector) + .sort({ createdAt: -1 }) + .limit(limit); - // filter directly using ids - if (params.ids) { - selector = { _id: { $in: params.ids } }; - } + const count = await Companies.find(selector).countDocuments(); - // filter by lead status - if (params.leadStatus) { - selector.leadStatus = params.leadStatus; + return { + list: companies, + totalCount: count, + }; } - // filter by life cycle state - if (params.lifecycleState) { - selector.lifecycleState = params.lifecycleState; - } + /* + * prepare all queries. do not do any action + */ + public async buildAllQueries(): Promise { + await super.buildAllQueries(); - // filter by brandId - if (params.brand) { - selector = { ...selector, ...(await brandFilter(params.brand)) }; + // filter by brand + if (this.params.brand) { + await this.brandFilter(this.params.brand); + } } - - return selector; -}; +} diff --git a/src/data/modules/coc/customers.ts b/src/data/modules/coc/customers.ts index 5be3094bd..484d9b054 100644 --- a/src/data/modules/coc/customers.ts +++ b/src/data/modules/coc/customers.ts @@ -1,8 +1,8 @@ import * as moment from 'moment'; import * as _ from 'underscore'; -import { Brands, Forms, Integrations, Segments } from '../../../db/models'; -import { STATUSES } from '../../../db/models/definitions/constants'; -import QueryBuilder from '../segments/queryBuilder'; +import { Customers, FormSubmissions, Integrations } from '../../../db/models'; +import { IConformityQueryParams } from '../../resolvers/queries/types'; +import { CommonBuilder } from './utils'; interface ISortParams { [index: string]: number; @@ -12,7 +12,7 @@ export const sortBuilder = (params: IListArgs): ISortParams => { const sortField = params.sortField; const sortDirection = params.sortDirection || 0; - let sortParams: ISortParams = { 'messengerData.lastSeenAt': -1 }; + let sortParams: ISortParams = { createdAt: -1 }; if (sortField) { sortParams = { [sortField]: sortDirection }; @@ -21,13 +21,7 @@ export const sortBuilder = (params: IListArgs): ISortParams => { return sortParams; }; -interface IIn { - $in: string[]; -} - -export interface IListArgs { - page?: number; - perPage?: number; +export interface IListArgs extends IConformityQueryParams { segment?: string; tag?: string; ids?: string[]; @@ -36,240 +30,163 @@ export interface IListArgs { form?: string; startDate?: string; endDate?: string; - lifecycleState?: string; leadStatus?: string; type?: string; - sortField?: string; - sortDirection?: number; - byFakeSegment?: any; integrationType?: string; integration?: string; + sortField?: string; + sortDirection?: number; + popupData?: string; } -interface IIntegrationIds { - integrationId: IIn; -} - -interface IIdsFilter { - _id: IIn; -} - -export class Builder { - public params: IListArgs; - public queries: any; +export class Builder extends CommonBuilder { + constructor(params: IListArgs, context) { + super('customers', params, context); - constructor(params: IListArgs) { - this.params = params; + this.addStateFilter(); } - public defaultFilters(): { status: {}; profileScore: { $gt: number } } { - return { - status: { $ne: STATUSES.DELETED }, - profileScore: { $gt: 0 }, - }; + public addStateFilter() { + if (this.params.type) { + this.positiveList.push({ + term: { + state: this.params.type, + }, + }); + } } - // filter by segment - public async segmentFilter(segmentId: string) { - const segment = await Segments.findOne({ _id: segmentId }); - const brandsMapping = {}; - - const brands = await Brands.find({}); + public resetPositiveList() { + this.positiveList = []; - for (const brand of brands) { - const integrations = await Integrations.find({ brandId: brand._id }); + this.addStateFilter(); - const integrationIds = integrations.map(integration => integration._id); - - brandsMapping[brand._id] = integrationIds; + if (this.context.commonQuerySelectorElk) { + this.positiveList.push(this.context.commonQuerySelectorElk); } - - return QueryBuilder.segments(segment, null, brandsMapping); } // filter by brand - public async brandFilter(brandId: string): Promise { - const integrations = await Integrations.find({ brandId }); - - return { integrationId: { $in: integrations.map(i => i._id) } }; - } - - // filter by integration kind - public async integrationTypeFilter(kind: string): Promise { - const integrations = await Integrations.find({ kind }); + public async brandFilter(brandId: string): Promise { + const integrations = await Integrations.findIntegrations({ brandId }); - return { integrationId: { $in: integrations.map(i => i._id) } }; + this.positiveList.push({ + terms: { + relatedIntegrationIds: integrations.map(i => i._id), + }, + }); } // filter by integration - public async integrationFilter(integration: string): Promise { - const integrations = await Integrations.find({ - kind: integration, - }); + public async integrationFilter(integration: string): Promise { + const integrations = await Integrations.findIntegrations({ kind: integration }); + /** * Since both of brand and integration filters use a same integrationId field * we need to intersect two arrays of integration ids. */ - const ids = integrations.map(i => i._id); - - const intersectionedIds = this.queries.integrationId ? _.intersection(ids, this.queries.integrationId.$in) : ids; - - return { integrationId: { $in: intersectionedIds } }; - } - - // filter by tagId - public tagFilter(tagId: string): { tagIds: IIn } { - return { tagIds: { $in: [tagId] } }; - } - - // filter by search value - public searchFilter(value: string): { $or: any } { - const fields = [ - { firstName: new RegExp(`.*${value}.*`, 'i') }, - { lastName: new RegExp(`.*${value}.*`, 'i') }, - { primaryEmail: new RegExp(`.*${value}.*`, 'i') }, - { primaryPhone: new RegExp(`.*${value}.*`, 'i') }, - { emails: { $in: [new RegExp(`.*${value}.*`, 'i')] } }, - { phones: { $in: [new RegExp(`.*${value}.*`, 'i')] } }, - { 'visitorContactInfo.email': new RegExp(`.*${value}.*`, 'i') }, - { 'visitorContactInfo.phone': new RegExp(`.*${value}.*`, 'i') }, - ]; - - return { $or: fields }; - } - - // filter by id - public idsFilter(ids: string[]): IIdsFilter { - return { _id: { $in: ids } }; + this.positiveList.push({ + terms: { + relatedIntegrationIds: integrations.map(i => i._id), + }, + }); } - // filter by leadStatus - public leadStatusFilter(leadStatus: string): { leadStatus: string } { - return { leadStatus }; - } + // filter by integration kind + public async integrationTypeFilter(kind: string): Promise { + const integrations = await Integrations.findIntegrations({ kind }); - // filter by lifecycleState - public lifecycleStateFilter(lifecycleState: string): { lifecycleState: string } { - return { lifecycleState }; + this.positiveList.push({ + terms: { + relatedIntegrationIds: integrations.map(i => i._id), + }, + }); } // filter by form - public async formFilter(formId: string, startDate?: string, endDate?: string): Promise { - const formObj = await Forms.findOne({ _id: formId }); - const { submissions = [] } = formObj || {}; + public async formFilter(formId: string, startDate?: string, endDate?: string): Promise { + const submissions = await FormSubmissions.find({ formId }); const ids: string[] = []; for (const submission of submissions) { const { customerId, submittedAt } = submission; - // Collecting customerIds inbetween dates only - if (startDate && endDate && !ids.includes(customerId)) { - if (moment(submittedAt).isBetween(startDate, endDate)) { + if (customerId) { + // Collecting customerIds inbetween dates only + if (startDate && endDate && !ids.includes(customerId)) { + if (moment(submittedAt).isBetween(startDate, endDate)) { + ids.push(customerId); + } + + // If date is not specified collecting all customers + } else { ids.push(customerId); } - - // If date is not specified collecting all customers - } else { - ids.push(customerId); } } - return { _id: { $in: ids } }; + this.positiveList.push({ + terms: { + _id: ids, + }, + }); + } + + public async findAllMongo(limit: number) { + const activeIntegrations = await Integrations.findIntegrations({}, { _id: 1 }); + + const selector = { + ...this.context.commonQuerySelector, + status: { $ne: 'deleted' }, + state: this.params.type || 'customer', + $or: [ + { + integrationId: { $in: [null, undefined, ''] }, + }, + { integrationId: { $in: activeIntegrations.map(integration => integration._id) } }, + ], + }; + + const customers = await Customers.find(selector) + .sort({ createdAt: -1 }) + .limit(limit); + + const count = await Customers.find(selector).countDocuments(); + + return { + list: customers, + totalCount: count, + }; } + /* * prepare all queries. do not do any action */ public async buildAllQueries(): Promise { - this.queries = { - default: this.defaultFilters(), - type: {}, - segment: {}, - tag: {}, - ids: {}, - searchValue: {}, - brand: {}, - integration: {}, - form: {}, - integrationType: {}, - }; - - // filter by type - if (this.params.type) { - this.queries.type = { isUser: this.params.type === 'user' ? true : { $ne: true } }; - } - - // filter by segment - if (this.params.segment) { - this.queries.segment = await this.segmentFilter(this.params.segment); - } - - // filter by tag - if (this.params.tag) { - this.queries.tag = this.tagFilter(this.params.tag); - } + await super.buildAllQueries(); // filter by brand if (this.params.brand) { - this.queries.brand = await this.brandFilter(this.params.brand); + await this.brandFilter(this.params.brand); } // filter by integration kind if (this.params.integrationType) { - this.queries.integrationType = await this.integrationTypeFilter(this.params.integrationType); - } - - // filter by form - if (this.params.form) { - this.queries.form = await this.formFilter(this.params.form); - - if (this.params.startDate && this.params.endDate) { - this.queries.form = await this.formFilter(this.params.form, this.params.startDate, this.params.endDate); - } - } - - /* If there are ids and form params, returning ids filter only - * filter by ids - */ - if (this.params.ids) { - this.queries.ids = this.idsFilter(this.params.ids); + await this.integrationTypeFilter(this.params.integrationType); } // filter by integration if (this.params.integration) { - this.queries.integration = await this.integrationFilter(this.params.integration); - } - - // filter by search value - if (this.params.searchValue) { - this.queries.searchValue = this.searchFilter(this.params.searchValue); - } - - // filter by leadStatus - if (this.params.leadStatus) { - this.queries.leadStatus = this.leadStatusFilter(this.params.leadStatus); + await this.integrationFilter(this.params.integration); } - // filter by lifecycleState - if (this.params.lifecycleState) { - this.queries.lifecycleState = this.lifecycleStateFilter(this.params.lifecycleState); + // filter by form + if (this.params.form) { + if (this.params.startDate && this.params.endDate) { + await this.formFilter(this.params.form, this.params.startDate, this.params.endDate); + } else { + await this.formFilter(this.params.form); + } } } - - public mainQuery(): any { - return { - ...this.queries.default, - ...this.queries.type, - ...this.queries.segment, - ...this.queries.tag, - ...this.queries.segment, - ...this.queries.brand, - ...this.queries.integrationType, - ...this.queries.form, - ...this.queries.ids, - ...this.queries.integration, - ...this.queries.searchValue, - ...this.queries.leadStatus, - ...this.queries.lifecycleState, - }; - } } diff --git a/src/data/modules/coc/exporter.ts b/src/data/modules/coc/exporter.ts deleted file mode 100644 index da23fdbae..000000000 --- a/src/data/modules/coc/exporter.ts +++ /dev/null @@ -1,117 +0,0 @@ -import * as moment from 'moment'; -import { Companies, Customers, Fields } from '../../../db/models'; -import { ICompanyDocument } from '../../../db/models/definitions/companies'; -import { ICustomerDocument } from '../../../db/models/definitions/customers'; -import { COMPANY_BASIC_INFOS, CUSTOMER_BASIC_INFOS } from '../../constants'; -import { createXlsFile, generateXlsx, paginate } from '../../utils'; -import { - filter as companiesFilter, - IListArgs as ICompanyListArgs, - sortBuilder as companiesSortBuilder, -} from './companies'; - -import { IUserDocument } from '../../../db/models/definitions/users'; -import { can } from '../../permissions/utils'; -import { - Builder as BuildQuery, - IListArgs as ICustomerListArgs, - sortBuilder as customersSortBuilder, -} from './customers'; - -type TDocs = ICustomerDocument | ICompanyDocument; - -/** - * Export customers or companies - */ -const cocsExport = async (cocs: TDocs[], cocType: string): Promise<{ name: string; response: string }> => { - let basicInfos = CUSTOMER_BASIC_INFOS; - - if (cocType === 'company') { - basicInfos = COMPANY_BASIC_INFOS; - } - - // Reads default template - const { workbook, sheet } = await createXlsFile(); - - const cols: string[] = []; - let rowIndex: number = 1; - - const addCell = (col: string, value: TDocs): void => { - // Checking if existing column - if (cols.includes(col)) { - // If column already exists adding cell - sheet.cell(rowIndex, cols.indexOf(col) + 1).value(value); - } else { - // Creating column - sheet.cell(1, cols.length + 1).value(col); - // Creating cell - sheet.cell(rowIndex, cols.length + 1).value(value); - - cols.push(col); - } - }; - - for (const coc of cocs) { - rowIndex++; - - // Iterating through coc basic infos - for (const info of basicInfos) { - if (coc[info] && coc[info] !== '') { - addCell(info, coc[info]); - } - } - - // Iterating through coc custom properties - if (coc.customFieldsData) { - for (const fieldId of coc.customFieldsData) { - const propertyObj = await Fields.findOne({ _id: fieldId }); - - if (propertyObj && propertyObj.text) { - const { text } = propertyObj; - addCell(text, coc.customFieldsData[fieldId]); - } - } - } - } - - const name = `${cocType} - ${moment().format('YYYY-MM-DD HH:mm')}`; - - return { - name, - response: await generateXlsx(workbook), - }; -}; - -/** - * Export companies to xls file - */ -export const companiesExport = async (params: ICompanyListArgs, user: IUserDocument) => { - if (!(await can('exportCompanies', user))) { - throw new Error('Permission denied'); - } - - const selector = await companiesFilter(params); - const sort = companiesSortBuilder(params); - const companies = await paginate(Companies.find(selector), params).sort(sort); - - return cocsExport(companies, 'company'); -}; - -/** - * Export customers to xls file - */ -export const customersExport = async (params: ICustomerListArgs, user: IUserDocument) => { - if (!(await can('exportCustomers', user))) { - throw new Error('Permission denied'); - } - - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - const sort = customersSortBuilder(params); - - const customers = await Customers.find(qb.mainQuery()).sort(sort); - - return cocsExport(customers, 'customer'); -}; diff --git a/src/data/modules/coc/utils.ts b/src/data/modules/coc/utils.ts new file mode 100644 index 000000000..d9b3ceb0e --- /dev/null +++ b/src/data/modules/coc/utils.ts @@ -0,0 +1,359 @@ +import * as _ from 'underscore'; +import { Brands, Conformities, Segments, Tags } from '../../../db/models'; +import { companySchema } from '../../../db/models/definitions/companies'; +import { KIND_CHOICES } from '../../../db/models/definitions/constants'; +import { customerSchema } from '../../../db/models/definitions/customers'; +import { debugBase } from '../../../debuggers'; +import { fetchElk } from '../../../elasticsearch'; +import { COC_LEAD_STATUS_TYPES } from '../../constants'; +import { fetchBySegments } from '../segments/queryBuilder'; + +export interface ICountBy { + [index: string]: number; +} + +export const getEsTypes = (contentType: string) => { + const schema = ['company', 'companies'].includes(contentType) ? companySchema : customerSchema; + + const typesMap: { [key: string]: any } = {}; + + schema.eachPath(name => { + const path = schema.paths[name]; + typesMap[name] = path.options.esType; + }); + + return typesMap; +}; + +export const countBySegment = async (contentType: string, qb): Promise => { + const counts: ICountBy = {}; + + // Count customers by segments + const segments = await Segments.find({ contentType }); + + // Count customers by segment + for (const s of segments) { + try { + await qb.buildAllQueries(); + await qb.segmentFilter(s._id); + counts[s._id] = await qb.runQueries('count'); + } catch (e) { + debugBase(`Error during segment count ${e.message}`); + counts[s._id] = 0; + } + } + + return counts; +}; + +export const countByBrand = async (qb): Promise => { + const counts: ICountBy = {}; + + // Count customers by brand + const brands = await Brands.find({}); + + for (const brand of brands) { + await qb.buildAllQueries(); + await qb.brandFilter(brand._id); + + counts[brand._id] = await qb.runQueries('count'); + } + + return counts; +}; + +export const countByTag = async (type: string, qb): Promise => { + const counts: ICountBy = {}; + + // Count customers by tag + const tags = await Tags.find({ type }).select('_id'); + + for (const tag of tags) { + await qb.buildAllQueries(); + await qb.tagFilter(tag._id); + + counts[tag._id] = await qb.runQueries('count'); + } + + return counts; +}; + +export const countByLeadStatus = async (qb): Promise => { + const counts: ICountBy = {}; + + for (const type of COC_LEAD_STATUS_TYPES) { + await qb.buildAllQueries(); + qb.leadStatusFilter(type); + + counts[type] = await qb.runQueries('count'); + } + + return counts; +}; + +export const countByIntegrationType = async (qb): Promise => { + const counts: ICountBy = {}; + + for (const type of KIND_CHOICES.ALL) { + await qb.buildAllQueries(); + await qb.integrationTypeFilter(type); + + counts[type] = await qb.runQueries('count'); + } + + return counts; +}; + +interface ICommonListArgs { + page?: number; + perPage?: number; + sortField?: string; + sortDirection?: number; + segment?: string; + tag?: string; + ids?: string[]; + searchValue?: string; + autoCompletion?: boolean; + autoCompletionType?: string; + brand?: string; + leadStatus?: string; + conformityMainType?: string; + conformityMainTypeId?: string; + conformityIsRelated?: boolean; + conformityIsSaved?: boolean; +} + +export class CommonBuilder { + public params: IListArgs; + public context; + public positiveList: any[]; + public negativeList: any[]; + + private contentType: 'customers' | 'companies'; + + constructor(contentType: 'customers' | 'companies', params: IListArgs, context) { + this.contentType = contentType; + this.context = context; + this.params = params; + + this.positiveList = []; + this.negativeList = []; + + this.resetPositiveList(); + } + + public resetPositiveList() { + this.positiveList = []; + + if (this.context.commonQuerySelectorElk) { + this.positiveList.push(this.context.commonQuerySelectorElk); + } + } + + // filter by segment + public async segmentFilter(segmentId: string) { + const segment = await Segments.getSegment(segmentId); + + const { positiveList, negativeList } = await fetchBySegments(segment, 'count'); + + this.positiveList = [...this.positiveList, ...positiveList]; + this.negativeList = [...this.negativeList, ...negativeList]; + } + + // filter by tagId + public tagFilter(tagId: string) { + this.positiveList.push({ + terms: { + tagIds: [tagId], + }, + }); + } + + // filter by search value + public searchFilter(value: string): void { + this.positiveList.push({ + wildcard: { + searchText: `*${value.toLowerCase()}*`, + }, + }); + } + + // filter by auto-completion type + public searchByAutoCompletionType(value: string, type: string): void { + this.positiveList.push({ + wildcard: { + [type]: `*${value}*`, + }, + }); + } + + // filter by id + public idsFilter(ids: string[]): void { + this.positiveList.push({ + terms: { + _id: ids, + }, + }); + } + + // filter by leadStatus + public leadStatusFilter(leadStatus: string): void { + this.positiveList.push({ + term: { + leadStatus, + }, + }); + } + + public async conformityFilter() { + const { conformityMainType, conformityMainTypeId, conformityIsRelated, conformityIsSaved } = this.params; + + if (!conformityMainType && !conformityMainTypeId) { + return; + } + + const relType = this.contentType === 'customers' ? 'customer' : 'company'; + + if (conformityIsRelated) { + const relTypeIds = await Conformities.relatedConformity({ + mainType: conformityMainType || '', + mainTypeId: conformityMainTypeId || '', + relType, + }); + + this.positiveList.push({ + terms: { + _id: relTypeIds || [], + }, + }); + } + + if (conformityIsSaved) { + const relTypeIds = await Conformities.savedConformity({ + mainType: conformityMainType || '', + mainTypeId: conformityMainTypeId || '', + relTypes: [relType], + }); + + this.positiveList.push({ + terms: { + _id: relTypeIds || [], + }, + }); + } + } + + /* + * prepare all queries. do not do any action + */ + public async buildAllQueries(): Promise { + this.resetPositiveList(); + this.negativeList = []; + + // filter by segment + if (this.params.segment) { + await this.segmentFilter(this.params.segment); + } + + // filter by tag + if (this.params.tag) { + this.tagFilter(this.params.tag); + } + + // filter by leadStatus + if (this.params.leadStatus) { + this.leadStatusFilter(this.params.leadStatus); + } + + // If there are ids and form params, returning ids filter only filter by ids + if (this.params.ids) { + this.idsFilter(this.params.ids.filter(id => id)); + } + + // filter by search value + if (this.params.searchValue) { + this.params.autoCompletion + ? this.searchByAutoCompletionType(this.params.searchValue, this.params.autoCompletionType || '') + : this.searchFilter(this.params.searchValue); + } + + await this.conformityFilter(); + } + + public async findAllMongo(_limit: number): Promise { + return Promise.resolve({ + list: [], + totalCount: 0, + }); + } + + /* + * Run queries + */ + public async runQueries(action = 'search', isExport?: boolean): Promise { + const { page = 0, perPage = 0, sortField, sortDirection } = this.params; + const paramKeys = Object.keys(this.params).join(','); + + const _page = Number(page || 1); + let _limit = Number(perPage || 20); + + if (isExport) { + _limit = 10000; + } + + if ( + !isExport && + page === 1 && + perPage === 20 && + (paramKeys === 'page,perPage' || paramKeys === 'page,perPage,type') + ) { + return this.findAllMongo(_limit); + } + + const queryOptions: any = { + query: { + bool: { + must: this.positiveList, + must_not: this.negativeList, + }, + }, + }; + + if (action === 'search') { + queryOptions.from = (_page - 1) * _limit; + queryOptions.size = _limit; + + const esTypes = getEsTypes(this.contentType); + + let fieldToSort = sortField || 'createdAt'; + + if (!esTypes[fieldToSort] || esTypes[fieldToSort] === 'email') { + fieldToSort = `${fieldToSort}.keyword`; + } + + queryOptions.sort = { + [fieldToSort]: { + order: sortDirection ? (sortDirection === -1 ? 'desc' : 'asc') : 'desc', + }, + }; + } + + const response = await fetchElk(action, this.contentType, queryOptions); + + if (action === 'count') { + return response.count; + } + + const list = response.hits.hits.map(hit => { + return { + _id: hit._id, + ...hit._source, + }; + }); + + return { + list, + totalCount: response.hits.total.value, + }; + } +} diff --git a/src/data/modules/fields/utils.ts b/src/data/modules/fields/utils.ts new file mode 100644 index 000000000..7c51d0ced --- /dev/null +++ b/src/data/modules/fields/utils.ts @@ -0,0 +1,300 @@ +import { Companies, Customers, Fields, FieldsGroups, Integrations, Products } from '../../../db/models'; +import { fetchElk } from '../../../elasticsearch'; +import { EXTEND_FIELDS, FIELD_CONTENT_TYPES } from '../../constants'; +import { BOARD_BASIC_INFOS } from '../fileExporter/constants'; + +const generateBasicInfosFromSchema = async (queSchema: any, namePrefix: string) => { + const queFields: string[] = []; + + // field definations + const paths = queSchema.paths; + + for (const name of Object.keys(paths)) { + const path = paths[name]; + + const label = path.options.label; + const type = path.instance; + + if (['String', 'Number', 'Date', 'Boolean'].includes(type) && label) { + // add to fields list + queFields.push(`${namePrefix}${name}`); + } + } + + return queFields; +}; + +// Checking field names, all field names must be configured correctly +export const checkFieldNames = async (type: string, fields: string[]) => { + const properties: any[] = []; + let schema: any; + let basicInfos: string[] = []; + + switch (type) { + case 'company': + schema = Companies.schema; + break; + + case 'customer': + schema = Customers.schema; + break; + + case 'lead': + schema = Customers.schema; + break; + + case 'product': + schema = Products.schema; + break; + + case 'deal': + case 'task': + case 'ticket': + basicInfos = BOARD_BASIC_INFOS; + break; + } + + if (schema) { + basicInfos = [...basicInfos, ...(await generateBasicInfosFromSchema(schema, ''))]; + + for (const name of Object.keys(schema.paths)) { + const path = schema.paths[name]; + + // extend fields list using sub schema fields + if (path.schema) { + basicInfos = [...basicInfos, ...(await generateBasicInfosFromSchema(path.schema, `${name}.`))]; + } + } + } + + for (let fieldName of fields) { + if (!fieldName) { + continue; + } + + fieldName = fieldName.trim(); + + const property: { [key: string]: any } = {}; + + const fieldObj = await Fields.findOne({ text: fieldName, contentType: type === 'lead' ? 'customer' : type }); + + // Collecting basic fields + if (basicInfos.includes(fieldName)) { + property.name = fieldName; + property.type = 'basic'; + } + + // Collecting custom fields + if (fieldObj) { + property.type = 'customProperty'; + property.id = fieldObj._id; + } + + if (fieldName === 'companiesPrimaryNames') { + property.name = 'companyIds'; + property.type = 'companiesPrimaryNames'; + } + + if (fieldName === 'customersPrimaryEmails') { + property.name = 'customerIds'; + property.type = 'customersPrimaryEmails'; + } + + if (fieldName === 'ownerEmail') { + property.name = 'ownerId'; + property.type = 'ownerEmail'; + } + + if (fieldName === 'tag') { + property.name = 'tagIds'; + property.type = 'tag'; + } + + if (fieldName === 'boardName') { + property.name = 'boardId'; + property.type = 'boardName'; + } + + if (fieldName === 'pipelineName') { + property.name = 'pipelineId'; + property.type = 'pipelineName'; + } + + if (fieldName === 'stageName') { + property.name = 'stageId'; + property.type = 'stageName'; + } + + if (fieldName === 'pronoun') { + property.name = 'pronoun'; + property.type = 'pronoun'; + } + + if (!property.type) { + throw new Error(`Bad column name ${fieldName}`); + } + + properties.push(property); + } + + return properties; +}; + +const getIntegrations = async () => { + return Integrations.aggregate([ + { + $project: { + _id: 0, + label: '$name', + value: '$_id', + }, + }, + ]); +}; + +/* + * Generates fields using given schema + */ +const generateFieldsFromSchema = async (queSchema: any, namePrefix: string) => { + const queFields: any = []; + + // field definations + const paths = queSchema.paths; + + const integrations = await getIntegrations(); + + for (const name of Object.keys(paths)) { + const path = paths[name]; + + const label = path.options.label; + const type = path.instance; + const selectOptions = name === 'integrationId' ? integrations || [] : path.options.selectOptions; + + if (['String', 'Number', 'Date', 'Boolean'].includes(type) && label) { + // add to fields list + queFields.push({ + _id: Math.random(), + name: `${namePrefix}${name}`, + label, + type: path.instance, + selectOptions, + }); + } + } + + return queFields; +}; + +/** + * Generates all field choices base on given kind. + */ +export const fieldsCombinedByContentType = async ({ + contentType, + usageType, + excludedNames, +}: { + contentType: string; + usageType?: string; + excludedNames?: string[]; +}) => { + let schema: any; + let extendFields: Array<{ name: string; label?: string }> = []; + let fields: Array<{ _id: number; name: string; label?: string }> = []; + + switch (contentType) { + case FIELD_CONTENT_TYPES.COMPANY: + schema = Companies.schema; + break; + + case FIELD_CONTENT_TYPES.PRODUCT: + schema = Products.schema; + extendFields = EXTEND_FIELDS.PRODUCT; + + break; + + case FIELD_CONTENT_TYPES.CUSTOMER: + schema = Customers.schema; + break; + } + + // generate list using customer or company schema + fields = [...fields, ...(await generateFieldsFromSchema(schema, ''))]; + + for (const name of Object.keys(schema.paths)) { + const path = schema.paths[name]; + + // extend fields list using sub schema fields + if (path.schema) { + fields = [...fields, ...(await generateFieldsFromSchema(path.schema, `${name}.`))]; + } + } + + const customFields = await Fields.find({ + contentType, + }); + + // extend fields list using custom fields data + for (const customField of customFields) { + const group = await FieldsGroups.findOne({ _id: customField.groupId }); + + if (group && group.isVisible && customField.isVisible) { + fields.push({ + _id: Math.random(), + name: `customFieldsData.${customField._id}`, + label: customField.text, + }); + } + } + + if (contentType === 'customer' && usageType) { + extendFields = EXTEND_FIELDS.CUSTOMER; + } + + for (const extendFeild of extendFields) { + fields.push({ + _id: Math.random(), + ...extendFeild, + }); + } + + if ((contentType === 'company' || contentType === 'customer') && (!usageType || usageType === 'export')) { + const aggre = await fetchElk( + 'search', + contentType === 'company' ? 'companies' : 'customers', + { + size: 0, + _source: false, + aggs: { + trackedDataKeys: { + nested: { + path: 'trackedData', + }, + aggs: { + fieldKeys: { + terms: { + field: 'trackedData.field', + size: 10000, + }, + }, + }, + }, + }, + }, + '', + { aggregations: { trackedDataKeys: {} } }, + ); + + const aggregations = aggre.aggregations || { trackedDataKeys: {} }; + const buckets = (aggregations.trackedDataKeys.fieldKeys || { buckets: [] }).buckets; + + for (const bucket of buckets) { + fields.push({ + _id: Math.random(), + name: `trackedData.${bucket.key}`, + label: bucket.key, + }); + } + } + + return fields.filter(field => !(excludedNames || []).includes(field.name)); +}; diff --git a/src/data/modules/fileExporter/constants.ts b/src/data/modules/fileExporter/constants.ts new file mode 100644 index 000000000..a88084686 --- /dev/null +++ b/src/data/modules/fileExporter/constants.ts @@ -0,0 +1,82 @@ +export const BOARD_BASIC_INFOS = [ + 'userId', + 'createdAt', + 'order', + 'name', + 'closeDate', + 'reminderMinute', + 'isComplete', + 'description', + 'assignedUsers', + 'watchedUserIds', + 'labelIds', + 'stageId', + 'initialStageId', + 'modifiedAt', + 'modifiedBy', + 'priority', +]; + +export const USER_BASIC_INFOS = [ + 'username', + 'isOwner', + 'email', + 'getNotificationByEmail', + 'isActive', + 'brandIds', + 'groupIds', + 'doNotDisturb', +]; + +export const BRAND_BASIC_INFOS = ['code', 'name', 'description', 'userId', 'createdAt']; + +export const CHANNEL_BASIC_INFOS = [ + 'createdAt', + 'name', + 'description', + 'conversationCount', + 'openConversationCount', + 'userId', + 'integrationIds', + 'memberIds', +]; + +export const PERMISSION_BASIC_INFOS = ['module', 'action', 'userId', 'groupId', 'requiredActions', 'allowed']; + +export const CUSTOMER_BASIC_INFOS = [ + 'state', + 'firstName', + 'lastName', + 'primaryEmail', + 'emails', + 'primaryPhone', + 'phones', + 'ownerId', + 'position', + 'department', + 'leadStatus', + 'status', + 'hasAuthority', + 'description', + 'doNotDisturb', + 'integrationId', + 'code', + 'mergedIds', +]; + +export const COMPANY_BASIC_INFOS = [ + 'primaryName', + 'names', + 'size', + 'industry', + 'website', + 'plan', + 'primaryEmail', + 'primaryPhone', + 'businessType', + 'description', + 'doNotDisturb', + 'parentCompanyId', +]; + +export const PRODUCT_BASIC_INFOS = ['name', 'categoryCode', 'type', 'description', 'sku', 'code', 'unitPrice']; diff --git a/src/data/modules/fileExporter/exporter.ts b/src/data/modules/fileExporter/exporter.ts new file mode 100644 index 000000000..1ef9d98ec --- /dev/null +++ b/src/data/modules/fileExporter/exporter.ts @@ -0,0 +1,330 @@ +import * as moment from 'moment'; +import { + Brands, + Channels, + ConversationMessages, + Deals, + Fields, + FormSubmissions, + Permissions, + Tasks, + Tickets, + Users, +} from '../../../db/models'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { debugBase } from '../../../debuggers'; +import { MODULE_NAMES } from '../../constants'; +import { can } from '../../permissions/utils'; +import { createXlsFile, generateXlsx } from '../../utils'; +import { Builder as CompanyBuildQuery, IListArgs as ICompanyListArgs } from '../coc/companies'; +import { Builder as CustomerBuildQuery, IListArgs as ICustomerListArgs } from '../coc/customers'; +import { fillCellValue, fillHeaders, IColumnLabel } from './spreadsheet'; + +// Prepares data depending on module type +const prepareData = async (query: any, user: IUserDocument): Promise => { + const { type, fromHistory } = query; + + let data: any[] = []; + + const isExport = fromHistory ? true : false; + + switch (type) { + case MODULE_NAMES.COMPANY: + if (!(await can('exportCompanies', user))) { + throw new Error('Permission denied'); + } + + const companyParams: ICompanyListArgs = query; + + const companyQb = new CompanyBuildQuery(companyParams, {}); + await companyQb.buildAllQueries(); + + const companyResponse = await companyQb.runQueries('search', isExport); + + data = companyResponse.list; + + break; + + case 'lead': + const leadParams: ICustomerListArgs = query; + const leadQp = new CustomerBuildQuery(leadParams, {}); + await leadQp.buildAllQueries(); + + const leadResponse = await leadQp.runQueries('search', isExport); + + data = leadResponse.list; + break; + case MODULE_NAMES.CUSTOMER: + if (!(await can('exportCustomers', user))) { + throw new Error('Permission denied'); + } + + const customerParams: ICustomerListArgs = query; + + if (customerParams.form && customerParams.popupData) { + debugBase('Start an query for popups export'); + + const fields = await Fields.find({ contentType: 'form', contentTypeId: customerParams.form }); + + if (fields.length === 0) { + return []; + } + + const messageQuery: any = { + 'formWidgetData._id': { $in: fields.map(field => field._id) }, + customerId: { $exists: true }, + }; + + const messages = await ConversationMessages.find(messageQuery, { + formWidgetData: 1, + customerId: 1, + createdAt: 1, + }); + + const messagesMap: { [key: string]: any[] } = {}; + + for (const message of messages) { + const customerId = message.customerId || ''; + + if (!messagesMap[customerId]) { + messagesMap[customerId] = []; + } + + messagesMap[customerId].push({ + datas: message.formWidgetData, + createdInfo: { + _id: 'created', + type: 'input', + validation: 'date', + text: 'Created', + value: message.createdAt, + }, + }); + } + + const uniqueCustomerIds = await FormSubmissions.find( + { formId: customerParams.form }, + { customerId: 1, submittedAt: 1 }, + ) + .sort({ + submittedAt: -1, + }) + .distinct('customerId'); + + const formDatas: any[] = []; + + for (const customerId of uniqueCustomerIds) { + const filteredMessages = messagesMap[customerId] || []; + + for (const { datas, createdInfo } of filteredMessages) { + const formData: any[] = datas; + + formData.push(createdInfo); + + formDatas.push(formData); + } + } + + debugBase('End an query for popups export'); + + data = formDatas; + } else { + const qb = new CustomerBuildQuery(customerParams, {}); + await qb.buildAllQueries(); + + const customerResponse = await qb.runQueries('search', isExport); + + data = customerResponse.list; + } + + break; + case MODULE_NAMES.DEAL: + if (!(await can('exportDeals', user))) { + throw new Error('Permission denied'); + } + + data = await Deals.find(); + + break; + case MODULE_NAMES.TASK: + if (!(await can('exportTasks', user))) { + throw new Error('Permission denied'); + } + + data = await Tasks.find(); + + break; + case MODULE_NAMES.TICKET: + if (!(await can('exportTickets', user))) { + throw new Error('Permission denied'); + } + + data = await Tickets.find(); + + break; + case MODULE_NAMES.USER: + if (!(await can('exportUsers', user))) { + throw new Error('Permission denied'); + } + + data = await Users.find({ isActive: true }); + + break; + case MODULE_NAMES.PERMISSION: + if (!(await can('exportPermissions', user))) { + throw new Error('Permission denied'); + } + + data = await Permissions.find(); + + break; + case MODULE_NAMES.BRAND: + if (!(await can('exportBrands', user))) { + throw new Error('Permission denied'); + } + + data = await Brands.find(); + + break; + case MODULE_NAMES.CHANNEL: + if (!(await can('exportChannels', user))) { + throw new Error('Permission denied'); + } + + data = await Channels.find(); + + break; + default: + break; + } + + return data; +}; + +const addCell = (col: IColumnLabel, value: string, sheet: any, columnNames: string[], rowIndex: number): void => { + // Checking if existing column + if (columnNames.includes(col.name)) { + // If column already exists adding cell + sheet.cell(rowIndex, columnNames.indexOf(col.name) + 1).value(value); + } else { + // Creating column + sheet.cell(1, columnNames.length + 1).value(col.label || col.name); + // Creating cell + sheet.cell(rowIndex, columnNames.length + 1).value(value); + + columnNames.push(col.name); + } +}; + +const fillLeadHeaders = async (formId: string) => { + const headers: IColumnLabel[] = []; + + const fields = await Fields.find({ contentType: 'form', contentTypeId: formId }).sort({ order: 1 }); + + for (const field of fields) { + headers.push({ name: field._id, label: field.text }); + } + + headers.push({ name: 'created', label: 'Created' }); + + return headers; +}; + +const buildLeadFile = async (datas: any, formId: string, sheet: any, columnNames: string[], rowIndex: number) => { + debugBase(`Start building an excel file for popups export`); + + const headers: IColumnLabel[] = await fillLeadHeaders(formId); + + const displayValue = item => { + if (!item) { + return ''; + } + + if (item.validation === 'date') { + return moment(item.value).format('YYYY/MM/DD HH:mm'); + } + + return item.value; + }; + + for (const data of datas) { + rowIndex++; + // Iterating through basic info columns + for (const column of headers) { + const item = await data.find(obj => obj._id === column.name || obj.text.trim() === column.label.trim()); + + const cellValue = displayValue(item); + + addCell(column, cellValue, sheet, columnNames, rowIndex); + } + } + + debugBase('End building an excel file for popups export'); +}; + +export const buildFile = async (query: any, user: IUserDocument): Promise<{ name: string; response: string }> => { + const { configs } = query; + let type = query.type; + + const data = await prepareData(query, user); + + // Reads default template + const { workbook, sheet } = await createXlsFile(); + + const columnNames: string[] = []; + let rowIndex: number = 1; + + if (type === MODULE_NAMES.CUSTOMER && query.form && query.popupData) { + await buildLeadFile(data, query.form, sheet, columnNames, rowIndex); + + type = 'Pop-Ups'; + } else { + let headers: IColumnLabel[] = fillHeaders(type); + + if (configs) { + headers = JSON.parse(configs); + } + + for (const item of data) { + rowIndex++; + // Iterating through basic info columns + for (const column of headers) { + if (column.name.startsWith('customFieldsData')) { + if (item.customFieldsData && item.customFieldsData.length > 0) { + for (const customFeild of item.customFieldsData) { + const field = await Fields.findOne({ + text: column.label.trim(), + contentType: type === 'lead' ? 'customer' : type, + }); + + if (field && field.text) { + let value = customFeild.value; + + if (Array.isArray(value)) { + value = value.join(', '); + } + + if (field.validation === 'date') { + value = moment(value).format('YYYY-MM-DD HH:mm'); + } + + addCell({ name: field.text, label: field.text }, value, sheet, columnNames, rowIndex); + } + } + } + } else { + const cellValue = await fillCellValue(column.name, item); + + addCell(column, cellValue, sheet, columnNames, rowIndex); + } + } + + // customer or company checking + } // end items for loop + } + + return { + name: `${type} - ${moment().format('YYYY-MM-DD HH:mm')}`, + response: await generateXlsx(workbook), + }; +}; diff --git a/src/data/modules/fileExporter/spreadsheet.ts b/src/data/modules/fileExporter/spreadsheet.ts new file mode 100644 index 000000000..f1551ea2d --- /dev/null +++ b/src/data/modules/fileExporter/spreadsheet.ts @@ -0,0 +1,297 @@ +import * as moment from 'moment'; +import { commonItemFieldsSchema, IStageDocument } from '../../../db/models/definitions/boards'; +import { brandSchema, IBrandDocument } from '../../../db/models/definitions/brands'; +import { channelSchema } from '../../../db/models/definitions/channels'; +import { companySchema, ICompanyDocument } from '../../../db/models/definitions/companies'; +import { customerSchema, ICustomerDocument } from '../../../db/models/definitions/customers'; +import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; +import { IUserGroupDocument, permissionSchema } from '../../../db/models/definitions/permissions'; +import { IPipelineLabelDocument } from '../../../db/models/definitions/pipelineLabels'; +import { ITagDocument } from '../../../db/models/definitions/tags'; +import { ticketSchema } from '../../../db/models/definitions/tickets'; +import { IUserDocument, userSchema } from '../../../db/models/definitions/users'; +import { + Brands, + Companies, + Conformities, + Customers, + Integrations, + PipelineLabels, + Stages, + Tags, + Users, + UsersGroups, +} from '../../../db/models/index'; + +import { MODULE_NAMES } from '../../constants'; +import { + BOARD_BASIC_INFOS, + BRAND_BASIC_INFOS, + CHANNEL_BASIC_INFOS, + COMPANY_BASIC_INFOS, + CUSTOMER_BASIC_INFOS, + PERMISSION_BASIC_INFOS, + USER_BASIC_INFOS, +} from './constants'; + +export interface IColumnLabel { + name: string; + label: string; +} + +const findSchemaLabels = (schema: any, basicFields: string[]): IColumnLabel[] => { + const fields: IColumnLabel[] = []; + + for (const name of basicFields) { + const field = schema.obj ? schema.obj[name] : schema[name]; + + if (field && field.label) { + fields.push({ name, label: field.label }); + } else { + fields.push({ name, label: name }); + } + } + + return fields; +}; + +export const fillHeaders = (itemType: string): IColumnLabel[] => { + let columnNames: IColumnLabel[] = []; + + switch (itemType) { + case MODULE_NAMES.COMPANY: + columnNames = findSchemaLabels(companySchema, COMPANY_BASIC_INFOS); + break; + case MODULE_NAMES.CUSTOMER: + columnNames = findSchemaLabels(customerSchema, CUSTOMER_BASIC_INFOS); + break; + case MODULE_NAMES.DEAL: + case MODULE_NAMES.TASK: + columnNames = findSchemaLabels(commonItemFieldsSchema, BOARD_BASIC_INFOS); + break; + case MODULE_NAMES.TICKET: + columnNames = findSchemaLabels(ticketSchema, [...BOARD_BASIC_INFOS, 'source']); + break; + case MODULE_NAMES.USER: + columnNames = findSchemaLabels(userSchema, USER_BASIC_INFOS); + break; + case MODULE_NAMES.BRAND: + columnNames = findSchemaLabels(brandSchema, BRAND_BASIC_INFOS); + break; + case MODULE_NAMES.CHANNEL: + columnNames = findSchemaLabels(channelSchema, CHANNEL_BASIC_INFOS); + break; + case MODULE_NAMES.PERMISSION: + columnNames = findSchemaLabels(permissionSchema, PERMISSION_BASIC_INFOS); + break; + default: + break; + } + + return columnNames; +}; + +const getCompanyNames = async _id => { + const conformities = await Conformities.find({ mainTypeId: _id }); + const companyNames = [] as any; + + for (const conf of conformities) { + const company: ICompanyDocument | null = await Companies.findOne({ _id: conf.relTypeId }); + + if (company) { + companyNames.push(company.primaryName ? company.primaryName : 'unknown'); + } + } + + return companyNames; +}; + +const getCellValue = (item, colName) => { + const names = colName.split('.'); + + if (names.length === 1) { + return item[colName]; + } else if (names[0] === 'trackedData') { + const trackedDatas = item.trackedData || []; + + if (trackedDatas[0]) { + const foundedData = trackedDatas.find(data => data.field === names[1]); + return foundedData ? foundedData.value : ''; + } + + return ''; + } else { + const value = item[names[0]]; + + return value ? value[names[1]] : ''; + } +}; +/** + * Finds given field of database collection row and format it in a human-friendly way. + * @param {string} colName Database field name + * @param {any} item Database row + * @todo If same field names from different collections arrive, then this function will + * not find the from the proper collection. As for now, those field names are defined + * in distinctly defined static variables. + */ +export const fillCellValue = async (colName: string, item: any): Promise => { + const emptyMsg = '-'; + + if (!item) { + return emptyMsg; + } + + let cellValue: any = getCellValue(item, colName); + + if (typeof item[colName] === 'boolean') { + cellValue = item[colName] ? 'Yes' : 'No'; + } + + switch (colName) { + case 'createdAt': + case 'closeDate': + case 'modifiedAt': + cellValue = moment(cellValue).format('YYYY-MM-DD HH:mm'); + + break; + case 'userId': + const createdUser: IUserDocument | null = await Users.findOne({ _id: item.userId }); + + cellValue = createdUser ? createdUser.username : 'user not found'; + + break; + // deal, task, ticket fields + case 'assignedUserIds': + const assignedUsers: IUserDocument[] = await Users.find({ _id: { $in: item.assignedUserIds } }); + + cellValue = assignedUsers.map(user => user.username || user.email).join(', '); + + break; + case 'watchedUserIds': + const watchedUsers: IUserDocument[] = await Users.find({ _id: { $in: item.watchedUserIds } }); + + cellValue = watchedUsers.map(user => user.username || user.email).join(', '); + + break; + case 'labelIds': + const labels: IPipelineLabelDocument[] = await PipelineLabels.find({ _id: { $in: item.labelIds } }); + + cellValue = labels.map(label => label.name).join(', '); + + break; + case 'stageId': + const stage: IStageDocument | null = await Stages.findOne({ _id: item.stageId }); + + cellValue = stage ? stage.name : emptyMsg; + + break; + case 'initialStageId': + const initialStage: IStageDocument | null = await Stages.findOne({ _id: item.initialStageId }); + + cellValue = initialStage ? initialStage.name : emptyMsg; + + break; + case 'modifiedBy': + const modifiedBy: IUserDocument | null = await Users.findOne({ _id: item.modifiedBy }); + + cellValue = modifiedBy ? modifiedBy.username : emptyMsg; + + break; + + // user fields + case 'brandIds': + const brands: IBrandDocument[] = await Brands.find({ _id: item.brandIds }); + + cellValue = brands.map(brand => brand.name).join(', '); + + break; + case 'groupIds': + const groups: IUserGroupDocument[] = await UsersGroups.find({ _id: { $in: item.groupIds } }); + + cellValue = groups.map(g => g.name).join(', '); + + break; + + // channel fields + case 'integrationIds': + const integrations: IIntegrationDocument[] = await Integrations.find({ _id: { $in: item.integrationIds } }); + + cellValue = integrations.map(i => i.name).join(', '); + + break; + case 'memberIds': + const members: IUserDocument[] = await Users.find({ _id: { $in: item.memberIds } }); + + cellValue = members.map(m => m.username).join(', '); + + break; + + // permission fields + case 'groupId': + const group: IUserGroupDocument | null = await UsersGroups.findOne({ _id: item.groupId }); + + cellValue = group ? group.name : emptyMsg; + + break; + case 'requiredActions': + cellValue = (item.requiredActions || []).join(', '); + + break; + // customer fields + case 'integrationId': + const integration: IIntegrationDocument | null = await Integrations.findOne({ _id: item.integrationId }); + + cellValue = integration ? integration.name : emptyMsg; + + break; + case 'emails': + cellValue = (item.emails || []).join(', '); + break; + case 'phones': + cellValue = (item.phones || []).join(', '); + break; + case 'mergedIds': + const customers: ICustomerDocument[] | null = await Customers.find({ _id: { $in: item.mergedIds } }); + + cellValue = customers.map(cus => cus.firstName || cus.primaryEmail).join(', '); + + break; + // company fields + case 'names': + cellValue = (item.names || []).join(', '); + + break; + case 'parentCompanyId': + const parent: ICompanyDocument | null = await Companies.findOne({ _id: item.parentCompanyId }); + + cellValue = parent ? parent.primaryName : ''; + + break; + + case 'tag': + const tag: ITagDocument | null = await Tags.findOne({ _id: item.tagId }); + + cellValue = tag ? tag.name : ''; + + break; + + case 'companiesPrimaryNames': + const companyNames = await getCompanyNames(item._id); + + cellValue = companyNames.join(', '); + + break; + + case 'ownerEmail': + const owner: IUserDocument | null = await Users.findOne({ _id: item.ownerId }); + + cellValue = owner ? owner.email : ''; + + break; + + default: + break; + } + + return cellValue || emptyMsg; +}; diff --git a/src/data/modules/fileExporter/templateExport.ts b/src/data/modules/fileExporter/templateExport.ts new file mode 100644 index 000000000..b88b7c06e --- /dev/null +++ b/src/data/modules/fileExporter/templateExport.ts @@ -0,0 +1,36 @@ +import * as json2csv from 'json2csv'; +import { createXlsFile, generateXlsx } from '../../utils'; + +export const templateExport = async (args: any) => { + const { configs, type, importType } = args; + + if (importType === 'csv') { + const { Parser } = json2csv; + + const parser = new Parser({ fields: configs }); + const csv = parser.parse(''); + + return { + name: `${type}-import-template`, + response: csv, + }; + } + + const { workbook, sheet } = await createXlsFile(); + + let rowIndex: number = 1; + + const addCell = (value: string, index: number): void => { + sheet.cell(1, index).value(value === 'sex' ? 'pronoun' : value); + }; + + for (const config of configs) { + addCell(config, rowIndex); + rowIndex++; + } + + return { + name: `${type}-import-template`, + response: await generateXlsx(workbook), + }; +}; diff --git a/src/data/modules/insights/exportData.ts b/src/data/modules/insights/exportData.ts index a92bb8ef8..e57c6b9d5 100644 --- a/src/data/modules/insights/exportData.ts +++ b/src/data/modules/insights/exportData.ts @@ -251,7 +251,7 @@ export const generateTagReport = async (args: IListArgs, user: IUserDocument) => const tags = await Tags.find({ type: TAG_TYPES.CONVERSATION }).select('name'); - const integrationIds = await Integrations.find(filterSelector.integration).select('_id'); + const integrationIds = await Integrations.findIntegrations(filterSelector.integration).select('_id'); const rawIntegrationIds = integrationIds.map(row => row._id); diff --git a/src/data/modules/insights/exportUtils.ts b/src/data/modules/insights/exportUtils.ts index 82f3ca7ac..bb76f476f 100644 --- a/src/data/modules/insights/exportUtils.ts +++ b/src/data/modules/insights/exportUtils.ts @@ -81,7 +81,7 @@ export const addCell = (args: IAddCellArgs): void => { export const nextTime = (start: Date, type?: string) => { return new Date( moment(start) - .add(1, type ? 'hours' : 'days') + .add(1, type === 'volumeByTime' ? 'hours' : 'days') .toString(), ); }; diff --git a/src/data/modules/insights/insightExports.ts b/src/data/modules/insights/insightExports.ts index 5cb0d98a5..abae6c585 100644 --- a/src/data/modules/insights/insightExports.ts +++ b/src/data/modules/insights/insightExports.ts @@ -32,7 +32,9 @@ export const insightVolumeReportExport = async (args: IListArgs, user: IUserDocu // Reads default template const { workbook, sheet } = await createXlsFile(); - await addHeader(`Volume Report By ${type || 'date'}`, args, sheet); + const header = `Volume Report By ${type === 'volumeByTime' ? 'Time' : 'Date'}`; + + await addHeader(header, args, sheet); let rowIndex: number = 3; const cols: string[] = []; @@ -52,7 +54,7 @@ export const insightVolumeReportExport = async (args: IListArgs, user: IUserDocu } } - const name = `Volume report By ${type || 'date'} - ${dateToString(start)} - ${dateToString(end)}`; + const name = `${header} - ${dateToString(start)} - ${dateToString(end)}`; return { name, @@ -91,7 +93,7 @@ export const insightActivityReportExport = async (args: IListArgs, user: IUserDo const cols: string[] = []; const generateData = async () => { - const next = nextTime(begin, 'time'); + const next = nextTime(begin, 'volumeByTime'); rowIndex++; diff --git a/src/data/modules/insights/utils.ts b/src/data/modules/insights/utils.ts index d2890ebad..858786522 100644 --- a/src/data/modules/insights/utils.ts +++ b/src/data/modules/insights/utils.ts @@ -1,620 +1,620 @@ -import * as moment from 'moment'; -import * as _ from 'underscore'; -import { ConversationMessages, Conversations, Deals, Integrations, Pipelines, Stages, Users } from '../../../db/models'; -import { IStageDocument } from '../../../db/models/definitions/boards'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; -import { IUser } from '../../../db/models/definitions/users'; -import { INSIGHT_TYPES } from '../../constants'; -import { fixDate } from '../../utils'; -import { getDateFieldAsStr } from './aggregationUtils'; -import { - IDealListArgs, - IDealSelector, - IFilterSelector, - IFixDates, - IGenerateChartData, - IGenerateMessage, - IGeneratePunchCard, - IGenerateResponseData, - IGenerateTimeIntervals, - IGenerateUserChartData, - IListArgs, - IMessageSelector, - IResponseUserData, - IStageSelector, -} from './types'; - -/** - * Return filterSelector - * @param args - */ -export const getFilterSelector = (args: IListArgs): any => { - const selector: IFilterSelector = { integration: {} }; - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - if (integrationIds) { - selector.integration.kind = { $in: integrationIds.split(',') }; - } - - if (brandIds) { - selector.integration.brandId = { $in: brandIds.split(',') }; - } - - selector.createdAt = { $gte: start, $lte: end }; - - return selector; -}; - -/** - * Return filterSelector - * @param args - */ -export const getDealSelector = async (args: IDealListArgs): Promise => { - const { startDate, endDate, boardId, pipelineIds, status } = args; - const { start, end } = fixDates(startDate, endDate); - - const selector: IDealSelector = {}; - const date = { - $gte: start, - $lte: end, - }; - - // If status is either won or lost, modified date is more important - if (status) { - selector.modifiedAt = date; - } else { - selector.createdAt = date; - } - - const stageSelector: IStageSelector = {}; - - if (status) { - stageSelector.probability = status; - } - - let stages: IStageDocument[] = []; - - if (boardId) { - if (pipelineIds) { - stageSelector.pipelineId = { $in: pipelineIds.split(',') }; - } else { - const pipelines = await Pipelines.find({ boardId }); - stageSelector.pipelineId = { $in: pipelines.map(p => p._id) }; - } - - stages = await Stages.find(stageSelector); - selector.stageId = { $in: stages.map(s => s._id) }; - } else { - if (status) { - stages = await Stages.find(stageSelector); - selector.stageId = { $in: stages.map(s => s._id) }; - } - } - - return selector; -}; - -/** - * Return conversationSelect for aggregation - * @param args - * @param conversationSelector - * @param selectIds - */ -export const getConversationSelector = async ( - filterSelector: any, - conversationSelector: any = {}, - fieldName: string = 'createdAt', -): Promise => { - if (Object.keys(filterSelector.integration).length > 0) { - const integrationIds = await Integrations.find(filterSelector.integration).select('_id'); - conversationSelector.integrationId = { $in: integrationIds.map(row => row._id) }; - } - - if (!conversationSelector[fieldName]) { - conversationSelector[fieldName] = filterSelector.createdAt; - } - - return { ...conversationSelector, ...noConversationSelector }; -}; -/** - * - * @param summaries - * @param collection - * @param selector - */ -export const getSummaryData = async ({ - start, - end, - selector, - collection, - dateFieldName = 'createdAt', -}: { - start: Date; - end: Date; - selector: any; - collection: any; - dateFieldName?: string; -}): Promise => { - const intervals = generateTimeIntervals(start, end); - const summaries: Array<{ title?: string; count?: number }> = []; - - // finds a respective message counts for different time intervals. - for (const interval of intervals) { - const facetMessageSelector = { ...selector }; - - facetMessageSelector[dateFieldName] = { - $gte: interval.start.toDate(), - $lte: interval.end.toDate(), - }; - const [intervalCount] = await collection.aggregate([ - { - $match: facetMessageSelector, - }, - { - $group: { - _id: null, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - count: 1, - }, - }, - ]); - - summaries.push({ - title: interval.title, - count: intervalCount ? intervalCount.count : 0, - }); - } - - return summaries; -}; - -/** - * Builds messages find query selector. - */ -export const getMessageSelector = async ({ args, createdAt }: IGenerateMessage): Promise => { - const messageSelector: any = { - fromBot: { $exists: false }, - userId: args.type === 'response' ? { $ne: null } : null, - }; - - const filterSelector = getFilterSelector(args); - messageSelector.createdAt = filterSelector.createdAt; - - // While searching by integration - if (Object.keys(filterSelector.integration).length > 0) { - const selector = await getConversationSelector(filterSelector, { createdAt }); - - const conversationIds = await Conversations.find(selector).select('_id'); - - const rawConversationIds = conversationIds.map(obj => obj._id); - messageSelector.conversationId = { $in: rawConversationIds }; - } - - return messageSelector; -}; -/** - * Fix trend for missing values because from then aggregation, - * it could return missing values for some dates. This method - * will assign 0 values for missing x values. - * @param startDate - * @param endDate - * @param data - */ -export const fixChartData = async (data: any[], hintX: string, hintY: string): Promise => { - const results = {}; - data.map(row => { - results[row[hintX]] = row[hintY]; - }); - - return Object.keys(results) - .sort() - .map(key => { - return { x: moment(key).format('MM-DD'), y: results[key] }; - }); -}; - -/** - * Populates message collection into date range - * by given duration and loop count for chart data. - */ -export const generateChartDataBySelector = async ({ - selector, - type = INSIGHT_TYPES.CONVERSATION, - dateFieldName = '$createdAt', -}: { - selector: IMessageSelector; - type?: string; - dateFieldName?: string; -}): Promise => { - const pipelineStages = [ - { - $match: selector, - }, - { - $project: { - date: getDateFieldAsStr({ fieldName: dateFieldName }), - }, - }, - { - $group: { - _id: '$date', - y: { $sum: 1 }, - }, - }, - { - $project: { - x: '$_id', - y: 1, - _id: 0, - }, - }, - { - $sort: { - x: 1, - }, - }, - ]; - - if (type === INSIGHT_TYPES.DEAL) { - return Deals.aggregate([pipelineStages]); - } - - return ConversationMessages.aggregate([pipelineStages]); -}; - -export const generatePunchData = async ( - collection: any, - selector: object, - user: IUser, -): Promise => { - const pipelineStages = [ - { - $match: selector, - }, - { - $project: { - hour: { $hour: { date: '$createdAt', timezone: '+08' } }, - date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), - }, - }, - { - $group: { - _id: { - hour: '$hour', - date: '$date', - }, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - hour: '$_id.hour', - date: '$_id.date', - count: 1, - }, - }, - ]; - - return collection.aggregate(pipelineStages); -}; - -/** - * Populates message collection into date range - * by given duration and loop count for chart data. - */ - -export const generateChartDataByCollection = async (collection: any): Promise => { - const results = {}; - - collection.map(obj => { - const date = moment(obj.createdAt).format('YYYY-MM-DD'); - - results[date] = (results[date] || 0) + 1; - }); - - return Object.keys(results) - .sort() - .map(key => { - return { x: moment(key).format('MM-DD'), y: results[key] }; - }); -}; - -/** - * Generates time intervals for main report - */ -export const generateTimeIntervals = (start: Date, end: Date): IGenerateTimeIntervals[] => { - const month = moment(end).month(); - - return [ - { - title: 'In time range', - start: moment(start), - end: moment(end), - }, - { - title: 'This month', - start: moment(1, 'DD'), - end: moment(), - }, - { - title: 'This week', - start: moment(end).weekday(0), - end: moment(end), - }, - { - title: 'Today', - start: moment(end).add(-1, 'days'), - end: moment(end), - }, - { - title: 'Last 30 days', - start: moment(end).add(-30, 'days'), - end: moment(end), - }, - { - title: 'Last month', - start: moment(month + 1, 'MM').subtract(1, 'months'), - end: moment(month + 1, 'MM'), - }, - { - title: 'Last week', - start: moment(end).weekday(-7), - end: moment(end).weekday(0), - }, - { - title: 'Yesterday', - start: moment(end).add(-2, 'days'), - end: moment(end).add(-1, 'days'), - }, - ]; -}; - -/** - * Generate chart data for given user - */ -export const generateUserChartData = async ({ - userId, - userMessages, -}: { - userId: string; - userMessages: IMessageDocument[]; -}): Promise => { - const user = await Users.findOne({ _id: userId }); - const userData = await generateChartDataByCollection(userMessages); - - if (!user) { - return { - graph: userData, - }; - } - - const userDetail = user.details; - - return { - fullName: userDetail ? userDetail.fullName : '', - avatar: userDetail ? userDetail.avatar : '', - graph: userData, - }; -}; - -export const fixDates = (startValue: string, endValue: string, count?: number): IFixDates => { - // convert given value or get today - const endDate = fixDate(endValue); - - const startDateDefaultValue = new Date( - moment(endDate) - .add(count ? count * -1 : -7, 'days') - .toString(), - ); - - // convert given value or generate from endDate - const startDate = fixDate(startValue, startDateDefaultValue); - - return { start: startDate, end: endDate }; -}; - -export const getSummaryDates = (endValue: string): any => { - // convert given value or get today - const endDate = fixDate(endValue); - - const month = moment(endDate).month(); - const startDate = new Date( - moment(month + 1, 'MM') - .subtract(1, 'months') - .toString(), - ); - - return { $gte: startDate, $lte: endDate }; -}; - -/** - * Generate response chart data. - */ -export const generateResponseData = async ( - responseData: IMessageDocument[], - responseUserData: IResponseUserData, - allResponseTime: number, -): Promise => { - // preparing trend chart data - const trend = await generateChartDataByCollection(responseData); - - // Average response time for all messages - const time = Math.floor(allResponseTime / responseData.length); - - const teamMembers: any = []; - - const userIds = _.uniq(_.pluck(responseData, 'userId')); - - for (const userId of userIds) { - const { responseTime, count, summaries } = responseUserData[userId]; - - // Average response time for users. - const avgResTime = Math.floor(responseTime / count); - - // preparing each team member's chart data - teamMembers.push({ - data: await generateUserChartData({ - userId, - userMessages: responseData.filter(message => userId === message.userId), - }), - time: avgResTime, - summaries, - }); - } - - return { trend, time, teamMembers }; -}; - -export const getTimezone = (user: IUser): string => { - return (user.details ? user.details.location : '+08') || '+08'; -}; - -export const noConversationSelector = { - $or: [ - { userId: { $exists: true }, messageCount: { $gt: 1 } }, - { - userId: { $exists: false }, - $or: [ - { - closedAt: { $exists: true }, - closedUserId: { $exists: true }, - status: CONVERSATION_STATUSES.CLOSED, - }, - { - status: { $ne: CONVERSATION_STATUSES.CLOSED }, - }, - ], - }, - ], -}; - -export const timeIntervals: any[] = [ - { name: '0-5 second', count: 5 }, - { name: '6-10 second', count: 10 }, - { name: '11-15 second', count: 15 }, - { name: '16-20 second', count: 20 }, - { name: '21-25 second', count: 25 }, - { name: '26-30 second', count: 30 }, - { name: '31-35 second', count: 35 }, - { name: '36-40 second', count: 40 }, - { name: '41-45 second', count: 45 }, - { name: '46-50 second', count: 50 }, - { name: '51-55 second', count: 55 }, - { name: '56-60 second', count: 60 }, - { name: '1-2 min', count: 120 }, - { name: '2-3 min', count: 180 }, - { name: '3-4 min', count: 240 }, - { name: '4-5 min', count: 300 }, - { name: '5+ min' }, -]; - -export const timeIntervalBranches = () => { - const copyTimeIntervals = [...timeIntervals]; - copyTimeIntervals.pop(); - - return copyTimeIntervals.map(t => ({ - case: { $lte: ['$firstRespondTime', t.count] }, - then: t.name, - })); -}; - -/** - * Return conversationSelect for aggregation - * @param filterSelector - * @param conversationSelector - * @param messageSelector - */ -export const getConversationSelectorToMsg = async ( - integrationIds: string, - brandIds: string, - conversationSelector: any = {}, -): Promise => { - const filterSelector: IFilterSelector = { integration: {} }; - if (integrationIds) { - filterSelector.integration.kind = { $in: integrationIds.split(',') }; - } - - if (brandIds) { - filterSelector.integration.brandId = { $in: brandIds.split(',') }; - } - - if (Object.keys(filterSelector.integration).length > 0) { - const integrationIdsList = await Integrations.find(filterSelector.integration).select('_id'); - conversationSelector.integrationId = { $in: integrationIdsList.map(row => row._id) }; - } - return { ...conversationSelector }; -}; - -export const getConversationSelectorByMsg = async ( - integrationIds: string, - brandIds: string, - conversationSelector: any = {}, - messageSelector: any = {}, -): Promise => { - const conversationFinder = await getConversationSelectorToMsg(integrationIds, brandIds, conversationSelector); - const conversationIds = await Conversations.find(conversationFinder).select('_id'); - - const rawConversationIds = await conversationIds.map(obj => obj._id); - messageSelector.conversationId = { $in: rawConversationIds }; - - return { ...messageSelector }; -}; - -export const getConversationReportLookup = async (): Promise => { - return { - lookupPrevMsg: { - $lookup: { - from: 'conversation_messages', - let: { checkConversation: '$conversationId', checkAt: '$createdAt' }, - pipeline: [ - { - $match: { - $expr: { - $and: [{ $eq: ['$conversationId', '$$checkConversation'] }, { $lt: ['$createdAt', '$$checkAt'] }], - }, - }, - }, - { - $project: { - conversationId: 1, - createdAt: 1, - internal: 1, - userId: 1, - customerId: 1, - sizeMentionedIds: { $size: '$mentionedUserIds' }, - }, - }, - ], - as: 'prevMsgs', - }, - }, - prevMsgSlice: { - $addFields: { prevMsg: { $slice: ['$prevMsgs', -1] } }, - }, - diffSecondCalc: { - $addFields: { - diffSec: { - $divide: [{ $subtract: ['$createdAt', '$prevMsg.createdAt'] }, 1000], - }, - }, - }, - firstProject: { - $project: { - conversationId: 1, - createdAt: 1, - internal: 1, - userId: 1, - customerId: 1, - prevMsg: 1, - }, - }, - }; -}; +import * as moment from 'moment'; +import * as _ from 'underscore'; +import { ConversationMessages, Conversations, Deals, Integrations, Pipelines, Stages, Users } from '../../../db/models'; +import { IStageDocument } from '../../../db/models/definitions/boards'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; +import { IUser } from '../../../db/models/definitions/users'; +import { INSIGHT_TYPES } from '../../constants'; +import { fixDate } from '../../utils'; +import { getDateFieldAsStr } from './aggregationUtils'; +import { + IDealListArgs, + IDealSelector, + IFilterSelector, + IFixDates, + IGenerateChartData, + IGenerateMessage, + IGeneratePunchCard, + IGenerateResponseData, + IGenerateTimeIntervals, + IGenerateUserChartData, + IListArgs, + IMessageSelector, + IResponseUserData, + IStageSelector, +} from './types'; + +/** + * Return filterSelector + * @param args + */ +export const getFilterSelector = (args: IListArgs): { [key: string]: any } => { + const selector: IFilterSelector = { integration: {} }; + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + if (integrationIds) { + selector.integration.kind = { $in: integrationIds.split(',') }; + } + + if (brandIds) { + selector.integration.brandId = { $in: brandIds.split(',') }; + } + + selector.createdAt = { $gte: start, $lte: end }; + + return selector; +}; + +/** + * Return filterSelector + * @param args + */ +export const getDealSelector = async (args: IDealListArgs): Promise => { + const { startDate, endDate, boardId, pipelineIds, status } = args; + const { start, end } = fixDates(startDate, endDate); + + const selector: IDealSelector = {}; + const date = { + $gte: start, + $lte: end, + }; + + // If status is either won or lost, modified date is more important + if (status) { + selector.modifiedAt = date; + } else { + selector.createdAt = date; + } + + const stageSelector: IStageSelector = {}; + + if (status) { + stageSelector.probability = status; + } + + let stages: IStageDocument[] = []; + + if (boardId) { + if (pipelineIds) { + stageSelector.pipelineId = { $in: pipelineIds.split(',') }; + } else { + const pipelines = await Pipelines.find({ boardId }); + stageSelector.pipelineId = { $in: pipelines.map(p => p._id) }; + } + + stages = await Stages.find(stageSelector); + selector.stageId = { $in: stages.map(s => s._id) }; + } else { + if (status) { + stages = await Stages.find(stageSelector); + selector.stageId = { $in: stages.map(s => s._id) }; + } + } + + return selector; +}; + +/** + * Return conversationSelect for aggregation + * @param args + * @param conversationSelector + * @param selectIds + */ +export const getConversationSelector = async ( + filterSelector: any, + conversationSelector: any = {}, + fieldName: string = 'createdAt', +): Promise => { + if (Object.keys(filterSelector.integration).length > 0) { + const integrationIds = await Integrations.findIntegrations(filterSelector.integration).select('_id'); + conversationSelector.integrationId = { $in: integrationIds.map(row => row._id) }; + } + + if (!conversationSelector[fieldName]) { + conversationSelector[fieldName] = filterSelector.createdAt; + } + + return { ...conversationSelector, ...noConversationSelector }; +}; +/** + * + * @param summaries + * @param collection + * @param selector + */ +export const getSummaryData = async ({ + start, + end, + selector, + collection, + dateFieldName = 'createdAt', +}: { + start: Date; + end: Date; + selector: any; + collection: any; + dateFieldName?: string; +}): Promise => { + const intervals = generateTimeIntervals(start, end); + const summaries: Array<{ title?: string; count?: number }> = []; + + // finds a respective message counts for different time intervals. + for (const interval of intervals) { + const facetMessageSelector = { ...selector }; + + facetMessageSelector[dateFieldName] = { + $gte: interval.start.toDate(), + $lte: interval.end.toDate(), + }; + const [intervalCount] = await collection.aggregate([ + { + $match: facetMessageSelector, + }, + { + $group: { + _id: null, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + count: 1, + }, + }, + ]); + + summaries.push({ + title: interval.title, + count: intervalCount ? intervalCount.count : 0, + }); + } + + return summaries; +}; + +/** + * Builds messages find query selector. + */ +export const getMessageSelector = async ({ args, createdAt }: IGenerateMessage): Promise => { + const messageSelector: any = { + fromBot: { $exists: false }, + userId: args.type === 'response' ? { $ne: null } : null, + }; + + const filterSelector = getFilterSelector(args); + messageSelector.createdAt = filterSelector.createdAt; + + // While searching by integration + if (Object.keys(filterSelector.integration).length > 0) { + const selector = await getConversationSelector(filterSelector, { createdAt }); + + const conversationIds = await Conversations.find(selector).select('_id'); + + const rawConversationIds = conversationIds.map(obj => obj._id); + messageSelector.conversationId = { $in: rawConversationIds }; + } + + return messageSelector; +}; +/** + * Fix trend for missing values because from then aggregation, + * it could return missing values for some dates. This method + * will assign 0 values for missing x values. + * @param startDate + * @param endDate + * @param data + */ +export const fixChartData = async (data: any[], hintX: string, hintY: string): Promise => { + const results = {}; + data.map(row => { + results[row[hintX]] = row[hintY]; + }); + + return Object.keys(results) + .sort() + .map(key => { + return { x: moment(key).format('MM-DD'), y: results[key] }; + }); +}; + +/** + * Populates message collection into date range + * by given duration and loop count for chart data. + */ +export const generateChartDataBySelector = async ({ + selector, + type = INSIGHT_TYPES.CONVERSATION, + dateFieldName = '$createdAt', +}: { + selector: IMessageSelector; + type?: string; + dateFieldName?: string; +}): Promise => { + const pipelineStages = [ + { + $match: selector, + }, + { + $project: { + date: getDateFieldAsStr({ fieldName: dateFieldName }), + }, + }, + { + $group: { + _id: '$date', + y: { $sum: 1 }, + }, + }, + { + $project: { + x: '$_id', + y: 1, + _id: 0, + }, + }, + { + $sort: { + x: 1, + }, + }, + ]; + + if (type === INSIGHT_TYPES.DEAL) { + return Deals.aggregate([pipelineStages]); + } + + return ConversationMessages.aggregate([pipelineStages]); +}; + +export const generatePunchData = async ( + collection: any, + selector: object, + user: IUser, +): Promise => { + const pipelineStages = [ + { + $match: selector, + }, + { + $project: { + hour: { $hour: { date: '$createdAt', timezone: '+08' } }, + date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), + }, + }, + { + $group: { + _id: { + hour: '$hour', + date: '$date', + }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + hour: '$_id.hour', + date: '$_id.date', + count: 1, + }, + }, + ]; + + return collection.aggregate(pipelineStages); +}; + +/** + * Populates message collection into date range + * by given duration and loop count for chart data. + */ + +export const generateChartDataByCollection = async (collection: any): Promise => { + const results = {}; + + collection.map(obj => { + const date = moment(obj.createdAt).format('YYYY-MM-DD'); + + results[date] = (results[date] || 0) + 1; + }); + + return Object.keys(results) + .sort() + .map(key => { + return { x: moment(key).format('MM-DD'), y: results[key] }; + }); +}; + +/** + * Generates time intervals for main report + */ +export const generateTimeIntervals = (start: Date, end: Date): IGenerateTimeIntervals[] => { + const month = moment(end).month(); + + return [ + { + title: 'In time range', + start: moment(start), + end: moment(end), + }, + { + title: 'This month', + start: moment(1, 'DD'), + end: moment(), + }, + { + title: 'This week', + start: moment(end).weekday(0), + end: moment(end), + }, + { + title: 'Today', + start: moment(end).add(-1, 'days'), + end: moment(end), + }, + { + title: 'Last 30 days', + start: moment(end).add(-30, 'days'), + end: moment(end), + }, + { + title: 'Last month', + start: moment(month + 1, 'MM').subtract(1, 'months'), + end: moment(month + 1, 'MM'), + }, + { + title: 'Last week', + start: moment(end).weekday(-7), + end: moment(end).weekday(0), + }, + { + title: 'Yesterday', + start: moment(end).add(-2, 'days'), + end: moment(end).add(-1, 'days'), + }, + ]; +}; + +/** + * Generate chart data for given user + */ +export const generateUserChartData = async ({ + userId, + userMessages, +}: { + userId: string; + userMessages: IMessageDocument[]; +}): Promise => { + const user = await Users.findOne({ _id: userId }); + const userData = await generateChartDataByCollection(userMessages); + + if (!user) { + return { + graph: userData, + }; + } + + const userDetail = user.details; + + return { + fullName: userDetail ? userDetail.fullName : '', + avatar: userDetail ? userDetail.avatar : '', + graph: userData, + }; +}; + +export const fixDates = (startValue: string, endValue: string, count?: number): IFixDates => { + // convert given value or get today + const endDate = fixDate(endValue); + + const startDateDefaultValue = new Date( + moment(endDate) + .add(count ? count * -1 : -7, 'days') + .toString(), + ); + + // convert given value or generate from endDate + const startDate = fixDate(startValue, startDateDefaultValue); + + return { start: startDate, end: endDate }; +}; + +export const getSummaryDates = (endValue: string): any => { + // convert given value or get today + const endDate = fixDate(endValue); + + const month = moment(endDate).month(); + const startDate = new Date( + moment(month + 1, 'MM') + .subtract(1, 'months') + .toString(), + ); + + return { $gte: startDate, $lte: endDate }; +}; + +/** + * Generate response chart data. + */ +export const generateResponseData = async ( + responseData: IMessageDocument[], + responseUserData: IResponseUserData, + allResponseTime: number, +): Promise => { + // preparing trend chart data + const trend = await generateChartDataByCollection(responseData); + + // Average response time for all messages + const time = Math.floor(allResponseTime / responseData.length); + + const teamMembers: any = []; + + const userIds = _.uniq(_.pluck(responseData, 'userId')) as string[]; + + for (const userId of userIds) { + const { responseTime, count, summaries } = responseUserData[userId || '']; + + // Average response time for users. + const avgResTime = Math.floor(responseTime / count); + + // preparing each team member's chart data + teamMembers.push({ + data: await generateUserChartData({ + userId: userId || '', + userMessages: responseData.filter(message => userId === message.userId), + }), + time: avgResTime, + summaries, + }); + } + + return { trend, time, teamMembers }; +}; + +export const getTimezone = (user: IUser): string => { + return (user.details ? user.details.location : '+08') || '+08'; +}; + +export const noConversationSelector = { + $or: [ + { userId: { $exists: true }, messageCount: { $gt: 1 } }, + { + userId: { $exists: false }, + $or: [ + { + closedAt: { $exists: true }, + closedUserId: { $exists: true }, + status: CONVERSATION_STATUSES.CLOSED, + }, + { + status: { $ne: CONVERSATION_STATUSES.CLOSED }, + }, + ], + }, + ], +}; + +export const timeIntervals: any[] = [ + { name: '0-5 second', count: 5 }, + { name: '6-10 second', count: 10 }, + { name: '11-15 second', count: 15 }, + { name: '16-20 second', count: 20 }, + { name: '21-25 second', count: 25 }, + { name: '26-30 second', count: 30 }, + { name: '31-35 second', count: 35 }, + { name: '36-40 second', count: 40 }, + { name: '41-45 second', count: 45 }, + { name: '46-50 second', count: 50 }, + { name: '51-55 second', count: 55 }, + { name: '56-60 second', count: 60 }, + { name: '1-2 min', count: 120 }, + { name: '2-3 min', count: 180 }, + { name: '3-4 min', count: 240 }, + { name: '4-5 min', count: 300 }, + { name: '5+ min' }, +]; + +export const timeIntervalBranches = () => { + const copyTimeIntervals = [...timeIntervals]; + copyTimeIntervals.pop(); + + return copyTimeIntervals.map(t => ({ + case: { $lte: ['$firstRespondTime', t.count] }, + then: t.name, + })); +}; + +/** + * Return conversationSelect for aggregation + * @param filterSelector + * @param conversationSelector + * @param messageSelector + */ +export const getConversationSelectorToMsg = async ( + integrationIds: string, + brandIds: string, + conversationSelector: any = {}, +): Promise => { + const filterSelector: IFilterSelector = { integration: {} }; + if (integrationIds) { + filterSelector.integration.kind = { $in: integrationIds.split(',') }; + } + + if (brandIds) { + filterSelector.integration.brandId = { $in: brandIds.split(',') }; + } + + if (Object.keys(filterSelector.integration).length > 0) { + const integrationIdsList = await Integrations.findIntegrations(filterSelector.integration).select('_id'); + conversationSelector.integrationId = { $in: integrationIdsList.map(row => row._id) }; + } + return { ...conversationSelector }; +}; + +export const getConversationSelectorByMsg = async ( + integrationIds: string, + brandIds: string, + conversationSelector: any = {}, + messageSelector: any = {}, +): Promise => { + const conversationFinder = await getConversationSelectorToMsg(integrationIds, brandIds, conversationSelector); + const conversationIds = await Conversations.find(conversationFinder).select('_id'); + + const rawConversationIds = await conversationIds.map(obj => obj._id); + messageSelector.conversationId = { $in: rawConversationIds }; + + return { ...messageSelector }; +}; + +export const getConversationReportLookup = async (): Promise => { + return { + lookupPrevMsg: { + $lookup: { + from: 'conversation_messages', + let: { checkConversation: '$conversationId', checkAt: '$createdAt' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$conversationId', '$$checkConversation'] }, { $lt: ['$createdAt', '$$checkAt'] }], + }, + }, + }, + { + $project: { + conversationId: 1, + createdAt: 1, + internal: 1, + userId: 1, + customerId: 1, + sizeMentionedIds: { $size: '$mentionedUserIds' }, + }, + }, + ], + as: 'prevMsgs', + }, + }, + prevMsgSlice: { + $addFields: { prevMsg: { $slice: ['$prevMsgs', -1] } }, + }, + diffSecondCalc: { + $addFields: { + diffSec: { + $divide: [{ $subtract: ['$createdAt', '$prevMsg.createdAt'] }, 1000], + }, + }, + }, + firstProject: { + $project: { + conversationId: 1, + createdAt: 1, + internal: 1, + userId: 1, + customerId: 1, + prevMsg: 1, + }, + }, + }; +}; diff --git a/src/data/modules/integrations/receiveMessage.ts b/src/data/modules/integrations/receiveMessage.ts new file mode 100644 index 000000000..9d73029d2 --- /dev/null +++ b/src/data/modules/integrations/receiveMessage.ts @@ -0,0 +1,161 @@ +import { + ConversationMessages, + Conversations, + Customers, + EmailDeliveries, + Integrations, + Users, +} from '../../../db/models'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { graphqlPubsub } from '../../../pubsub'; +import { getConfigs } from '../../utils'; + +const sendError = message => ({ + status: 'error', + errorMessage: message, +}); + +const sendSuccess = data => ({ + status: 'success', + data, +}); + +/* + * Handle requests from integrations api + */ +export const receiveRpcMessage = async msg => { + const { action, metaInfo, payload } = msg; + const doc = JSON.parse(payload || '{}'); + + if (action === 'get-create-update-customer') { + const integration = await Integrations.findOne({ _id: doc.integrationId }); + + if (!integration) { + return sendError(`Integration not found: ${doc.integrationId}`); + } + + const { primaryEmail, primaryPhone } = doc; + + let customer; + + const getCustomer = async selector => Customers.findOne(selector).lean(); + + if (primaryPhone) { + customer = await getCustomer({ primaryPhone }); + + if (customer) { + await Customers.updateCustomer(customer._id, doc); + return sendSuccess({ _id: customer._id }); + } + } + + if (primaryEmail) { + customer = await getCustomer({ primaryEmail }); + } + + if (customer) { + return sendSuccess({ _id: customer._id }); + } else { + customer = await Customers.createCustomer({ + ...doc, + scopeBrandIds: integration.brandId, + }); + } + + return sendSuccess({ _id: customer._id }); + } + + if (action === 'create-or-update-conversation') { + const { conversationId, content, owner } = doc; + + let user; + + if (owner) { + user = await Users.findOne({ 'details.operatorPhone': owner }); + } + + const assignedUserId = user ? user._id : null; + + if (conversationId) { + await Conversations.updateConversation(conversationId, { content, assignedUserId }); + + return sendSuccess({ _id: conversationId }); + } + + doc.assignedUserId = assignedUserId; + + const conversation = await Conversations.createConversation(doc); + + return sendSuccess({ _id: conversation._id }); + } + + if (action === 'create-conversation-message') { + const message = await ConversationMessages.createMessage(doc); + + const conversationDoc: { status: string; readUserIds: string[]; content?: string; updatedAt?: Date } = { + // Reopen its conversation if it's closed + status: doc.unread || doc.unread === undefined ? CONVERSATION_STATUSES.OPEN : CONVERSATION_STATUSES.CLOSED, + + // Mark as unread + readUserIds: [], + }; + + if (message.content && metaInfo === 'replaceContent') { + conversationDoc.content = message.content; + } + + if (doc.createdAt) { + conversationDoc.updatedAt = doc.createdAt; + } + + await Conversations.updateConversation(message.conversationId, conversationDoc); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + return sendSuccess({ _id: message._id }); + } + + if (action === 'get-configs') { + const configs = await getConfigs(); + return sendSuccess({ configs }); + } + + if (action === 'getUserIds') { + const users = await Users.find({}, { _id: 1 }); + return sendSuccess({ userIds: users.map(user => user._id) }); + } +}; + +/* + * Integrations api notification + */ +export const receiveIntegrationsNotification = async msg => { + const { action } = msg; + + if (action === 'external-integration-entry-added') { + graphqlPubsub.publish('conversationExternalIntegrationMessageInserted', {}); + + return sendSuccess({ status: 'ok' }); + } +}; + +/* + * Engages notification + */ +export const receiveEngagesNotification = async msg => { + const { action, data } = msg; + + if (action === 'setDoNotDisturb') { + await Customers.updateOne({ _id: data.customerId }, { $set: { doNotDisturb: 'Yes' } }); + } + + if (action === 'transactionEmail') { + await EmailDeliveries.updateEmailDeliveryStatus(data.emailDeliveryId, data.status); + } +}; diff --git a/src/data/modules/segments/queryBuilder.ts b/src/data/modules/segments/queryBuilder.ts index b56ee379e..38e453b94 100644 --- a/src/data/modules/segments/queryBuilder.ts +++ b/src/data/modules/segments/queryBuilder.ts @@ -1,135 +1,397 @@ -import * as moment from 'moment'; +import * as _ from 'underscore'; import { Segments } from '../../../db/models'; -import { ICondition, ISegment, ISegmentDocument } from '../../../db/models/definitions/segments'; +import { SEGMENT_DATE_OPERATORS, SEGMENT_NUMBER_OPERATORS } from '../../../db/models/definitions/constants'; +import { ICondition, ISegment } from '../../../db/models/definitions/segments'; +import { fetchElk } from '../../../elasticsearch'; +import { getEsTypes } from '../coc/utils'; -const generateFilter = (condition: ICondition, brandsMapping) => { - const conditionFilter = { [condition.field]: convertConditionToQuery(condition) }; +export const fetchBySegments = async (segment: ISegment, action: 'search' | 'count' = 'search'): Promise => { + if (!segment || !segment.conditions) { + return []; + } + + const { contentType } = segment; + const index = contentType === 'company' ? 'companies' : 'customers'; + const idField = contentType === 'company' ? 'companyId' : 'customerId'; + const typesMap = getEsTypes(contentType); + + const propertyPositive: any[] = []; + const propertyNegative: any[] = []; + + if (contentType !== 'company') { + propertyNegative.push({ + term: { + status: 'Deleted', + }, + }); + } + + const eventPositive = []; + const eventNegative = []; + + await generateQueryBySegment({ segment, typesMap, propertyPositive, propertyNegative, eventNegative, eventPositive }); - if (condition.brandId && brandsMapping) { + let idsByEvents = []; + + if (eventPositive.length > 0 || eventNegative.length > 0) { + const eventsResponse = await fetchElk('search', 'events', { + _source: idField, + size: 10000, + query: { + bool: { + must: eventPositive, + must_not: eventNegative, + }, + }, + }); + + idsByEvents = eventsResponse.hits.hits.map(hit => hit._source[idField]); + + propertyPositive.push({ + terms: { + _id: idsByEvents, + }, + }); + } + + if (action === 'count') { return { - $and: [conditionFilter, { integrationId: { $in: brandsMapping[condition.brandId] || [] } }], + positiveList: propertyPositive, + negativeList: propertyNegative, }; } - return conditionFilter; + const response = await fetchElk('search', index, { + _source: false, + size: 10000, + query: { + bool: { + must: propertyPositive, + must_not: propertyNegative, + }, + }, + }); + + const idsByContentType = response.hits.hits.map(hit => hit._id); + + let ids = idsByContentType.length ? idsByContentType : idsByEvents; + + if (idsByContentType.length > 0 && idsByEvents.length > 0) { + ids = _.intersection(idsByContentType, idsByEvents); + } + + return ids; }; -export default { - async segments( - segment?: ISegment | null, - headSegment?: ISegmentDocument | null, - brandsMapping?: { [key: string]: string[] }, - ): Promise { - const query: any = { $and: [] }; +const generateQueryBySegment = async (args: { + propertyPositive; + propertyNegative; + eventPositive; + eventNegative; + segment: ISegment; + typesMap: { [key: string]: any }; +}) => { + const { segment, typesMap, propertyNegative, propertyPositive, eventNegative, eventPositive } = args; + + // Fetching parent segment + const embeddedParentSegment = await Segments.findOne({ _id: segment.subOf }); + const parentSegment = embeddedParentSegment; + + if (parentSegment) { + await generateQueryBySegment({ ...args, segment: parentSegment }); + } - if (!segment || !segment.connector || !segment.conditions) { - return {}; + const propertyConditions: ICondition[] = []; + const eventConditions: ICondition[] = []; + + for (const condition of segment.conditions) { + if (condition.type === 'property') { + propertyConditions.push(condition); } - const childQuery = { - [segment.connector === 'any' ? '$or' : '$and']: segment.conditions.map(condition => { - return generateFilter(condition, brandsMapping); - }), - }; + if (condition.type === 'event') { + eventConditions.push(condition); + } + } + + for (const condition of propertyConditions) { + const field = condition.propertyName || ''; + + elkConvertConditionToQuery({ + field, + type: typesMap[field], + operator: condition.propertyOperator || '', + value: condition.propertyValue || '', + positive: propertyPositive, + negative: propertyNegative, + }); + } + + for (const condition of eventConditions) { + const { eventOccurence, eventName, eventOccurenceValue, eventAttributeFilters = [] } = condition; - if (segment.conditions.length) { - query.$and.push(childQuery); + if (!eventOccurence || !eventOccurenceValue) { + continue; } - // Fetching parent segment - const embeddedParentSegment = await Segments.findOne({ _id: segment.subOf }); - const parentSegment = headSegment || embeddedParentSegment; + eventPositive.push({ + term: { + name: eventName, + }, + }); - if (parentSegment) { - const parentQuery = { - [parentSegment.connector === 'any' ? '$or' : '$and']: parentSegment.conditions.map(condition => { - return generateFilter(condition, brandsMapping); - }), - }; + if (eventOccurence === 'exactly') { + eventPositive.push({ + term: { + count: eventOccurenceValue, + }, + }); + } - if (parentSegment.conditions.length) { - query.$and.push(parentQuery); - } + if (eventOccurence === 'atleast') { + eventPositive.push({ + range: { + count: { + gte: eventOccurenceValue, + }, + }, + }); + } + + if (eventOccurence === 'atmost') { + eventPositive.push({ + range: { + count: { + lte: eventOccurenceValue, + }, + }, + }); } - return query.$and.length ? query : {}; - }, + for (const filter of eventAttributeFilters) { + elkConvertConditionToQuery({ + field: `attributes.${filter.name}`, + operator: filter.operator, + value: filter.value, + positive: eventPositive, + negative: eventNegative, + }); + } + } }; -function convertConditionToQuery(condition: ICondition) { - const { operator, type, dateUnit = '', value = '' } = condition; - - let transformedValue; - - switch (type) { - case 'string': - transformedValue = value.toLowerCase(); - break; - case 'number': - case 'date': - transformedValue = parseInt(value, 10); - break; - default: - transformedValue = value; - break; - } - - switch (operator) { - case 'e': - case 'et': - default: - return transformedValue; - case 'dne': - return { $ne: transformedValue }; - case 'c': - return { - $regex: new RegExp(`.*${escapeRegExp(transformedValue)}.*`, 'i'), - }; - case 'dnc': - return { - $regex: new RegExp(`^((?!${escapeRegExp(transformedValue)}).)*$`, 'i'), - }; - case 'igt': - return { $gt: transformedValue }; - case 'ilt': - return { $lt: transformedValue }; - case 'it': - return true; - case 'if': - return false; - case 'wlt': - return { - $gte: moment() - .subtract(transformedValue, dateUnit) - .toDate(), - $lte: new Date(), +const generateNestedQuery = (kind: string, field: string, operator: string, query: any) => { + const fieldKey = field.replace(`${kind}.`, ''); + + let fieldValue = 'value'; + + if (SEGMENT_NUMBER_OPERATORS.includes(operator)) { + fieldValue = 'numberValue'; + } + + if (SEGMENT_DATE_OPERATORS.includes(operator)) { + fieldValue = 'dateValue'; + } + + let updatedQuery = query; + + updatedQuery = JSON.stringify(updatedQuery).replace(`${kind}.${fieldKey}`, `${kind}.${fieldValue}`); + updatedQuery = JSON.parse(updatedQuery); + + return { + nested: { + path: kind, + query: { + bool: { + must: [ + { + term: { + [`${kind}.field`]: fieldKey, + }, + }, + updatedQuery, + ], + }, + }, + }, + }; +}; + +function elkConvertConditionToQuery(args: { + field: string; + type?: any; + operator: string; + value: string; + positive; + negative; +}) { + const { field, type, operator, value, positive, negative } = args; + + const fixedValue = value.toLocaleLowerCase(); + + let positiveQuery; + let negativeQuery; + + // equal + if (['e', 'numbere'].includes(operator)) { + if (['keyword', 'email'].includes(type) || operator === 'numbere') { + positiveQuery = { + term: { [field]: value }, }; - case 'wmt': - return { - $lte: moment() - .subtract(transformedValue, dateUnit) - .toDate(), + } else { + positiveQuery = { + match_phrase: { [field]: value }, }; - case 'wow': - return { - $lte: moment(Date.now()) - .add(transformedValue, dateUnit) - .toDate(), - $gte: moment(Date.now()).toDate(), + } + } + + // does not equal + if (['dne', 'numberdne'].includes(operator)) { + if (['keyword', 'email'].includes(type) || operator === 'numberdne') { + negativeQuery = { + term: { [field]: value }, }; - case 'woa': - return { - $gte: moment() - .add(transformedValue, dateUnit) - .toDate(), + } else { + negativeQuery = { + match_phrase: { [field]: value }, }; - case 'is': - return { $exists: true, $ne: '' }; - case 'ins': - return { $exists: false }; + } + } + + // contains + if (operator === 'c') { + positiveQuery = { + wildcard: { + [field]: `*${fixedValue}*`, + }, + }; + } + + // does not contains + if (operator === 'dnc') { + negativeQuery = { + wildcard: { + [field]: `*${fixedValue}*`, + }, + }; + } + + // greater than equal + if (['igt', 'numberigt', 'dateigt'].includes(operator)) { + positiveQuery = { + range: { + [field]: { + gte: fixedValue, + }, + }, + }; + } + + // less then equal + if (['ilt', 'numberilt', 'dateilt'].includes(operator)) { + positiveQuery = { + range: { + [field]: { + lte: fixedValue, + }, + }, + }; } -} -function escapeRegExp(str: string) { - return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + // is true + if (operator === 'it') { + positiveQuery = { + term: { + [field]: true, + }, + }; + } + + // is false + if (operator === 'if') { + positiveQuery = { + term: { + [field]: false, + }, + }; + } + + // is set + if (operator === 'is') { + positiveQuery = { + exists: { + field, + }, + }; + } + + // is not set + if (operator === 'ins') { + negativeQuery = { + exists: { + field, + }, + }; + } + + if (['woam', 'wobm', 'woad', 'wobd'].includes(operator)) { + let gte = ''; + let lte = ''; + + // will occur after on following n-th minute + if (operator === 'woam') { + gte = `now-${fixedValue}m/m`; + lte = `now-${fixedValue}m/m`; + } + + // will occur before on following n-th minute + if (operator === 'wobm') { + gte = `now+${fixedValue}m/m`; + lte = `now+${fixedValue}m/m`; + } + + // will occur after on following n-th day + if (operator === 'woad') { + gte = `now-${fixedValue}d/d`; + lte = `now-${fixedValue}d/d`; + } + + // will occur before on following n-th day + if (operator === 'wobd') { + gte = `now+${fixedValue}d/d`; + lte = `now+${fixedValue}d/d`; + } + + positiveQuery = { range: { [field]: { gte, lte } } }; + } + + // date relative less than + if (operator === 'drlt') { + positiveQuery = { range: { [field]: { lte: fixedValue } } }; + } + + // date relative greater than + if (operator === 'drgt') { + positiveQuery = { range: { [field]: { gte: fixedValue } } }; + } + + for (const nestedType of ['customFieldsData', 'trackedData', 'attributes']) { + if (field.includes(nestedType)) { + if (positiveQuery) { + positiveQuery = generateNestedQuery(nestedType, field, operator, positiveQuery); + } + + if (negativeQuery) { + negativeQuery = generateNestedQuery(nestedType, field, operator, negativeQuery); + } + } + } + + if (positiveQuery) { + positive.push(positiveQuery); + } + + if (negativeQuery) { + negative.push(negativeQuery); + } } diff --git a/src/data/permissions/actions/permission.ts b/src/data/permissions/actions/permission.ts index 6bb18765b..e7948fa71 100644 --- a/src/data/permissions/actions/permission.ts +++ b/src/data/permissions/actions/permission.ts @@ -16,6 +16,10 @@ export const moduleObjects = { name: 'showBrands', description: 'Show brands', }, + { + name: 'exportBrands', + description: 'Export brands', + }, ], }, channels: { @@ -35,6 +39,10 @@ export const moduleObjects = { name: 'showChannels', description: 'Show channel', }, + { + name: 'exportChannels', + description: 'Export channels', + }, ], }, companies: { @@ -47,7 +55,6 @@ export const moduleObjects = { use: [ 'companiesAdd', 'companiesEdit', - 'companiesEditCustomers', 'companiesRemove', 'companiesMerge', 'showCompanies', @@ -67,10 +74,6 @@ export const moduleObjects = { name: 'companiesRemove', description: 'Remove companies', }, - { - name: 'companiesEditCustomers', - description: 'Edit companies customer', - }, { name: 'companiesMerge', description: 'Merge companies', @@ -100,10 +103,10 @@ export const moduleObjects = { 'showCustomers', 'customersAdd', 'customersEdit', - 'customersEditCompanies', 'customersMerge', 'customersRemove', 'exportCustomers', + 'customersChangeState', ], }, { @@ -122,10 +125,6 @@ export const moduleObjects = { name: 'customersEdit', description: 'Edit customer', }, - { - name: 'customersEditCompanies', - description: 'Update customers companies', - }, { name: 'customersMerge', description: 'Merge customers', @@ -134,6 +133,10 @@ export const moduleObjects = { name: 'customersRemove', description: 'Remove customers', }, + { + name: 'customersChangeState', + description: 'Change customer state', + }, ], }, deals: { @@ -160,8 +163,9 @@ export const moduleObjects = { 'dealsAdd', 'dealsEdit', 'dealsRemove', - 'dealsUpdateOrder', 'dealsWatch', + 'dealsArchive', + 'exportDeals', ], }, { @@ -220,10 +224,6 @@ export const moduleObjects = { name: 'dealsEdit', description: 'Edit deal', }, - { - name: 'dealsUpdateOrder', - description: 'Update deal order', - }, { name: 'dealsRemove', description: 'Remove deal', @@ -232,6 +232,14 @@ export const moduleObjects = { name: 'dealsWatch', description: 'Watch deal', }, + { + name: 'dealsArchive', + description: 'Archive all deals in a specific stage', + }, + { + name: 'exportDeals', + description: 'Export deals', + }, ], }, tickets: { @@ -258,8 +266,9 @@ export const moduleObjects = { 'ticketsAdd', 'ticketsEdit', 'ticketsRemove', - 'ticketsUpdateOrder', 'ticketsWatch', + 'ticketsArchive', + 'exportTickets', ], }, { @@ -318,10 +327,6 @@ export const moduleObjects = { name: 'ticketsEdit', description: 'Edit ticket', }, - { - name: 'ticketsUpdateOrder', - description: 'Update ticket order', - }, { name: 'ticketsRemove', description: 'Remove ticket', @@ -330,6 +335,137 @@ export const moduleObjects = { name: 'ticketsWatch', description: 'Watch ticket', }, + { + name: 'ticketsArchive', + description: 'Archive all tickets in a specific stage', + }, + { + name: 'exportTickets', + description: 'Export tickets', + }, + ], + }, + growthHacks: { + name: 'growthHacks', + description: 'Growth hacking', + actions: [ + { + name: 'growthHacksAll', + description: 'All', + use: [ + 'showGrowthHacks', + 'growthHackBoardsAdd', + 'growthHackBoardsEdit', + 'growthHackBoardsRemove', + 'growthHackPipelinesAdd', + 'growthHackPipelinesEdit', + 'growthHackPipelinesUpdateOrder', + 'growthHackPipelinesWatch', + 'growthHackPipelinesRemove', + 'growthHackStagesAdd', + 'growthHackStagesEdit', + 'growthHackStagesUpdateOrder', + 'growthHackStagesRemove', + 'growthHacksAdd', + 'growthHacksEdit', + 'growthHacksRemove', + 'growthHacksWatch', + 'growthHacksArchive', + 'growthHackTemplatesAdd', + 'growthHackTemplatesEdit', + 'growthHackTemplatesRemove', + 'growthHackTemplatesDuplicate', + 'showGrowthHackTemplates', + ], + }, + { + name: 'showGrowthHacks', + description: 'Show growth hacks', + }, + { + name: 'growthHackBoardsAdd', + description: 'Add growth hacking board', + }, + { + name: 'growthHackBoardsRemove', + description: 'Remove growth hacking board', + }, + { + name: 'growthHackPipelinesAdd', + description: 'Add growth hacking pipeline', + }, + { + name: 'growthHackPipelinesEdit', + description: 'Edit growth hacking pipeline', + }, + { + name: 'growthHackPipelinesRemove', + description: 'Remove growth hacking pipeline', + }, + { + name: 'growthHackPipelinesWatch', + description: 'Growth hacking pipeline watch', + }, + { + name: 'growthHackPipelinesUpdateOrder', + description: 'Update pipeline order', + }, + { + name: 'growthHackStagesAdd', + description: 'Add growth hacking stage', + }, + { + name: 'growthHackStagesEdit', + description: 'Edit growth hacking stage', + }, + { + name: 'growthHackStagesUpdateOrder', + description: 'Update stage order', + }, + { + name: 'growthHackStagesRemove', + description: 'Remove growth hacking stage', + }, + { + name: 'growthHacksAdd', + description: 'Add growth hacking', + }, + { + name: 'growthHacksEdit', + description: 'Edit growth hacking', + }, + { + name: 'growthHacksRemove', + description: 'Remove growth hacking', + }, + { + name: 'growthHacksWatch', + description: 'Watch growth hacking', + }, + { + name: 'growthHacksArchive', + description: 'Archive all growth hacks in a specific stage', + }, + { + name: 'growthHackTemplatesAdd', + description: 'Add growth hacking template', + }, + { + name: 'growthHackTemplatesEdit', + description: 'Edit growth hacking template', + }, + { + name: 'growthHackTemplatesRemove', + description: 'Remove growth hacking template', + }, + { + name: 'growthHackTemplatesDuplicate', + description: 'Duplicate growth hacking template', + }, + { + name: 'showGrowthHackTemplates', + description: 'Show growth hacking template', + }, ], }, tasks: { @@ -356,8 +492,10 @@ export const moduleObjects = { 'tasksAdd', 'tasksEdit', 'tasksRemove', - 'tasksUpdateOrder', 'tasksWatch', + 'tasksArchive', + 'taskUpdateTimeTracking', + 'exportTasks', ], }, { @@ -416,10 +554,6 @@ export const moduleObjects = { name: 'tasksEdit', description: 'Edit task', }, - { - name: 'tasksUpdateOrder', - description: 'Update task order', - }, { name: 'tasksRemove', description: 'Remove task', @@ -428,6 +562,18 @@ export const moduleObjects = { name: 'tasksWatch', description: 'Watch task', }, + { + name: 'tasksArchive', + description: 'Archive all tasks in a specific stage', + }, + { + name: 'taskUpdateTimeTracking', + description: 'Update time tracking for a task', + }, + { + name: 'exportTasks', + description: 'Export tasks', + }, ], }, engages: { @@ -522,7 +668,13 @@ export const moduleObjects = { { name: 'permissionsAll', description: 'All', - use: ['managePermissions', 'showPermissions', 'showPermissionModules', 'showPermissionActions'], + use: [ + 'managePermissions', + 'showPermissions', + 'showPermissionModules', + 'showPermissionActions', + 'exportPermissions', + ], }, { name: 'managePermissions', @@ -540,6 +692,10 @@ export const moduleObjects = { name: 'showPermissionsActions', description: 'Show permissions actions', }, + { + name: 'exportPermissions', + description: 'Export permissions', + }, ], }, usersGroups: { @@ -602,28 +758,32 @@ export const moduleObjects = { }, users: { name: 'users', - description: 'Users', + description: 'Team members', actions: [ { name: 'usersAll', description: 'All', - use: ['showUsers', 'usersEdit', 'usersInvite', 'usersSetActiveStatus'], + use: ['showUsers', 'usersEdit', 'usersInvite', 'usersSetActiveStatus', 'exportUsers'], }, { name: 'showUsers', - description: 'Show users', + description: 'Show team members', }, { name: 'usersSetActiveStatus', - description: 'Set active/deactive user', + description: 'Set active/deactive team member', }, { name: 'usersEdit', - description: 'Update user', + description: 'Update team member', }, { name: 'usersInvite', - description: 'Invite user', + description: 'Invite team member', + }, + { + name: 'exportUsers', + description: 'Export team members', }, ], }, @@ -758,9 +918,11 @@ export const moduleObjects = { 'integrationsEditMessengerIntegration', 'integrationsSaveMessengerAppearanceData', 'integrationsSaveMessengerConfigs', - 'integrationsCreateFormIntegration', - 'integrationsEditFormIntegration', + 'integrationsCreateLeadIntegration', + 'integrationsEditLeadIntegration', 'integrationsRemove', + 'integrationsArchive', + 'integrationsEdit', ], }, { @@ -784,54 +946,24 @@ export const moduleObjects = { description: 'Save messenger config', }, { - name: 'integrationsCreateFormIntegration', - description: 'Create form integration', + name: 'integrationsCreateLeadIntegration', + description: 'Create lead integration', }, { - name: 'integrationsEditFormIntegration', - description: 'Edit form integration', + name: 'integrationsEditLeadIntegration', + description: 'Edit lead integration', }, { name: 'integrationsRemove', description: 'Remove integration', }, - ], - }, - fields: { - name: 'fields', - description: 'Fields', - actions: [ { - name: 'fieldsAll', - description: 'All', - use: ['showFields', 'manageFields'], - }, - { - name: 'manageFields', - description: 'Manage fields', + name: 'integrationsArchive', + description: 'Archive an integration', }, { - name: 'showFields', - description: 'Show fields', - }, - ], - }, - fieldsGroups: { - name: 'fieldsGroups', - description: 'Fields groups', - actions: [ - { - name: 'fieldsGroupsAll', - description: 'All', - use: ['showFieldsGroups', 'manageFieldsGroups'], - }, - { - name: 'manageFieldsGroups', - description: 'Manage fields groups', - }, - { - name: 'showFieldsGroups', - description: 'Show fields groups', + name: 'integrationsEdit', + description: 'Edit common integration fields', }, ], }, @@ -860,6 +992,10 @@ export const moduleObjects = { name: 'conversationMessageAdd', description: 'Add conversation message', }, + { + name: 'conversationResolveAll', + description: 'Resolve all converstaion', + }, ], }, generalSettings: { @@ -900,4 +1036,33 @@ export const moduleObjects = { }, ], }, + logs: { + name: 'logs', + description: 'Logs', + actions: [ + { + name: 'viewLogs', + description: 'View logs', + }, + ], + }, + webhooks: { + name: 'webhooks', + description: 'Webhooks', + actions: [ + { + name: 'webhooksAll', + description: 'All', + use: ['showWebhooks', 'manageWebhooks'], + }, + { + name: 'showWebhooks', + description: 'Show webhooks', + }, + { + name: 'manageWebhooks', + description: 'Manage webhooks', + }, + ], + }, }; diff --git a/src/data/permissions/utils.ts b/src/data/permissions/utils.ts index 4cade7207..7b3c5958b 100644 --- a/src/data/permissions/utils.ts +++ b/src/data/permissions/utils.ts @@ -1,8 +1,8 @@ import { Permissions, Users } from '../../db/models'; import { IUserDocument } from '../../db/models/definitions/users'; -import { get, set } from '../../redisClient'; +import memoryStorage from '../../inmemoryStorage'; -export interface IModulesMap { +export interface IModuleMap { name: string; description?: string; actions?: IActionsMap[]; @@ -16,7 +16,7 @@ export interface IActionsMap { } // Schema: {name: description} -export const modulesMap: IModulesMap[] = []; +export const modulesMap: IModuleMap[] = []; /* Schema: @@ -88,7 +88,7 @@ export const can = async (action: string, user: IUserDocument): Promise return true; } - const actionMap: IActionMap = await getUserAllowedActions(user); + const actionMap: IActionMap = await getUserActionsMap(user); return actionMap[action] === true; }; @@ -100,25 +100,42 @@ interface IActionMap { const getKey = (user: IUserDocument) => `user_permissions_${user._id}`; /* - * Get given users permission map from redis or database + * Get given users permission map from inmemory storage or database */ -export const getUserAllowedActions = async (user: IUserDocument): Promise => { +export const getUserActionsMap = async (user: IUserDocument): Promise => { const key = getKey(user); - const permissionCache = await get(key); + const permissionCache = await memoryStorage().get(key); let actionMap: IActionMap; - if (permissionCache) { + if (permissionCache && permissionCache !== '{}') { actionMap = JSON.parse(permissionCache); } else { - actionMap = await userAllowedActions(user); + actionMap = await userActionsMap(user); - set(key, JSON.stringify(actionMap)); + memoryStorage().set(key, JSON.stringify(actionMap)); } return actionMap; }; +/* + * Get allowed actions + */ +export const getUserAllowedActions = async (user: IUserDocument): Promise => { + const map = await getUserActionsMap(user); + + const allowedActions: string[] = []; + + for (const key of Object.keys(map)) { + if (map[key]) { + allowedActions.push(key); + } + } + + return allowedActions; +}; + /* * Reset permissions map for all users */ @@ -128,11 +145,11 @@ export const resetPermissionsCache = async () => { for (const user of users) { const key = getKey(user); - set(key, ''); + memoryStorage().set(key, ''); } }; -export const userAllowedActions = async (user: IUserDocument): Promise => { +export const userActionsMap = async (user: IUserDocument): Promise => { const userPermissions = await Permissions.find({ userId: user._id }); const groupPermissions = await Permissions.find({ groupId: { $in: user.groupIds } }); diff --git a/src/data/permissions/wrappers.ts b/src/data/permissions/wrappers.ts index 7c20b3e09..fb65916c7 100644 --- a/src/data/permissions/wrappers.ts +++ b/src/data/permissions/wrappers.ts @@ -16,12 +16,14 @@ export const checkLogin = (user: IUserDocument) => { export const permissionWrapper = (cls: any, methodName: string, checkers: any) => { const oldMethod = cls[methodName]; - cls[methodName] = (root, args, { user }) => { + cls[methodName] = (root, args, context) => { + const { user } = context; + for (const checker of checkers) { checker(user); } - return oldMethod(root, args, { user }); + return oldMethod(root, args, context); }; }; @@ -66,11 +68,7 @@ export const checkPermission = async (cls: any, methodName: string, actionName: checkLogin(user); - let allowed = await can(actionName, user); - - if (user.isOwner) { - allowed = true; - } + const allowed = await can(actionName, user); if (!allowed) { if (defaultValue) { diff --git a/src/data/resolvers/activityLog.ts b/src/data/resolvers/activityLog.ts index c9d6848e1..f5405fe51 100644 --- a/src/data/resolvers/activityLog.ts +++ b/src/data/resolvers/activityLog.ts @@ -1,59 +1,132 @@ -import { Users } from '../../db/models'; -import { IActivityLogDocument } from '../../db/models/definitions/activityLogs'; -import { ACTIVITY_PERFORMER_TYPES } from '../../db/models/definitions/constants'; +import { + Brands, + ChecklistItems, + Checklists, + Companies, + Customers, + Deals, + GrowthHacks, + Integrations, + Stages, + Tasks, + Tickets, + Users, +} from '../../db/models'; +import { IActivityLog } from '../../db/models/definitions/activityLogs'; +import { ACTIVITY_ACTIONS } from '../../db/models/definitions/constants'; +import { IUserDocument } from '../../db/models/definitions/users'; -/* - * Placeholder object for ActivityLog resolver (used with graphql) - */ export default { - /** - * Finds id of the activity - */ - id(obj: IActivityLogDocument) { - return obj.activity.id; - }, + async createdByDetail(activityLog: IActivityLog) { + const user = await Users.findOne({ _id: activityLog.createdBy }); - /** - * Finds action of the activity - */ - action(obj: IActivityLogDocument) { - return `${obj.activity.type}-${obj.activity.action}`; - }, + if (user) { + return { type: 'user', content: user }; + } + + const integration = await Integrations.findOne({ _id: activityLog.createdBy }); + + if (integration) { + const brand = await Brands.findOne({ _id: integration.brandId }); + return { type: 'brand', content: brand }; + } - /** - * Finds content of the activity - */ - content(obj: IActivityLogDocument) { - return obj.activity.content; + return; }, - /** - * Finds content of the activity - */ - async by(obj: IActivityLogDocument) { - const performedBy = obj.performedBy; + async contentTypeDetail(activityLog: IActivityLog) { + const { contentType, contentId, content } = activityLog; - if (!performedBy) { - return null; + let item = {}; + + switch (contentType) { + case 'deal': + item = await Deals.getDeal(contentId); + break; + case 'task': + item = await Tasks.getTask(contentId); + break; + case 'growthHack': + item = await GrowthHacks.getGrowthHack(contentId); + break; + case 'ticket': + item = await Tickets.getTicket(contentId); + break; + case 'checklist': + item = (await Checklists.findOne({ _id: content._id })) || {}; + break; + case 'checklistitem': + item = (await ChecklistItems.findOne({ _id: content._id })) || {}; + break; } - if (performedBy.type === ACTIVITY_PERFORMER_TYPES.USER) { - const user = await Users.findOne({ _id: performedBy.id }); + return item; + }, + + async contentDetail(activityLog: IActivityLog) { + const { action, content, contentType, contentId } = activityLog; - if (!user) { - return null; + if (action === ACTIVITY_ACTIONS.MOVED) { + let item = {}; + + switch (contentType) { + case 'deal': + item = await Deals.getDeal(contentId); + break; + case 'task': + item = await Tasks.getTask(contentId); + break; + case 'growthHack': + item = await GrowthHacks.getGrowthHack(contentId); + break; + case 'ticket': + item = await Tickets.getTicket(contentId); + break; + } + + const { oldStageId, destinationStageId } = content; + + const destinationStage = await Stages.findOne({ _id: destinationStageId }); + const oldStage = await Stages.findOne({ _id: oldStageId }); + + if (destinationStage && oldStage) { + return { + destinationStage: destinationStage.name, + oldStage: oldStage.name, + item, + }; } return { - _id: user._id, - type: performedBy.type, - details: user.details, + text: content.text, }; } - return { - type: performedBy.type, - details: {}, - }; + if (action === ACTIVITY_ACTIONS.MERGE) { + let result = {}; + + switch (contentType) { + case 'company': + result = await Companies.find({ _id: { $in: activityLog.content } }); + break; + case 'customer': + result = await Customers.find({ _id: { $in: activityLog.content } }); + break; + } + + return result; + } + + if (action === ACTIVITY_ACTIONS.ASSIGNEE) { + let addedUsers: IUserDocument[] = []; + let removedUsers: IUserDocument[] = []; + + if (content) { + addedUsers = await Users.find({ _id: { $in: content.addedUserIds } }); + removedUsers = await Users.find({ _id: { $in: content.removedUserIds } }); + } + + return { addedUsers, removedUsers }; + } }, }; diff --git a/src/data/resolvers/boardUtils.ts b/src/data/resolvers/boardUtils.ts index 740c9aeb1..d69e7eeff 100644 --- a/src/data/resolvers/boardUtils.ts +++ b/src/data/resolvers/boardUtils.ts @@ -1,151 +1,116 @@ -import { Boards, Pipelines, Stages } from '../../db/models'; +import { Boards, ChecklistItems, Checklists, Conformities, PipelineLabels, Pipelines, Stages } from '../../db/models'; +import { getCollection, getNewOrder } from '../../db/models/boardUtils'; import { NOTIFICATION_TYPES } from '../../db/models/definitions/constants'; +import { IDealDocument } from '../../db/models/definitions/deals'; +import { ITaskDocument } from '../../db/models/definitions/tasks'; +import { ITicketDocument } from '../../db/models/definitions/tickets'; import { IUserDocument } from '../../db/models/definitions/users'; import { can } from '../permissions/utils'; import { checkLogin } from '../permissions/wrappers'; import utils from '../utils'; export const notifiedUserIds = async (item: any) => { - const userIds: string[] = []; + let userIds: string[] = []; - if (item.assignedUserIds) { - userIds.concat(item.assignedUserIds); - } + userIds = userIds.concat(item.assignedUserIds || []); - if (item.watchedUserIds) { - userIds.concat(item.watchedUserIds); - } + userIds = userIds.concat(item.watchedUserIds || []); - const stage = await Stages.getStage(item.stageId || ''); - const pipeline = await Pipelines.getPipeline(stage.pipelineId || ''); + const stage = await Stages.getStage(item.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); - if (pipeline.watchedUserIds) { - userIds.concat(pipeline.watchedUserIds); - } + userIds = userIds.concat(pipeline.watchedUserIds || []); return userIds; }; +export interface IBoardNotificationParams { + item: IDealDocument; + user: IUserDocument; + type: string; + action?: string; + content?: string; + contentType: string; + invitedUsers?: string[]; + removedUsers?: string[]; +} + /** * Send notification to all members of this content except the sender */ -export const sendNotifications = async ( - stageId: string, - user: IUserDocument, - type: string, - userIds: string[], - content: string, - contentType: string, -) => { - const stage = await Stages.findOne({ _id: stageId }); - - if (!stage) { - throw new Error('Stage not found'); +export const sendNotifications = async ({ + item, + user, + type, + action, + content, + contentType, + invitedUsers, + removedUsers, +}: IBoardNotificationParams) => { + const stage = await Stages.getStage(item.stageId); + + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + + const title = `${contentType} updated`; + + if (!content) { + content = `${contentType} '${item.name}'`; } - const pipeline = await Pipelines.findOne({ _id: stage.pipelineId }); + let route = ''; - if (!pipeline) { - throw new Error('Pipeline not found'); + if (contentType === 'ticket') { + route = '/inbox'; } - await utils.sendNotification({ - createdUser: user._id, + const usersToExclude = [...(removedUsers || []), ...(invitedUsers || []), user._id]; + + const notificationDoc = { + createdUser: user, + title, + contentType, + contentTypeId: item._id, notifType: type, - title: content, + action: action ? action : `has updated ${contentType}`, content, - link: `/${contentType}/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}`, - - // exclude current user - receivers: userIds.filter(id => id !== user._id), - }); -}; - -export const manageNotifications = async (collection: any, item: any, user: IUserDocument, type: string) => { - const { _id } = item; - const oldItem = await collection.findOne({ _id }); - - const oldUserIds = await notifiedUserIds(oldItem); - const userIds = await notifiedUserIds(item); - - // new assignee users - const newUserIds = userIds.filter(userId => oldUserIds.indexOf(userId) < 0); - - if (newUserIds.length > 0) { - await sendNotifications( - item.stageId || '', - user, - NOTIFICATION_TYPES[`${type.toUpperCase()}_ADD`], - newUserIds, - `'{userName}' invited you to the ${type}: '${item.name}'.`, - type, - ); - } - - // remove from assignee users - const removedUserIds = oldUserIds.filter(userId => userIds.indexOf(userId) < 0); - - if (removedUserIds.length > 0) { - await sendNotifications( - item.stageId || '', - user, - NOTIFICATION_TYPES[`${type.toUpperCase()}_REMOVE_ASSIGN`], - removedUserIds, - `'{userName}' removed you from ${type}: '${item.name}'.`, - type, - ); + link: `${route}/${contentType}/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}&itemId=${item._id}`, + + // exclude current user, invited user and removed users + receivers: (await notifiedUserIds(item)).filter(id => { + return usersToExclude.indexOf(id) < 0; + }), + }; + + if (removedUsers && removedUsers.length > 0) { + await utils.sendNotification({ + ...notificationDoc, + notifType: NOTIFICATION_TYPES[`${contentType.toUpperCase()}_REMOVE_ASSIGN`], + action: `removed you from ${contentType}`, + content: `'${item.name}'`, + receivers: removedUsers.filter(id => id !== user._id), + }); } - // dont assignee change and other edit - if (removedUserIds.length === 0 && newUserIds.length === 0) { - await sendNotifications( - item.stageId || '', - user, - NOTIFICATION_TYPES[`${type.toUpperCase()}_EDIT`], - userIds, - `'{userName}' edited your ${type} '${item.name}'`, - type, - ); + if (invitedUsers && invitedUsers.length > 0) { + await utils.sendNotification({ + ...notificationDoc, + notifType: NOTIFICATION_TYPES[`${contentType.toUpperCase()}_ADD`], + action: `invited you to the ${contentType}: `, + content: `'${item.name}'`, + receivers: invitedUsers.filter(id => id !== user._id), + }); } -}; - -export const itemsChange = async (collection: any, item: any, type: string, destinationStageId: string) => { - const oldItem = await collection.findOne({ _id: item._id }); - const oldStageId = oldItem ? oldItem.stageId || '' : ''; - - let content = `'{userName}' changed order your ${type}:'${item.name}'`; - - if (oldStageId !== destinationStageId) { - const stage = await Stages.findOne({ _id: destinationStageId }); - - if (!stage) { - throw new Error('Stage not found'); - } - content = `'{userName}' moved your ${type} '${item.name}' to the '${stage.name}'.`; - } - - return content; + await utils.sendNotification({ + ...notificationDoc, + }); }; export const boardId = async (item: any) => { - const stage = await Stages.findOne({ _id: item.stageId }); - - if (!stage) { - return null; - } - - const pipeline = await Pipelines.findOne({ _id: stage.pipelineId }); - - if (!pipeline) { - return null; - } - - const board = await Boards.findOne({ _id: pipeline.boardId }); - - if (!board) { - return null; - } + const stage = await Stages.getStage(item.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + const board = await Boards.getBoard(pipeline.boardId); return board._id; }; @@ -159,6 +124,7 @@ const PERMISSION_MAP = { pipelinesEdit: 'dealPipelinesEdit', pipelinesRemove: 'dealPipelinesRemove', pipelinesWatch: 'dealPipelinesWatch', + stagesEdit: 'dealStagesEdit', }, ticket: { boardsAdd: 'ticketBoardsAdd', @@ -168,6 +134,7 @@ const PERMISSION_MAP = { pipelinesEdit: 'ticketPipelinesEdit', pipelinesRemove: 'ticketPipelinesRemove', pipelinesWatch: 'ticketPipelinesWatch', + stagesEdit: 'ticketStagesEdit', }, task: { boardsAdd: 'taskBoardsAdd', @@ -177,6 +144,22 @@ const PERMISSION_MAP = { pipelinesEdit: 'taskPipelinesEdit', pipelinesRemove: 'taskPipelinesRemove', pipelinesWatch: 'taskPipelinesWatch', + stagesEdit: 'taskStagesEdit', + }, + growthHack: { + boardsAdd: 'growthHackBoardsAdd', + boardsEdit: 'growthHackBoardsEdit', + boardsRemove: 'growthHackBoardsRemove', + pipelinesAdd: 'growthHackPipelinesAdd', + pipelinesEdit: 'growthHackPipelinesEdit', + pipelinesRemove: 'growthHackPipelinesRemove', + pipelinesWatch: 'growthHackPipelinesWatch', + stagesEdit: 'growthHackStagesEdit', + templatesAdd: 'growthHackTemplatesAdd', + templatesEdit: 'growthHackTemplatesEdit', + templatesRemove: 'growthHackTemplatesRemove', + templatesDuplicate: 'growthHackTemplatesDuplicate', + showTemplates: 'showGrowthHackTemplates', }, }; @@ -197,3 +180,155 @@ export const checkPermission = async (type: string, user: IUserDocument, mutatio return; }; + +export const createConformity = async ({ + companyIds, + customerIds, + mainType, + mainTypeId, +}: { + companyIds?: string[]; + customerIds?: string[]; + mainType: string; + mainTypeId: string; +}) => { + for (const companyId of companyIds || []) { + await Conformities.addConformity({ + mainType, + mainTypeId, + relType: 'company', + relTypeId: companyId, + }); + } + + for (const customerId of customerIds || []) { + await Conformities.addConformity({ + mainType, + mainTypeId, + relType: 'customer', + relTypeId: customerId, + }); + } +}; + +interface ILabelParams { + item: IDealDocument | ITaskDocument | ITicketDocument; + doc: any; + user: IUserDocument; +} + +/** + * Copies pipeline labels alongside deal/task/tickets when they are moved between different pipelines. + */ +export const copyPipelineLabels = async (params: ILabelParams) => { + const { item, doc, user } = params; + + const oldStage = await Stages.findOne({ _id: item.stageId }); + const newStage = await Stages.findOne({ _id: doc.stageId }); + + if (!(oldStage && newStage)) { + throw new Error('Stage not found'); + } + + if (oldStage.pipelineId === newStage.pipelineId) { + return; + } + + const oldLabels = await PipelineLabels.find({ _id: { $in: item.labelIds } }); + const updatedLabelIds: string[] = []; + + for (const label of oldLabels) { + const filter = { + name: label.name, + colorCode: label.colorCode, + pipelineId: newStage.pipelineId, + }; + + const exists = await PipelineLabels.findOne(filter); + + if (!exists) { + const newLabel = await PipelineLabels.createPipelineLabel({ + ...filter, + createdAt: new Date(), + createdBy: user._id, + }); + + updatedLabelIds.push(newLabel._id); + } else { + updatedLabelIds.push(exists._id); + } + } // end label loop + + await PipelineLabels.labelsLabel(newStage.pipelineId, item._id, updatedLabelIds); +}; + +interface IChecklistParams { + contentType: string; + contentTypeId: string; + targetContentId: string; + user: IUserDocument; +} + +/** + * Copies checklists of board item + */ +export const copyChecklists = async (params: IChecklistParams) => { + const { contentType, contentTypeId, targetContentId, user } = params; + + const checklists = await Checklists.find({ contentType, contentTypeId }); + + for (const list of checklists) { + const checklist = await Checklists.createChecklist( + { + contentType, + contentTypeId: targetContentId, + title: `${list.title}-copied`, + }, + user, + ); + + const items = await ChecklistItems.find({ checklistId: list._id }); + + for (const item of items) { + await ChecklistItems.createChecklistItem( + { + isChecked: false, + checklistId: checklist._id, + content: item.content, + }, + user, + ); + } + } // end checklist loop +}; + +export const prepareBoardItemDoc = async (_id: string, type: string, userId: string) => { + const collection = await getCollection(type); + const item = await collection.findOne({ _id }); + + const doc = { + ...item, + userId, + modifiedBy: userId, + watchedUserIds: [userId], + assignedUserIds: item.assignedUserIds, + name: `${item.name}-copied`, + initialStageId: item.initialStageId, + stageId: item.stageId, + description: item.description, + priority: item.priority, + labelIds: item.labelIds, + order: await getNewOrder({ collection, stageId: item.stageId, aboveItemId: item._id }), + + attachments: (item.attachments || []).map(a => ({ + url: a.url, + name: a.name, + type: a.type, + size: a.size, + })), + }; + + delete doc._id; + + return doc; +}; diff --git a/src/data/resolvers/boards.ts b/src/data/resolvers/boards.ts index 36241ab38..830a4f4ae 100644 --- a/src/data/resolvers/boards.ts +++ b/src/data/resolvers/boards.ts @@ -1,9 +1,9 @@ import { Pipelines } from '../../db/models'; import { IBoardDocument } from '../../db/models/definitions/boards'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; export default { - pipelines(board: IBoardDocument, {}, { user }: { user: IUserDocument }) { + pipelines(board: IBoardDocument, {}, { user }: IContext) { if (user.isOwner) { return Pipelines.find({ boardId: board._id }); } diff --git a/src/data/resolvers/brand.ts b/src/data/resolvers/brand.ts index 1688ecb5f..ee9fe2bd5 100644 --- a/src/data/resolvers/brand.ts +++ b/src/data/resolvers/brand.ts @@ -3,6 +3,6 @@ import { IBrandDocument } from '../../db/models/definitions/brands'; export default { integrations(brand: IBrandDocument) { - return Integrations.find({ brandId: brand._id }); + return Integrations.findIntegrations({ brandId: brand._id }); }, }; diff --git a/src/data/resolvers/channel.ts b/src/data/resolvers/channel.ts index 49f8b54c2..fd3a1079d 100644 --- a/src/data/resolvers/channel.ts +++ b/src/data/resolvers/channel.ts @@ -1,8 +1,19 @@ -import { Integrations } from '../../db/models'; +import { Integrations, Users } from '../../db/models'; import { IChannelDocument } from '../../db/models/definitions/channels'; export default { integrations(channel: IChannelDocument) { - return Integrations.find({ _id: { $in: channel.integrationIds } }); + return Integrations.findIntegrations({ _id: { $in: channel.integrationIds } }); + }, + + members(channel: IChannelDocument) { + const selector = { + _id: 1, + email: 1, + 'details.avatar': 1, + 'details.fullName': 1, + }; + + return Users.find({ _id: { $in: channel.memberIds }, isActive: { $ne: false } }).select(selector); }, }; diff --git a/src/data/resolvers/checklists.ts b/src/data/resolvers/checklists.ts new file mode 100644 index 000000000..96ce13d64 --- /dev/null +++ b/src/data/resolvers/checklists.ts @@ -0,0 +1,22 @@ +import { ChecklistItems } from '../../db/models'; +import { IChecklistDocument } from '../../db/models/definitions/checklists'; + +export default { + items(checklist: IChecklistDocument) { + return ChecklistItems.find({ checklistId: checklist._id }).sort({ order: 1 }); + }, + + async percent(checklist: IChecklistDocument) { + const items = await ChecklistItems.find({ checklistId: checklist._id }); + + if (items.length === 0) { + return 0; + } + + const checkedItems = items.filter(item => { + return item.isChecked; + }); + + return (checkedItems.length / items.length) * 100; + }, +}; diff --git a/src/data/resolvers/company.ts b/src/data/resolvers/company.ts index cca0527b7..0525344ee 100644 --- a/src/data/resolvers/company.ts +++ b/src/data/resolvers/company.ts @@ -1,9 +1,15 @@ -import { Companies, Customers, Deals, Tags, Users } from '../../db/models'; +import { Companies, Conformities, Customers, Tags, Users } from '../../db/models'; import { ICompanyDocument } from '../../db/models/definitions/companies'; export default { - customers(company: ICompanyDocument) { - return Customers.find({ companyIds: { $in: [company._id] } }); + async customers(company: ICompanyDocument) { + const customerIds = await Conformities.savedConformity({ + mainType: 'company', + mainTypeId: company._id, + relTypes: ['customer'], + }); + + return Customers.find({ _id: { $in: customerIds || [] } }); }, getTags(company: ICompanyDocument) { @@ -17,8 +23,4 @@ export default { parentCompany(company: ICompanyDocument) { return Companies.findOne({ _id: company.parentCompanyId }); }, - - deals(company: ICompanyDocument) { - return Deals.find({ companyIds: { $in: [company._id] || [] } }); - }, }; diff --git a/src/data/resolvers/conversation.ts b/src/data/resolvers/conversation.ts index 0eb38c466..79f5d3346 100644 --- a/src/data/resolvers/conversation.ts +++ b/src/data/resolvers/conversation.ts @@ -1,5 +1,8 @@ import { ConversationMessages, Customers, Integrations, Tags, Users } from '../../db/models'; +import { MESSAGE_TYPES } from '../../db/models/definitions/constants'; import { IConversationDocument } from '../../db/models/definitions/conversations'; +import { debugExternalApi } from '../../debuggers'; +import { IContext } from '../types'; export default { /** @@ -43,7 +46,85 @@ export default { }); }, + async facebookPost(conv: IConversationDocument, _args, { dataSources }: IContext) { + const integration = await Integrations.findOne({ _id: conv.integrationId }).lean(); + + if (integration && integration.kind !== 'facebook-post') { + return null; + } + + try { + const response = await dataSources.IntegrationsAPI.fetchApi('/facebook/get-post', { + erxesApiId: conv._id, + integrationId: integration._id, + }); + + return response; + } catch (e) { + debugExternalApi(e); + return null; + } + }, + + async callProAudio(conv: IConversationDocument, _args, { dataSources, user }: IContext) { + const integration = await Integrations.findOne({ _id: conv.integrationId }).lean(); + + if (integration && integration.kind !== 'callpro') { + return null; + } + + if (user.isOwner || user._id === conv.assignedUserId) { + try { + const response = await dataSources.IntegrationsAPI.fetchApi('/callpro/get-audio', { + erxesApiId: conv._id, + integrationId: integration._id, + }); + + return response ? response.audioSrc : ''; + } catch (e) { + debugExternalApi(e); + return null; + } + } + + return null; + }, + tags(conv: IConversationDocument) { return Tags.find({ _id: { $in: conv.tagIds || [] } }); }, + + async videoCallData(conversation: IConversationDocument, _args, { dataSources }: IContext) { + const message = await ConversationMessages.findOne({ + conversationId: conversation._id, + contentType: MESSAGE_TYPES.VIDEO_CALL, + }); + + if (!message) { + return null; + } + + try { + const response = await dataSources.IntegrationsAPI.fetchApi('/daily/get-active-room', { + erxesApiConversationId: conversation._id, + }); + + return response; + } catch (e) { + debugExternalApi(e); + return null; + } + }, + + async productBoardLink(conversation: IConversationDocument, _args, { dataSources }: IContext) { + try { + const response = await dataSources.IntegrationsAPI.fetchApi('/productBoard/note', { + erxesApiId: conversation._id, + }); + return response; + } catch (e) { + debugExternalApi(e); + return null; + } + }, }; diff --git a/src/data/resolvers/conversationMessage.ts b/src/data/resolvers/conversationMessage.ts index fe71fa3cd..8a5d75ffc 100644 --- a/src/data/resolvers/conversationMessage.ts +++ b/src/data/resolvers/conversationMessage.ts @@ -1,5 +1,8 @@ -import { Customers, Users } from '../../db/models'; +import { Conversations, Customers, Integrations, Users } from '../../db/models'; +import { MESSAGE_TYPES } from '../../db/models/definitions/constants'; import { IMessageDocument } from '../../db/models/definitions/conversationMessages'; +import { debugExternalApi } from '../../debuggers'; +import { IContext } from '../types'; export default { user(message: IMessageDocument) { @@ -9,4 +12,52 @@ export default { customer(message: IMessageDocument) { return Customers.findOne({ _id: message.customerId }); }, + + async mailData(message: IMessageDocument, _args, { dataSources }: IContext) { + const conversation = await Conversations.findOne({ _id: message.conversationId }).lean(); + + if (!conversation || message.internal) { + return null; + } + + const integration = await Integrations.findOne({ _id: conversation.integrationId }).lean(); + + if (!integration) { + return null; + } + + const { kind } = integration; + + // Not mail + if (!kind.includes('nylas') && kind !== 'gmail') { + return null; + } + + const path = kind.includes('nylas') ? `/nylas/get-message` : `/${kind}/get-message`; + + return dataSources.IntegrationsAPI.fetchApi(path, { + erxesApiMessageId: message._id, + integrationId: integration._id, + }); + }, + + async videoCallData(message: IMessageDocument, _args, { dataSources }: IContext) { + const conversation = await Conversations.findOne({ _id: message.conversationId }); + + if (!conversation || message.internal) { + return null; + } + + if (message.contentType !== MESSAGE_TYPES.VIDEO_CALL) { + return null; + } + + try { + const response = await dataSources.IntegrationsAPI.fetchApi('/daily/room', { erxesApiMessageId: message._id }); + return response; + } catch (e) { + debugExternalApi(e); + return null; + } + }, }; diff --git a/src/data/resolvers/customScalars.ts b/src/data/resolvers/customScalars.ts index 40701a32b..c644a28f6 100644 --- a/src/data/resolvers/customScalars.ts +++ b/src/data/resolvers/customScalars.ts @@ -36,13 +36,18 @@ export default { parseValue(value) { return new Date(value); // value from the client }, - serialize(value) { - if (value.getTime) { - return value.getTime(); // value sent to the client + serialize: value => { + if (value instanceof Date) { + return value; } - return new Date(value).getTime(); + if (value.toISOString) { + return value.toISOString(); + } + + return new Date(value); }, + parseLiteral(ast) { if (ast.kind === Kind.INT) { return parseInt(ast.value, 10); // ast value is always in string format diff --git a/src/data/resolvers/customer.ts b/src/data/resolvers/customer.ts index 2a5263c76..c00208bb7 100644 --- a/src/data/resolvers/customer.ts +++ b/src/data/resolvers/customer.ts @@ -1,55 +1,66 @@ -import { Companies, Conversations, Deals, Integrations, Tags, Users } from '../../db/models'; +import { Companies, Conformities, Conversations, Integrations, Tags, Users } from '../../db/models'; import { ICustomerDocument } from '../../db/models/definitions/customers'; - -interface IMessengerCustomData { - name: string; - value: string; -} +import { fetchElk } from '../../elasticsearch'; export default { integration(customer: ICustomerDocument) { return Integrations.findOne({ _id: customer.integrationId }); }, - getIntegrationData(customer: ICustomerDocument) { - return { - messenger: customer.messengerData || {}, - // TODO: Add other integration data - }; + getTags(customer: ICustomerDocument) { + return Tags.find({ _id: { $in: customer.tagIds || [] } }); }, - getMessengerCustomData(customer: ICustomerDocument) { - const results: IMessengerCustomData[] = []; - const messengerData: any = customer.messengerData || {}; - const data = messengerData.customData || {}; + async urlVisits(customer: ICustomerDocument) { + const response = await fetchElk( + 'search', + 'events', + { + _source: ['createdAt', 'count', 'attributes'], + query: { + bool: { + must: [ + { + term: { customerId: customer._id }, + }, + { + term: { name: 'viewPage' }, + }, + ], + }, + }, + }, + '', + { hits: { hits: [] } }, + ); - Object.keys(data).forEach(key => { - results.push({ - name: key.replace(/_/g, ' '), - value: data[key], - }); - }); + return response.hits.hits.map(hit => { + const source = hit._source; + const firstAttribute = source.attributes[0] || {}; - return results; - }, - - getTags(customer: ICustomerDocument) { - return Tags.find({ _id: { $in: customer.tagIds || [] } }); + return { + createdAt: source.createdAt, + count: source.count, + url: firstAttribute.value, + }; + }); }, conversations(customer: ICustomerDocument) { return Conversations.find({ customerId: customer._id }); }, - companies(customer: ICustomerDocument) { - return Companies.find({ _id: { $in: customer.companyIds || [] } }); + async companies(customer: ICustomerDocument) { + const companyIds = await Conformities.savedConformity({ + mainType: 'customer', + mainTypeId: customer._id, + relTypes: ['company'], + }); + + return Companies.find({ _id: { $in: companyIds || [] } }).limit(10); }, owner(customer: ICustomerDocument) { return Users.findOne({ _id: customer.ownerId }); }, - - deals(customer: ICustomerDocument) { - return Deals.find({ customerIds: { $in: [customer._id] } }); - }, }; diff --git a/src/data/resolvers/deals.ts b/src/data/resolvers/deals.ts index c38449ffc..0109bb249 100644 --- a/src/data/resolvers/deals.ts +++ b/src/data/resolvers/deals.ts @@ -1,40 +1,86 @@ -import { Companies, Customers, Pipelines, Products, Stages, Users } from '../../db/models'; +import { + Companies, + Conformities, + Customers, + Fields, + Notifications, + PipelineLabels, + Pipelines, + Products, + Stages, + Users, +} from '../../db/models'; import { IDealDocument } from '../../db/models/definitions/deals'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { - companies(deal: IDealDocument) { - return Companies.find({ _id: { $in: deal.companyIds || [] } }); + async companies(deal: IDealDocument) { + const companyIds = await Conformities.savedConformity({ + mainType: 'deal', + mainTypeId: deal._id, + relTypes: ['company'], + }); + + return Companies.find({ _id: { $in: companyIds } }); }, - customers(deal: IDealDocument) { - return Customers.find({ _id: { $in: deal.customerIds || [] } }); + async customers(deal: IDealDocument) { + const customerIds = await Conformities.savedConformity({ + mainType: 'deal', + mainTypeId: deal._id, + relTypes: ['customer'], + }); + + return Customers.find({ _id: { $in: customerIds } }); }, async products(deal: IDealDocument) { const products: any = []; for (const data of deal.productsData || []) { - const product = await Products.findOne({ _id: data.productId }); - - // Add product object to resulting list - if (data && product) { - products.push({ - ...data.toJSON(), - product: product.toJSON(), - }); + if (!data.productId) { + continue; + } + + const product = await Products.getProduct({ _id: data.productId }); + + const { customFieldsData } = product; + + const customFields = []; + + for (const customFieldData of customFieldsData || []) { + const field = await Fields.findOne({ _id: customFieldData.field }); + + if (field) { + customFields[customFieldData.field] = { + text: field.text, + data: customFieldData.value, + }; + } } + + product.customFieldsData = customFields; + + products.push({ + ...(typeof data.toJSON === 'function' ? data.toJSON() : data), + product, + }); } return products; }, amount(deal: IDealDocument) { - const data = deal.productsData || []; + const productsData = deal.productsData || []; const amountsMap = {}; - data.forEach(product => { + productsData.forEach(product => { + // Tick paid or used is false then exclude + if (!product.tickUsed) { + return; + } + const type = product.currency; if (type) { @@ -50,15 +96,11 @@ export default { }, assignedUsers(deal: IDealDocument) { - return Users.find({ _id: { $in: deal.assignedUserIds } }); + return Users.find({ _id: { $in: deal.assignedUserIds || [] } }); }, async pipeline(deal: IDealDocument) { - const stage = await Stages.findOne({ _id: deal.stageId }); - - if (!stage) { - return null; - } + const stage = await Stages.getStage(deal.stageId); return Pipelines.findOne({ _id: stage.pipelineId }); }, @@ -68,16 +110,28 @@ export default { }, stage(deal: IDealDocument) { - return Stages.findOne({ _id: deal.stageId }); + return Stages.getStage(deal.stageId); }, - isWatched(deal: IDealDocument, _args, { user }: { user: IUserDocument }) { + isWatched(deal: IDealDocument, _args, { user }: IContext) { const watchedUserIds = deal.watchedUserIds || []; - if (watchedUserIds.includes(user._id)) { + if (watchedUserIds && watchedUserIds.includes(user._id)) { return true; } return false; }, + + hasNotified(deal: IDealDocument, _args, { user }: IContext) { + return Notifications.checkIfRead(user._id, deal._id); + }, + + labels(deal: IDealDocument) { + return PipelineLabels.find({ _id: { $in: deal.labelIds || [] } }); + }, + + createdUser(deal: IDealDocument) { + return Users.findOne({ _id: deal.userId }); + }, }; diff --git a/src/data/resolvers/emailDeliveries.ts b/src/data/resolvers/emailDeliveries.ts new file mode 100644 index 000000000..030e1e244 --- /dev/null +++ b/src/data/resolvers/emailDeliveries.ts @@ -0,0 +1,14 @@ +import { Integrations, Users } from '../../db/models'; +import { IEmailDeliveriesDocument } from '../../db/models/definitions/emailDeliveries'; + +export default { + async fromUser(emailDelivery: IEmailDeliveriesDocument) { + return Users.findOne({ _id: emailDelivery.userId }) || {}; + }, + + async fromEmail(emailDelivery: IEmailDeliveriesDocument) { + const integration = await Integrations.findOne({ _id: emailDelivery.from }); + + return integration ? integration.name : ''; + }, +}; diff --git a/src/data/resolvers/engage.ts b/src/data/resolvers/engage.ts index 35368991a..de394a30d 100644 --- a/src/data/resolvers/engage.ts +++ b/src/data/resolvers/engage.ts @@ -1,7 +1,14 @@ -import { Brands, Segments, Tags, Users } from '../../db/models'; +import { Brands, EngageMessages, Integrations, Segments, Tags, Users } from '../../db/models'; import { IEngageMessageDocument } from '../../db/models/definitions/engages'; +import { IContext } from '../types'; -export default { +export const deliveryReport = { + engage(root) { + return EngageMessages.findOne({ _id: root.engageMessageId }, { title: 1 }).lean(); + }, +}; + +export const message = { segments(engageMessage: IEngageMessageDocument) { return Segments.find({ _id: { $in: engageMessage.segmentIds } }); }, @@ -29,4 +36,24 @@ export default { return Brands.findOne({ _id: messenger.brandId }); } }, + + stats(engageMessage: IEngageMessageDocument, _args, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesStats(engageMessage._id); + }, + + logs(engageMessage: IEngageMessageDocument, _args, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesLogs(engageMessage._id); + }, + + smsStats(engageMessage: IEngageMessageDocument, _args, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesSmsStats(engageMessage._id); + }, + + fromIntegration(engageMessage: IEngageMessageDocument) { + if (engageMessage.shortMessage && engageMessage.shortMessage.fromIntegrationId) { + return Integrations.getIntegration(engageMessage.shortMessage.fromIntegrationId); + } + + return null; + }, }; diff --git a/src/data/resolvers/field.ts b/src/data/resolvers/field.ts index 0a3e8b159..ddd52009d 100644 --- a/src/data/resolvers/field.ts +++ b/src/data/resolvers/field.ts @@ -2,6 +2,10 @@ import { Fields, Users } from '../../db/models'; import { IFieldDocument, IFieldGroupDocument } from '../../db/models/definitions/fields'; export const field = { + name(root: IFieldDocument) { + return `erxes-form-field-${root._id}`; + }, + lastUpdatedUser(root: IFieldDocument) { const { lastUpdatedUserId } = root; diff --git a/src/data/resolvers/forms.ts b/src/data/resolvers/forms.ts index abcef6d4f..59783478d 100644 --- a/src/data/resolvers/forms.ts +++ b/src/data/resolvers/forms.ts @@ -1,8 +1,14 @@ -import { Users } from '../../db/models'; +import { Fields, Users } from '../../db/models'; import { IFormDocument } from '../../db/models/definitions/forms'; export default { createdUser(form: IFormDocument) { return Users.findOne({ _id: form.createdUserId }); }, + + fields(form: IFormDocument) { + return Fields.find({ contentType: 'form', contentTypeId: form._id }).sort({ + order: 1, + }); + }, }; diff --git a/src/data/resolvers/growthHacks.ts b/src/data/resolvers/growthHacks.ts new file mode 100644 index 000000000..37f5536ac --- /dev/null +++ b/src/data/resolvers/growthHacks.ts @@ -0,0 +1,100 @@ +import { Fields, FormSubmissions, PipelineLabels, Pipelines, Stages, Users } from '../../db/models'; +import { IGrowthHackDocument } from '../../db/models/definitions/growthHacks'; +import { IUserDocument } from '../../db/models/definitions/users'; +import { boardId } from './boardUtils'; +import { IFieldsQuery } from './queries/fields'; + +export default { + async formSubmissions(growthHack: IGrowthHackDocument) { + const stage = await Stages.getStage(growthHack.stageId); + + const result = {}; + + if (stage.formId) { + const submissions = await FormSubmissions.find({ + contentTypeId: growthHack._id, + contentType: 'growthHack', + formId: stage.formId, + }); + + for (const submission of submissions) { + if (submission.formFieldId) { + result[submission.formFieldId] = submission.value; + } + } + } + + return result; + }, + + async formFields(growthHack: IGrowthHackDocument) { + const stage = await Stages.getStage(growthHack.stageId); + + const query: IFieldsQuery = { contentType: 'form' }; + + if (stage.formId) { + query.contentTypeId = stage.formId; + } + + return Fields.find(query).sort({ order: 1 }); + }, + + assignedUsers(growthHack: IGrowthHackDocument) { + return Users.find({ _id: { $in: growthHack.assignedUserIds || [] } }); + }, + + votedUsers(growthHack: IGrowthHackDocument) { + return Users.find({ _id: { $in: growthHack.votedUserIds || [] } }); + }, + + isVoted(growthHack: IGrowthHackDocument, _args, { user }: { user: IUserDocument }) { + return growthHack.votedUserIds && growthHack.votedUserIds.length > 0 + ? growthHack.votedUserIds.indexOf(user._id) !== -1 + : false; + }, + + async pipeline(growthHack: IGrowthHackDocument) { + const stage = await Stages.getStage(growthHack.stageId); + + return Pipelines.findOne({ _id: stage.pipelineId }); + }, + + boardId(growthHack: IGrowthHackDocument) { + return boardId(growthHack); + }, + + async formId(growthHack: IGrowthHackDocument) { + const stage = await Stages.getStage(growthHack.stageId); + + return stage.formId; + }, + + async scoringType(growthHack: IGrowthHackDocument) { + const stage = await Stages.getStage(growthHack.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + + return pipeline.hackScoringType; + }, + + stage(growthHack: IGrowthHackDocument) { + return Stages.getStage(growthHack.stageId); + }, + + isWatched(growthHack: IGrowthHackDocument, _args, { user }: { user: IUserDocument }) { + const watchedUserIds = growthHack.watchedUserIds || []; + + if (watchedUserIds.includes(user._id)) { + return true; + } + + return false; + }, + + labels(growthHack: IGrowthHackDocument) { + return PipelineLabels.find({ _id: { $in: growthHack.labelIds || [] } }); + }, + + createdUser(growthHack: IGrowthHackDocument) { + return Users.findOne({ _id: growthHack.userId }); + }, +}; diff --git a/src/data/resolvers/index.ts b/src/data/resolvers/index.ts index 45403e116..95a93cc67 100644 --- a/src/data/resolvers/index.ts +++ b/src/data/resolvers/index.ts @@ -3,15 +3,18 @@ import ActivityLog from './activityLog'; import Board from './boards'; import Brand from './brand'; import Channel from './channel'; +import Checklist from './checklists'; import Company from './company'; import Conversation from './conversation'; import ConversationMessage from './conversationMessage'; import Customer from './customer'; import customScalars from './customScalars'; import Deal from './deals'; -import EngageMessage from './engage'; +import EmailDelivery from './emailDeliveries'; +import { deliveryReport as DeliveryReport, message as EngageMessage } from './engage'; import { field, fieldsGroup } from './field'; import Form from './forms'; +import GrowthHack from './growthHacks'; import ImportHistory from './importHistory'; import Integration from './integration'; import InternalNote from './internalNote'; @@ -22,6 +25,8 @@ import Mutation from './mutations'; import Notification from './notification'; import Permission from './permission'; import Pipeline from './pipeline'; +import Product from './product'; +import ProductCategory from './productCategory'; import Query from './queries'; import ResponseTemplate from './responseTemplate'; import Script from './script'; @@ -43,10 +48,13 @@ const resolvers: any = { Channel, Brand, InternalNote, + Checklist, Customer, Company, Segment, EngageMessage, + DeliveryReport, + EmailDelivery, Conversation, ConversationMessage, Deal, @@ -63,6 +71,9 @@ const resolvers: any = { Notification, + Product, + ProductCategory, + ActivityLog, Form, FieldsGroup: fieldsGroup, @@ -74,6 +85,7 @@ const resolvers: any = { Task, UsersGroup, Pipeline, + GrowthHack, }; export default resolvers; diff --git a/src/data/resolvers/integration.ts b/src/data/resolvers/integration.ts index 4877e3839..4c3a3ca6a 100644 --- a/src/data/resolvers/integration.ts +++ b/src/data/resolvers/integration.ts @@ -1,4 +1,5 @@ -import { Brands, Channels, Forms, Tags } from '../../db/models'; +import { Brands, Channels, Forms, MessengerApps, Tags } from '../../db/models'; +import { KIND_CHOICES } from '../../db/models/definitions/constants'; import { IIntegrationDocument } from '../../db/models/definitions/integrations'; export default { @@ -17,4 +18,25 @@ export default { tags(integration: IIntegrationDocument) { return Tags.find({ _id: { $in: integration.tagIds || [] } }); }, + + websiteMessengerApps(integration: IIntegrationDocument) { + if (integration.kind === KIND_CHOICES.MESSENGER) { + return MessengerApps.find({ kind: 'website', 'credentials.integrationId': integration._id }); + } + return []; + }, + + knowledgeBaseMessengerApps(integration: IIntegrationDocument) { + if (integration.kind === KIND_CHOICES.MESSENGER) { + return MessengerApps.find({ kind: 'knowledgebase', 'credentials.integrationId': integration._id }); + } + return []; + }, + + leadMessengerApps(integration: IIntegrationDocument) { + if (integration.kind === KIND_CHOICES.MESSENGER) { + return MessengerApps.find({ kind: 'lead', 'credentials.integrationId': integration._id }); + } + return []; + }, }; diff --git a/src/data/resolvers/knowledgeBaseCategory.ts b/src/data/resolvers/knowledgeBaseCategory.ts index 8b9076410..2e27be40e 100644 --- a/src/data/resolvers/knowledgeBaseCategory.ts +++ b/src/data/resolvers/knowledgeBaseCategory.ts @@ -1,9 +1,26 @@ -import { KnowledgeBaseArticles, KnowledgeBaseTopics } from '../../db/models'; +import { KnowledgeBaseArticles, KnowledgeBaseTopics, Users } from '../../db/models'; +import { PUBLISH_STATUSES } from '../../db/models/definitions/constants'; import { ICategoryDocument } from '../../db/models/definitions/knowledgebase'; export default { articles(category: ICategoryDocument) { - return KnowledgeBaseArticles.find({ _id: { $in: category.articleIds } }); + return KnowledgeBaseArticles.find({ _id: { $in: category.articleIds }, status: PUBLISH_STATUSES.PUBLISH }); + }, + + async authors(category: ICategoryDocument) { + const articles = await KnowledgeBaseArticles.find( + { + _id: { $in: category.articleIds }, + status: PUBLISH_STATUSES.PUBLISH, + }, + { createdBy: 1 }, + ); + + const authorIds = articles.map(article => article.createdBy); + + return Users.find({ + _id: { $in: authorIds }, + }); }, firstTopic(category: ICategoryDocument) { @@ -11,4 +28,11 @@ export default { categoryIds: { $in: [category._id] }, }); }, + + numOfArticles(category: ICategoryDocument) { + return KnowledgeBaseArticles.find({ + _id: { $in: category.articleIds }, + status: PUBLISH_STATUSES.PUBLISH, + }).countDocuments(); + }, }; diff --git a/src/data/resolvers/knowledgeBaseTopic.ts b/src/data/resolvers/knowledgeBaseTopic.ts index 142924daa..d18984027 100644 --- a/src/data/resolvers/knowledgeBaseTopic.ts +++ b/src/data/resolvers/knowledgeBaseTopic.ts @@ -11,6 +11,6 @@ export default { }, color(topic: ITopicDocument) { - return topic.color ? topic.color : ''; + return topic.color || ''; }, }; diff --git a/src/data/resolvers/mutations/boardUtils.ts b/src/data/resolvers/mutations/boardUtils.ts new file mode 100644 index 000000000..91fb9803f --- /dev/null +++ b/src/data/resolvers/mutations/boardUtils.ts @@ -0,0 +1,503 @@ +import resolvers from '..'; +import { ActivityLogs, Boards, Checklists, Conformities, Notifications, Pipelines, Stages } from '../../../db/models'; +import { getCollection, getCompanies, getCustomers, getItem, getNewOrder } from '../../../db/models/boardUtils'; +import { IItemCommonFields, IItemDragCommonFields, IStageDocument } from '../../../db/models/definitions/boards'; +import { BOARD_STATUSES, NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { IDeal, IDealDocument } from '../../../db/models/definitions/deals'; +import { IGrowthHack, IGrowthHackDocument } from '../../../db/models/definitions/growthHacks'; +import { ITaskDocument } from '../../../db/models/definitions/tasks'; +import { ITicket, ITicketDocument } from '../../../db/models/definitions/tickets'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { graphqlPubsub } from '../../../pubsub'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { checkUserIds } from '../../utils'; +import { + copyChecklists, + copyPipelineLabels, + createConformity, + IBoardNotificationParams, + prepareBoardItemDoc, + sendNotifications, +} from '../boardUtils'; + +const itemResolver = async (type: string, item: IItemCommonFields) => { + let resolverType = ''; + + switch (type) { + case 'deal': + resolverType = 'Deal'; + break; + + case 'task': + resolverType = 'Task'; + break; + + case 'ticket': + resolverType = 'Ticket'; + break; + + case 'growthHack': + resolverType = 'GrowthHack'; + break; + } + + const additionInfo = {}; + const resolver = resolvers[resolverType] || {}; + + for (const subResolver of Object.keys(resolver)) { + try { + additionInfo[subResolver] = await resolver[subResolver](item); + } catch (unused) { + continue; + } + } + + return additionInfo; +}; + +export const itemsAdd = async ( + doc: (IDeal | IItemCommonFields | ITicket | IGrowthHack) & { proccessId: string; aboveItemId: string }, + type: string, + user: IUserDocument, + docModifier: any, + createModel: any, +) => { + const collection = getCollection(type); + + doc.initialStageId = doc.stageId; + doc.watchedUserIds = [user._id]; + + const extendedDoc = { + ...docModifier(doc), + modifiedBy: user._id, + userId: user._id, + order: await getNewOrder({ collection, stageId: doc.stageId, aboveItemId: doc.aboveItemId }), + }; + + const item = await createModel(extendedDoc); + + await createConformity({ + mainType: type, + mainTypeId: item._id, + companyIds: doc.companyIds, + customerIds: doc.customerIds, + }); + + await sendNotifications({ + item, + user, + type: NOTIFICATION_TYPES.DEAL_ADD, + action: `invited you to the ${type}`, + content: `'${item.name}'.`, + contentType: type, + }); + + await putCreateLog( + { + type, + newData: extendedDoc, + object: item, + }, + user, + ); + + const stage = await Stages.getStage(item.stageId); + + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId: doc.proccessId, + action: 'itemAdd', + data: { + item, + aboveItemId: doc.aboveItemId, + destinationStageId: stage._id, + }, + }, + }); + + return item; +}; + +export const changeItemStatus = async ({ + type, + item, + status, + proccessId, + stage, +}: { + type: string; + item: any; + status: string; + proccessId: string; + stage: IStageDocument; +}) => { + if (status === 'archived') { + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemRemove', + data: { + item, + oldStageId: item.stageId, + }, + }, + }); + + return; + } + + const collection = getCollection(type); + + const aboveItems = await collection + .find({ + stageId: item.stageId, + status: { $ne: BOARD_STATUSES.ARCHIVED }, + order: { $lt: item.order }, + }) + .sort({ order: -1 }) + .limit(1); + + const aboveItemId = aboveItems[0]?._id || ''; + + // maybe, recovered order includes to oldOrders + await collection.updateOne( + { + _id: item._id, + }, + { + order: await getNewOrder({ + collection, + stageId: item.stageId, + aboveItemId, + }), + }, + ); + + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemAdd', + data: { + item: { ...item._doc, ...(await itemResolver(type, item)) }, + aboveItemId, + destinationStageId: item.stageId, + }, + }, + }); +}; + +export const itemsEdit = async ( + _id: string, + type: string, + oldItem: any, + doc: any, + proccessId: string, + user: IUserDocument, + modelUpate, +) => { + const extendedDoc = { + ...doc, + modifiedAt: new Date(), + modifiedBy: user._id, + }; + + const updatedItem = await modelUpate(_id, extendedDoc); + // labels should be copied to newly moved pipeline + if (doc.stageId) { + await copyPipelineLabels({ item: oldItem, doc, user }); + } + + const notificationDoc: IBoardNotificationParams = { + item: updatedItem, + user, + type: NOTIFICATION_TYPES.TASK_EDIT, + contentType: type, + }; + + const stage = await Stages.getStage(updatedItem.stageId); + + if (doc.status && oldItem.status && oldItem.status !== doc.status) { + const activityAction = doc.status === 'active' ? 'activated' : 'archived'; + + await ActivityLogs.createArchiveLog({ + item: updatedItem, + contentType: type, + action: activityAction, + userId: user._id, + }); + + // order notification + await changeItemStatus({ type, item: updatedItem, status: activityAction, proccessId, stage }); + } + + if (doc.assignedUserIds && doc.assignedUserIds.length > 0) { + const { addedUserIds, removedUserIds } = checkUserIds(oldItem.assignedUserIds, doc.assignedUserIds); + + const activityContent = { addedUserIds, removedUserIds }; + + await ActivityLogs.createAssigneLog({ + contentId: _id, + userId: user._id, + contentType: type, + content: activityContent, + }); + + notificationDoc.invitedUsers = addedUserIds; + notificationDoc.removedUsers = removedUserIds; + } + + await sendNotifications(notificationDoc); + + await putUpdateLog( + { + type, + object: oldItem, + newData: extendedDoc, + updatedDocument: updatedItem, + }, + user, + ); + + const pipelinesChangedIds = [updatedItem._id, stage.pipelineId]; + + for (const pipelinesChangedId of pipelinesChangedIds) { + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: pipelinesChangedId, + proccessId, + action: 'itemUpdate', + data: { + item: { ...updatedItem._doc, ...(await itemResolver(type, updatedItem)) }, + }, + }, + }); + } + + if (oldItem.stageId === updatedItem.stageId) { + return updatedItem; + } + + // if task moves between stages + const { content, action } = await itemMover(user._id, oldItem, type, updatedItem.stageId); + + await sendNotifications({ + item: updatedItem, + user, + type: NOTIFICATION_TYPES.TASK_CHANGE, + content, + action, + contentType: type, + }); + + return updatedItem; +}; + +export const itemMover = async ( + userId: string, + item: IDealDocument | ITaskDocument | ITicketDocument | IGrowthHackDocument, + contentType: string, + destinationStageId: string, +) => { + const oldStageId = item.stageId; + + let action = `changed order of your ${contentType}:`; + let content = `'${item.name}'`; + + if (oldStageId !== destinationStageId) { + const stage = await Stages.getStage(destinationStageId); + const oldStage = await Stages.getStage(oldStageId); + + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + const oldPipeline = await Pipelines.getPipeline(oldStage.pipelineId); + + const board = await Boards.getBoard(pipeline.boardId); + const oldBoard = await Boards.getBoard(oldPipeline.boardId); + + action = `moved '${item.name}' from ${oldBoard.name}-${oldPipeline.name}-${oldStage.name} to `; + + content = `${board.name}-${pipeline.name}-${stage.name}`; + + const activityLogContent = { + oldStageId, + destinationStageId, + text: `${oldStage.name} to ${stage.name}`, + }; + + ActivityLogs.createBoardItemMovementLog(item, contentType, userId, activityLogContent); + + const link = `/${contentType}/board?id=${board._id}&pipelineId=${pipeline._id}&itemId=${item._id}`; + + await Notifications.updateMany({ contentType, contentTypeId: item._id }, { $set: { link } }); + } + + return { content, action }; +}; + +export const itemsChange = async (doc: IItemDragCommonFields, type: string, user: IUserDocument, modelUpdate: any) => { + const collection = getCollection(type); + const { proccessId, itemId, aboveItemId, destinationStageId, sourceStageId } = doc; + + const item = await getItem(type, itemId); + + const extendedDoc = { + modifiedAt: new Date(), + modifiedBy: user._id, + stageId: destinationStageId, + order: await getNewOrder({ collection, stageId: destinationStageId, aboveItemId }), + }; + + const updatedItem = await modelUpdate(itemId, extendedDoc); + + const { content, action } = await itemMover(user._id, item, type, destinationStageId); + + await sendNotifications({ + item, + user, + type: NOTIFICATION_TYPES.DEAL_CHANGE, + content, + action, + contentType: type, + }); + + await putUpdateLog( + { + type, + object: item, + newData: extendedDoc, + updatedDocument: updatedItem, + }, + user, + ); + + // order notification + const stage = await Stages.getStage(item.stageId); + + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'orderUpdated', + data: { + item: { ...item._doc, ...(await itemResolver(type, item)) }, + aboveItemId, + destinationStageId, + oldStageId: sourceStageId, + }, + }, + }); + + return item; +}; + +export const itemsRemove = async (_id: string, type: string, user: IUserDocument) => { + const item = await getItem(type, _id); + + await sendNotifications({ + item, + user, + type: `${type}Delete`, + action: `deleted ${type}:`, + content: `'${item.name}'`, + contentType: type, + }); + + await Conformities.removeConformity({ mainType: type, mainTypeId: item._id }); + await Checklists.removeChecklists(type, item._id); + await ActivityLogs.removeActivityLog(item._id); + + const removed = await item.remove(); + + await putDeleteLog({ type, object: item }, user); + + return removed; +}; + +export const itemsCopy = async ( + _id: string, + proccessId: string, + type: string, + user: IUserDocument, + extraDocParam: string[], + modelCreate: any, +) => { + const item = await getItem(type, _id); + + const doc = await prepareBoardItemDoc(_id, type, user._id); + + for (const param of extraDocParam) { + doc[param] = item[param]; + } + + const clone = await modelCreate(doc); + + const companies = await getCompanies(type, _id); + const customers = await getCustomers(type, _id); + + await createConformity({ + mainType: type, + mainTypeId: clone._id, + customerIds: customers.map(c => c._id), + companyIds: companies.map(c => c._id), + }); + + await copyChecklists({ + contentType: type, + contentTypeId: item._id, + targetContentId: clone._id, + user, + }); + + // order notification + const stage = await Stages.getStage(clone.stageId); + + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemAdd', + data: { + item: { ...clone._doc, ...(await itemResolver(type, clone)) }, + aboveItemId: _id, + destinationStageId: stage._id, + }, + }, + }); + + return clone; +}; + +export const itemsArchive = async (stageId: string, type: string, proccessId: string, user: IUserDocument) => { + const collection = getCollection(type); + + const items = await collection.find({ stageId, status: { $ne: BOARD_STATUSES.ARCHIVED } }); + + await collection.updateMany({ stageId }, { $set: { status: BOARD_STATUSES.ARCHIVED } }); + + for (const item of items) { + await ActivityLogs.createArchiveLog({ + item, + contentType: type, + action: 'archived', + userId: user._id, + }); + } + + // order notification + const stage = await Stages.getStage(stageId); + + graphqlPubsub.publish('pipelinesChanged', { + pipelinesChanged: { + _id: stage.pipelineId, + proccessId, + action: 'itemsRemove', + data: { + destinationStageId: stage._id, + }, + }, + }); + + return 'ok'; +}; diff --git a/src/data/resolvers/mutations/boards.ts b/src/data/resolvers/mutations/boards.ts index fad157cd5..6de726015 100644 --- a/src/data/resolvers/mutations/boards.ts +++ b/src/data/resolvers/mutations/boards.ts @@ -1,7 +1,7 @@ import { Boards, Pipelines, Stages } from '../../../db/models'; -import { IBoard, IOrderInput, IPipeline, IStageDocument } from '../../../db/models/definitions/boards'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IBoard, IOrderInput, IPipeline, IStage, IStageDocument } from '../../../db/models/definitions/boards'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { IContext } from '../../types'; import { checkPermission } from '../boardUtils'; interface IBoardsEdit extends IBoard { @@ -16,19 +16,25 @@ interface IPipelinesEdit extends IPipelinesAdd { _id: string; } +interface IStageEdit extends IStage { + _id: string; +} + const boardMutations = { /** * Create new board */ - async boardsAdd(_root, doc: IBoard, { user }: { user: IUserDocument }) { + async boardsAdd(_root, doc: IBoard, { user, docModifier }: IContext) { await checkPermission(doc.type, user, 'boardsAdd'); - const board = await Boards.createBoard({ userId: user._id, ...doc }); + + const extendedDoc = docModifier({ userId: user._id, ...doc }); + + const board = await Boards.createBoard(extendedDoc); await putCreateLog( { - type: 'board', - newData: JSON.stringify(doc), - description: `${doc.name} has been created`, + type: `${doc.type}Boards`, + newData: extendedDoc, object: board, }, user, @@ -40,23 +46,21 @@ const boardMutations = { /** * Edit board */ - async boardsEdit(_root, { _id, ...doc }: IBoardsEdit, { user }: { user: IUserDocument }) { + async boardsEdit(_root, { _id, ...doc }: IBoardsEdit, { user }: IContext) { await checkPermission(doc.type, user, 'boardsEdit'); - const board = await Boards.findOne({ _id }); + const board = await Boards.getBoard(_id); const updated = await Boards.updateBoard(_id, doc); - if (board) { - await putUpdateLog( - { - type: 'board', - newData: JSON.stringify(doc), - description: `${doc.name} has been edited`, - object: board, - }, - user, - ); - } + await putUpdateLog( + { + type: `${doc.type}Boards`, + newData: doc, + object: board, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -64,43 +68,59 @@ const boardMutations = { /** * Remove board */ - async boardsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const board = await Boards.findOne({ _id }); + async boardsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const board = await Boards.getBoard(_id); - if (board) { - await checkPermission(board.type, user, 'boardsRemove'); - } + await checkPermission(board.type, user, 'boardsRemove'); const removed = await Boards.removeBoard(_id); - if (board && removed) { - await putDeleteLog( - { - type: 'board', - object: board, - description: `${board.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: `${board.type}Boards`, object: board }, user); + + return removed; }, /** * Create new pipeline */ - async pipelinesAdd(_root, { stages, ...doc }: IPipelinesAdd, { user }: { user: IUserDocument }) { + async pipelinesAdd(_root, { stages, ...doc }: IPipelinesAdd, { user }: IContext) { await checkPermission(doc.type, user, 'pipelinesAdd'); - return Pipelines.createPipeline({ userId: user._id, ...doc }, stages); + const pipeline = await Pipelines.createPipeline({ userId: user._id, ...doc }, stages); + + await putCreateLog( + { + type: `${doc.type}Pipelines`, + newData: doc, + object: pipeline, + }, + user, + ); + + return pipeline; }, /** * Edit pipeline */ - async pipelinesEdit(_root, { _id, stages, ...doc }: IPipelinesEdit, { user }: { user: IUserDocument }) { + async pipelinesEdit(_root, { _id, stages, ...doc }: IPipelinesEdit, { user }: IContext) { await checkPermission(doc.type, user, 'pipelinesEdit'); - return Pipelines.updatePipeline(_id, doc, stages); + const pipeline = await Pipelines.getPipeline(_id); + + const updated = await Pipelines.updatePipeline(_id, doc, stages); + + await putUpdateLog( + { + type: `${doc.type}Pipelines`, + newData: doc, + object: pipeline, + updatedDocument: updated, + }, + user, + ); + + return updated; }, /** @@ -113,33 +133,25 @@ const boardMutations = { /** * Watch pipeline */ - async pipelinesWatch( - _root, - { _id, isAdd, type }: { _id: string; isAdd: boolean; type: string }, - { user }: { user: IUserDocument }, - ) { + async pipelinesWatch(_root, { _id, isAdd, type }: { _id: string; isAdd: boolean; type: string }, { user }: IContext) { await checkPermission(type, user, 'pipelinesWatch'); - const pipeline = await Pipelines.findOne({ _id }); - - if (!pipeline) { - throw new Error('Pipeline not found'); - } - return Pipelines.watchPipeline(_id, isAdd, user._id); }, /** * Remove pipeline */ - async pipelinesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const pipeline = await Pipelines.findOne({ _id }); + async pipelinesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const pipeline = await Pipelines.getPipeline(_id); + + await checkPermission(pipeline.type, user, 'pipelinesRemove'); + + const removed = await Pipelines.removePipeline(_id); - if (pipeline) { - await checkPermission(pipeline.type, user, 'pipelinesRemove'); - } + await putDeleteLog({ type: `${pipeline.type}Pipelines`, object: pipeline }, user); - return Pipelines.removePipeline(_id); + return removed; }, /** @@ -148,6 +160,25 @@ const boardMutations = { stagesUpdateOrder(_root, { orders }: { orders: IOrderInput[] }) { return Stages.updateOrder(orders); }, + + /** + * Edit stage + */ + async stagesEdit(_root, { _id, ...doc }: IStageEdit, { user }: IContext) { + await checkPermission(doc.type, user, 'stagesEdit'); + + return Stages.updateStage(_id, doc); + }, + + /** + * Remove stage + */ + async stagesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const stage = await Stages.getStage(_id); + await checkPermission(stage.type, user, 'stagesRemove'); + + return Stages.removeStage(_id); + }, }; export default boardMutations; diff --git a/src/data/resolvers/mutations/brands.ts b/src/data/resolvers/mutations/brands.ts index 0b3d651f2..5a355c779 100644 --- a/src/data/resolvers/mutations/brands.ts +++ b/src/data/resolvers/mutations/brands.ts @@ -1,8 +1,9 @@ import { Brands } from '../../../db/models'; import { IBrand, IBrandEmailConfig } from '../../../db/models/definitions/brands'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface IBrandsEdit extends IBrand { _id: string; @@ -12,15 +13,14 @@ const brandMutations = { /** * Create new brand */ - async brandsAdd(_root, doc: IBrand, { user }: { user: IUserDocument }) { + async brandsAdd(_root, doc: IBrand, { user }: IContext) { const brand = await Brands.createBrand({ userId: user._id, ...doc }); await putCreateLog( { - type: 'brand', - newData: JSON.stringify(doc), + type: MODULE_NAMES.BRAND, + newData: { ...doc, userId: user._id }, object: brand, - description: `${doc.name} has been created`, }, user, ); @@ -31,21 +31,18 @@ const brandMutations = { /** * Update brand */ - async brandsEdit(_root, { _id, ...fields }: IBrandsEdit, { user }: { user: IUserDocument }) { - const brand = await Brands.findOne({ _id }); + async brandsEdit(_root, { _id, ...fields }: IBrandsEdit, { user }: IContext) { + const brand = await Brands.getBrand(_id); const updated = await Brands.updateBrand(_id, fields); - if (brand) { - await putUpdateLog( - { - type: 'brand', - object: brand, - newData: JSON.stringify(fields), - description: `${fields.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.BRAND, + object: brand, + newData: fields, + }, + user, + ); return updated; }, @@ -53,20 +50,11 @@ const brandMutations = { /** * Delete brand */ - async brandsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const brand = await Brands.findOne({ _id }); + async brandsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const brand = await Brands.getBrand(_id); const removed = await Brands.removeBrand(_id); - if (brand && removed) { - await putDeleteLog( - { - type: 'brand', - object: brand, - description: `${brand.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.BRAND, object: brand }, user); return removed; }, @@ -77,21 +65,20 @@ const brandMutations = { async brandsConfigEmail( _root, { _id, emailConfig }: { _id: string; emailConfig: IBrandEmailConfig }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { - const brand = await Brands.findOne({ _id }); + const brand = await Brands.getBrand(_id); const updated = await Brands.updateEmailConfig(_id, emailConfig); - if (brand) { - await putUpdateLog( - { - type: 'brand', - object: brand, - description: `${brand.name} email config has been changed`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.BRAND, + object: brand, + newData: { emailConfig }, + description: `${brand.name} email config has been changed`, + }, + user, + ); return updated; }, diff --git a/src/data/resolvers/mutations/channels.ts b/src/data/resolvers/mutations/channels.ts index 13e72ecf3..beaffdedf 100644 --- a/src/data/resolvers/mutations/channels.ts +++ b/src/data/resolvers/mutations/channels.ts @@ -1,9 +1,13 @@ +import * as _ from 'underscore'; import { Channels } from '../../../db/models'; import { IChannel, IChannelDocument } from '../../../db/models/definitions/channels'; -import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { NOTIFICATION_CONTENT_TYPES, NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import utils, { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; +import utils, { checkUserIds, registerOnboardHistory } from '../../utils'; interface IChannelsEdit extends IChannel { _id: string; @@ -12,18 +16,30 @@ interface IChannelsEdit extends IChannel { /** * Send notification to all members of this channel except the sender */ -export const sendChannelNotifications = async (channel: IChannelDocument) => { - const content = `You have invited to '${channel.name}' channel.`; +export const sendChannelNotifications = async ( + channel: IChannelDocument, + type: 'invited' | 'removed', + user: IUserDocument, + receivers?: string[], +) => { + let action = `invited you to the`; + + if (type === 'removed') { + action = `removed you from`; + } return utils.sendNotification({ - createdUser: channel.userId || '', + contentType: NOTIFICATION_CONTENT_TYPES.CHANNEL, + contentTypeId: channel._id, + createdUser: user, notifType: NOTIFICATION_TYPES.CHANNEL_MEMBERS_CHANGE, - title: content, - content, - link: `/inbox?channelId=${channel._id}`, + title: `Channel updated`, + action, + content: `${channel.name} channel`, + link: `/inbox/index?channelId=${channel._id}`, // exclude current user - receivers: (channel.memberIds || []).filter(id => id !== channel.userId), + receivers: receivers || (channel.memberIds || []).filter(id => id !== channel.userId), }); }; @@ -31,17 +47,16 @@ const channelMutations = { /** * Create a new channel and send notifications to its members bar the creator */ - async channelsAdd(_root, doc: IChannel, { user }: { user: IUserDocument }) { + async channelsAdd(_root, doc: IChannel, { user }: IContext) { const channel = await Channels.createChannel(doc, user._id); - await sendChannelNotifications(channel); + await sendChannelNotifications(channel, 'invited', user); await putCreateLog( { - type: 'channel', - newData: JSON.stringify(doc), + type: MODULE_NAMES.CHANNEL, + newData: { ...doc, userId: user._id }, object: channel, - description: `${doc.name} has been created`, }, user, ); @@ -52,20 +67,28 @@ const channelMutations = { /** * Update channel data */ - async channelsEdit(_root, { _id, ...doc }: IChannelsEdit, { user }: { user: IUserDocument }) { - const channel = await Channels.findOne({ _id }); + async channelsEdit(_root, { _id, ...doc }: IChannelsEdit, { user }: IContext) { + const channel = await Channels.getChannel(_id); + + const { addedUserIds, removedUserIds } = checkUserIds(channel.memberIds || [], doc.memberIds || []); + + await sendChannelNotifications(channel, 'invited', user, addedUserIds); + await sendChannelNotifications(channel, 'removed', user, removedUserIds); + const updated = await Channels.updateChannel(_id, doc); - if (channel) { - await putUpdateLog( - { - type: 'channel', - object: channel, - newData: JSON.stringify(doc), - description: `${channel.name} has been updated`, - }, - user, - ); + await putUpdateLog( + { + type: MODULE_NAMES.CHANNEL, + object: channel, + newData: doc, + updatedDocument: updated, + }, + user, + ); + + if ((channel.integrationIds || []).toString() !== (updated.integrationIds || []).toString()) { + registerOnboardHistory({ type: 'connectIntegrationsToChannel', user }); } return updated; @@ -74,20 +97,16 @@ const channelMutations = { /** * Remove a channel */ - async channelsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const channel = await Channels.findOne({ _id }); - const removed = await Channels.removeChannel(_id); - - if (channel && removed) { - await putDeleteLog( - { - type: 'channel', - object: channel, - description: `${channel.name} has been removed`, - }, - user, - ); - } + async channelsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const channel = await Channels.getChannel(_id); + + await sendChannelNotifications(channel, 'removed', user); + + await Channels.removeChannel(_id); + + await putDeleteLog({ type: MODULE_NAMES.CHANNEL, object: channel }, user); + + return true; }, }; diff --git a/src/data/resolvers/mutations/checklists.ts b/src/data/resolvers/mutations/checklists.ts new file mode 100644 index 000000000..337cb2499 --- /dev/null +++ b/src/data/resolvers/mutations/checklists.ts @@ -0,0 +1,155 @@ +import { ChecklistItems, Checklists } from '../../../db/models'; +import { IChecklist, IChecklistItem } from '../../../db/models/definitions/checklists'; +import { graphqlPubsub } from '../../../pubsub'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IChecklistsEdit extends IChecklist { + _id: string; +} + +interface IChecklistItemsEdit extends IChecklistItem { + _id: string; +} + +const checklistsChanged = (checklist: IChecklistsEdit) => { + graphqlPubsub.publish('checklistsChanged', { + checklistsChanged: { + _id: checklist._id, + contentType: checklist.contentType, + contentTypeId: checklist.contentTypeId, + }, + }); +}; + +const checklistDetailChanged = (_id: string) => { + graphqlPubsub.publish('checklistDetailChanged', { + checklistDetailChanged: { + _id, + }, + }); +}; + +const checklistMutations = { + /** + * Adds checklist object and also adds an activity log + */ + async checklistsAdd(_root, args: IChecklist, { user }: IContext) { + const checklist = await Checklists.createChecklist(args, user); + + await putCreateLog( + { + type: MODULE_NAMES.CHECKLIST, + newData: args, + object: checklist, + }, + user, + ); + + checklistsChanged(checklist); + + return checklist; + }, + + /** + * Updates checklist object + */ + async checklistsEdit(_root, { _id, ...doc }: IChecklistsEdit, { user }: IContext) { + const checklist = await Checklists.getChecklist(_id); + const updated = await Checklists.updateChecklist(_id, doc); + + await putUpdateLog( + { + type: MODULE_NAMES.CHECKLIST, + object: checklist, + newData: doc, + updatedDocument: updated, + }, + user, + ); + + checklistDetailChanged(_id); + + return updated; + }, + + /** + * Removes a checklist + */ + async checklistsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const checklist = await Checklists.getChecklist(_id); + const removed = await Checklists.removeChecklist(_id); + + await putDeleteLog({ type: MODULE_NAMES.CHECKLIST, object: checklist }, user); + + checklistsChanged(checklist); + + return removed; + }, + + /** + * Adds a checklist item and also adds an activity log + */ + async checklistItemsAdd(_root, args: IChecklistItem, { user }: IContext) { + const checklistItem = await ChecklistItems.createChecklistItem(args, user); + + await putCreateLog( + { + type: MODULE_NAMES.CHECKLIST_ITEM, + newData: args, + object: checklistItem, + }, + user, + ); + + checklistDetailChanged(checklistItem.checklistId); + + return checklistItem; + }, + + /** + * Updates a checklist item + */ + async checklistItemsEdit(_root, { _id, ...doc }: IChecklistItemsEdit, { user }: IContext) { + const checklistItem = await ChecklistItems.getChecklistItem(_id); + const updated = await ChecklistItems.updateChecklistItem(_id, doc); + + await putUpdateLog( + { + type: MODULE_NAMES.CHECKLIST_ITEM, + object: checklistItem, + newData: doc, + updatedDocument: updated, + }, + user, + ); + + checklistDetailChanged(updated.checklistId); + + return updated; + }, + + /** + * Removes a checklist item + */ + async checklistItemsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const checklistItem = await ChecklistItems.getChecklistItem(_id); + const removed = await ChecklistItems.removeChecklistItem(_id); + + await putDeleteLog({ type: MODULE_NAMES.CHECKLIST_ITEM, object: checklistItem }, user); + + checklistDetailChanged(checklistItem.checklistId); + + return removed; + }, + + async checklistItemsOrder(_root, { _id, destinationIndex }: { _id: string; destinationIndex: number }) { + return ChecklistItems.updateItemOrder(_id, destinationIndex); + }, +}; + +moduleRequireLogin(checklistMutations); + +export default checklistMutations; diff --git a/src/data/resolvers/mutations/companies.ts b/src/data/resolvers/mutations/companies.ts index 2503c41d6..656f05dc2 100644 --- a/src/data/resolvers/mutations/companies.ts +++ b/src/data/resolvers/mutations/companies.ts @@ -1,8 +1,9 @@ import { Companies } from '../../../db/models'; import { ICompany } from '../../../db/models/definitions/companies'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface ICompaniesEdit extends ICompany { _id: string; @@ -10,17 +11,16 @@ interface ICompaniesEdit extends ICompany { const companyMutations = { /** - * Create new company also adds Company registration log + * Creates a new company */ - async companiesAdd(_root, doc: ICompany, { user }: { user: IUserDocument }) { - const company = await Companies.createCompany(doc, user); + async companiesAdd(_root, doc: ICompany, { user, docModifier }: IContext) { + const company = await Companies.createCompany(docModifier(doc), user); await putCreateLog( { - type: 'company', - newData: JSON.stringify(doc), + type: MODULE_NAMES.COMPANY, + newData: doc, object: company, - description: `${company.primaryName} has been created`, }, user, ); @@ -31,51 +31,33 @@ const companyMutations = { /** * Updates a company */ - async companiesEdit(_root, { _id, ...doc }: ICompaniesEdit, { user }: { user: IUserDocument }) { - const company = await Companies.findOne({ _id }); + async companiesEdit(_root, { _id, ...doc }: ICompaniesEdit, { user }: IContext) { + const company = await Companies.getCompany(_id); const updated = await Companies.updateCompany(_id, doc); - if (company) { - await putUpdateLog( - { - type: 'company', - object: company, - newData: JSON.stringify(doc), - description: `${company.primaryName} has been updated`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.COMPANY, + object: company, + newData: doc, + updatedDocument: updated, + }, + user, + ); return updated; }, /** - * Update company Customers + * Removes companies */ - async companiesEditCustomers(_root, { _id, customerIds }: { _id: string; customerIds: string[] }) { - return Companies.updateCustomers(_id, customerIds); - }, + async companiesRemove(_root, { companyIds }: { companyIds: string[] }, { user }: IContext) { + const companies = await Companies.find({ _id: { $in: companyIds } }).lean(); - /** - * Remove companies - */ - async companiesRemove(_root, { companyIds }: { companyIds: string[] }, { user }: { user: IUserDocument }) { - for (const companyId of companyIds) { - const company = await Companies.findOne({ _id: companyId }); - // Removing every company and modules associated with - const removed = await Companies.removeCompany(companyId); + await Companies.removeCompanies(companyIds); - if (company && removed) { - await putDeleteLog( - { - type: 'company', - object: company, - description: `${company.primaryName} has been removed`, - }, - user, - ); - } + for (const company of companies) { + await putDeleteLog({ type: MODULE_NAMES.COMPANY, object: company }, user); } return companyIds; @@ -91,7 +73,6 @@ const companyMutations = { checkPermission(companyMutations, 'companiesAdd', 'companiesAdd'); checkPermission(companyMutations, 'companiesEdit', 'companiesEdit'); -checkPermission(companyMutations, 'companiesEditCustomers', 'companiesEditCustomers'); checkPermission(companyMutations, 'companiesRemove', 'companiesRemove'); checkPermission(companyMutations, 'companiesMerge', 'companiesMerge'); diff --git a/src/data/resolvers/mutations/configs.ts b/src/data/resolvers/mutations/configs.ts index d8b31d3c0..b65b587d1 100644 --- a/src/data/resolvers/mutations/configs.ts +++ b/src/data/resolvers/mutations/configs.ts @@ -1,13 +1,42 @@ import { Configs } from '../../../db/models'; -import { IConfig } from '../../../db/models/definitions/configs'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { initFirebase, registerOnboardHistory, resetConfigsCache } from '../../utils'; const configMutations = { /** * Create or update config object */ - configsInsert(_root, doc: IConfig) { - return Configs.createOrUpdateConfig(doc); + async configsUpdate(_root, { configsMap }, { user }: IContext) { + const codes = Object.keys(configsMap); + + for (const code of codes) { + if (!code) { + continue; + } + + const prevConfig = (await Configs.findOne({ code })) || { value: [] }; + + const value = configsMap[code]; + const doc = { code, value }; + + await Configs.createOrUpdateConfig(doc); + + resetConfigsCache(); + + const updatedConfig = await Configs.getConfig(code); + + if (['GOOGLE_APPLICATION_CREDENTIALS_JSON'].includes(code)) { + initFirebase(configsMap[code] || ''); + } + + if ( + ['dealUOM', 'dealCurrency'].includes(code) && + prevConfig.value.toString() !== updatedConfig.value.toString() + ) { + registerOnboardHistory({ type: `configure.${code}`, user }); + } + } }, }; diff --git a/src/data/resolvers/mutations/conformities.ts b/src/data/resolvers/mutations/conformities.ts new file mode 100644 index 000000000..c329a8582 --- /dev/null +++ b/src/data/resolvers/mutations/conformities.ts @@ -0,0 +1,20 @@ +import { Conformities } from '../../../db/models'; +import { IConformityAdd, IConformityEdit } from '../../../db/models/definitions/conformities'; + +const conformityMutations = { + /** + * Create new conformity + */ + async conformityAdd(_root, doc: IConformityAdd) { + return Conformities.addConformity({ ...doc }); + }, + + /** + * Edit conformity + */ + async conformityEdit(_root, doc: IConformityEdit) { + return Conformities.editConformity({ ...doc }); + }, +}; + +export default conformityMutations; diff --git a/src/data/resolvers/mutations/conversations.ts b/src/data/resolvers/mutations/conversations.ts index 43118081b..5dee2af08 100644 --- a/src/data/resolvers/mutations/conversations.ts +++ b/src/data/resolvers/mutations/conversations.ts @@ -1,17 +1,26 @@ import * as strip from 'strip'; import * as _ from 'underscore'; -import { ConversationMessages, Conversations, Customers, Integrations } from '../../../db/models'; -import { CONVERSATION_STATUSES, KIND_CHOICES, NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { ConversationMessages, Conversations, Customers, Integrations, Tags } from '../../../db/models'; +import Messages from '../../../db/models/ConversationMessages'; +import { + KIND_CHOICES, + MESSAGE_TYPES, + NOTIFICATION_CONTENT_TYPES, + NOTIFICATION_TYPES, +} from '../../../db/models/definitions/constants'; import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; import { IConversationDocument } from '../../../db/models/definitions/conversations'; -import { IMessengerData } from '../../../db/models/definitions/integrations'; import { IUserDocument } from '../../../db/models/definitions/users'; import { debugExternalApi } from '../../../debuggers'; +import messageBroker from '../../../messageBroker'; import { graphqlPubsub } from '../../../pubsub'; +import { AUTO_BOT_MESSAGES } from '../../constants'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import utils, { fetchIntegrationApi } from '../../utils'; +import { IContext } from '../../types'; +import utils from '../../utils'; +import QueryBuilder, { IListArgs } from '../queries/conversationQueryBuilder'; -interface IConversationMessageAdd { +export interface IConversationMessageAdd { conversationId: string; content: string; mentionedUserIds?: string[]; @@ -19,6 +28,58 @@ interface IConversationMessageAdd { attachments?: any; } +interface IReplyFacebookComment { + conversationId: string; + commentId: string; + content: string; +} + +/** + * Send conversation to integrations + */ + +const sendConversationToIntegrations = ( + type: string, + integrationId: string, + conversationId: string, + requestName: string, + doc: IConversationMessageAdd, + dataSources: any, + action?: string, +) => { + if (type === 'facebook') { + const regex = new RegExp(']* src="([^"]*)"', 'g'); + + const images: string[] = (doc.content.match(regex) || []).map(m => m.replace(regex, '$1')); + + const attachments = doc.attachments as any[]; + + images.forEach(img => { + attachments.push({ type: 'image', url: img }); + }); + + return messageBroker().sendMessage('erxes-api:integrations-notification', { + action, + type, + payload: JSON.stringify({ + integrationId, + conversationId, + content: strip(doc.content), + attachments: doc.attachments || [], + }), + }); + } + + if (dataSources && dataSources.IntegrationsAPI && requestName) { + return dataSources.IntegrationsAPI[requestName]({ + conversationId, + integrationId, + content: strip(doc.content), + attachments: doc.attachments || [], + }); + } +}; + /** * conversation notrification receiver ids */ @@ -35,7 +96,7 @@ export const conversationNotifReceivers = ( } // participated users can get notifications - if (conversation.participatedUserIds) { + if (conversation.participatedUserIds && conversation.participatedUserIds.length > 0) { userIds = _.union(userIds, conversation.participatedUserIds); } @@ -64,11 +125,7 @@ export const publishConversationsChanged = (_ids: string[], type: string): strin /** * Publish admin's message */ -export const publishMessage = (message?: IMessageDocument | null, customerId?: string) => { - if (!message) { - return; - } - +export const publishMessage = async (message: IMessageDocument, customerId?: string) => { graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: message, }); @@ -76,61 +133,89 @@ export const publishMessage = (message?: IMessageDocument | null, customerId?: s // widget is listening for this subscription to show notification // customerId available means trying to notify to client if (customerId) { - const extendedMessage = message.toJSON(); - extendedMessage.customerId = customerId; + const unreadCount = await Messages.widgetsGetUnreadMessagesCount(message.conversationId); graphqlPubsub.publish('conversationAdminMessageInserted', { - conversationAdminMessageInserted: extendedMessage, + conversationAdminMessageInserted: { + customerId, + unreadCount, + }, }); } }; -export const publishClientMessage = (message: IMessageDocument) => { - // notifying to total unread count - graphqlPubsub.publish('conversationClientMessageInserted', { - conversationClientMessageInserted: message, - }); +const sendNotifications = async ({ + user, + conversations, + type, + mobile, + messageContent, +}: { + user: IUserDocument; + conversations: IConversationDocument[]; + type: string; + mobile?: boolean; + messageContent?: string; +}) => { + for (const conversation of conversations) { + const doc = { + createdUser: user, + link: `/inbox/index?_id=${conversation._id}`, + title: 'Conversation updated', + content: messageContent ? messageContent : conversation.content || 'Conversation updated', + notifType: type, + receivers: conversationNotifReceivers(conversation, user._id), + action: 'updated conversation', + contentType: NOTIFICATION_CONTENT_TYPES.CONVERSATION, + contentTypeId: conversation._id, + }; + + switch (type) { + case NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE: + doc.action = `sent you a message`; + doc.receivers = conversationNotifReceivers(conversation, user._id); + break; + case NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE: + doc.action = 'has assigned you to conversation '; + break; + case 'unassign': + doc.notifType = NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE; + doc.action = 'has removed you from conversation'; + break; + case NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE: + doc.action = `changed conversation status to ${(conversation.status || '').toUpperCase()}`; + break; + } + + await utils.sendNotification(doc); + + if (mobile) { + // send mobile notification ====== + await utils.sendMobileNotification({ + title: doc.title, + body: strip(doc.content), + receivers: conversationNotifReceivers(conversation, user._id, false), + customerId: conversation.customerId, + conversationId: conversation._id, + }); + } + } }; const conversationMutations = { /** * Create new message in conversation */ - async conversationMessageAdd(_root, doc: IConversationMessageAdd, { user }: { user: IUserDocument }) { - const conversation = await Conversations.findOne({ - _id: doc.conversationId, - }); - - if (!conversation) { - throw new Error('Conversation not found'); - } - - const integration = await Integrations.findOne({ - _id: conversation.integrationId, - }); - - if (!integration) { - throw new Error('Integration not found'); - } - - // send notification ======= - const title = 'You have a new message.'; - - utils.sendNotification({ - createdUser: user._id, - notifType: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, - title, - content: doc.content, - link: `/inbox?_id=${conversation._id}`, - receivers: conversationNotifReceivers(conversation, user._id), - }); - - // send mobile notification ====== - utils.sendMobileNotification({ - title, - body: strip(doc.content), - receivers: conversationNotifReceivers(conversation, user._id, false), - customerId: conversation.customerId, + async conversationMessageAdd(_root, doc: IConversationMessageAdd, { user, dataSources }: IContext) { + const conversation = await Conversations.getConversation(doc.conversationId); + const integration = await Integrations.getIntegration(conversation.integrationId); + + await sendNotifications({ + user, + conversations: [conversation], + type: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, + mobile: true, + messageContent: doc.content, }); // do not send internal message to third service integrations @@ -144,6 +229,8 @@ const conversationMutations = { } const kind = integration.kind; + const integrationId = integration.id; + const conversationId = conversation.id; const customer = await Customers.findOne({ _id: conversation.customerId }); @@ -151,7 +238,7 @@ const conversationMutations = { // customer's email const email = customer ? customer.primaryEmail : ''; - if (kind === KIND_CHOICES.FORM && email) { + if (kind === KIND_CHOICES.LEAD && email) { utils.sendEmail({ toEmails: [email], title: 'Reply', @@ -161,47 +248,130 @@ const conversationMutations = { }); } + let requestName; + let type; + let action; + + if (kind === KIND_CHOICES.FACEBOOK_POST) { + type = 'facebook'; + action = 'reply-post'; + + return sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); + } + const message = await ConversationMessages.addMessage(doc, user._id); - // send reply to facebook - if (kind === KIND_CHOICES.FACEBOOK) { - fetchIntegrationApi({ - path: '/facebook/reply', - method: 'POST', - body: { - conversationId: conversation._id, - integrationId: integration._id, + /** + * Send SMS only when: + * - integration is of kind telnyx + * - customer has primary phone filled + * - customer's primary phone is valid + * - content length within 160 characters + */ + if ( + kind === KIND_CHOICES.TELNYX && + customer && + customer.primaryPhone && + customer.phoneValidationStatus === 'valid' && + doc.content.length <= 160 + ) { + await messageBroker().sendMessage('erxes-api:integrations-notification', { + action: 'sendConversationSms', + payload: JSON.stringify({ + conversationMessageId: message._id, + conversationId, + integrationId, + toPhone: customer.primaryPhone, content: strip(doc.content), - attachments: doc.attachments || [], - }, - }) - .then(response => { - debugExternalApi(response); - }) - .catch(e => { - debugExternalApi(e.message); - }); + }), + }); } - const dbMessage = await ConversationMessages.findOne({ - _id: message._id, - }); + // send reply to facebook + if (kind === KIND_CHOICES.FACEBOOK_MESSENGER) { + type = 'facebook'; + action = 'reply-messenger'; + } + + // send reply to chatfuel + if (kind === KIND_CHOICES.CHATFUEL) { + requestName = 'replyChatfuel'; + } + + if (kind === KIND_CHOICES.TWITTER_DM) { + requestName = 'replyTwitterDm'; + } + if (kind.includes('smooch')) { + requestName = 'replySmooch'; + } + + // send reply to whatsapp + if (kind === KIND_CHOICES.WHATSAPP) { + requestName = 'replyWhatsApp'; + } + + await sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); + + const dbMessage = await ConversationMessages.getMessage(message._id); + + await utils.sendToWebhook('create', 'userMessages', dbMessage); // Publishing both admin & client publishMessage(dbMessage, conversation.customerId); return dbMessage; }, + async conversationsReplyFacebookComment(_root, doc: IReplyFacebookComment, { user, dataSources }: IContext) { + const conversation = await Conversations.getConversation(doc.conversationId); + const integration = await Integrations.getIntegration(conversation.integrationId); + + await sendNotifications({ + user, + conversations: [conversation], + type: NOTIFICATION_TYPES.CONVERSATION_ADD_MESSAGE, + mobile: true, + messageContent: doc.content, + }); + + const requestName = 'replyFacebookPost'; + const integrationId = integration.id; + const conversationId = doc.commentId; + const type = 'facebook'; + const action = 'reply-post'; + + try { + await sendConversationToIntegrations(type, integrationId, conversationId, requestName, doc, dataSources, action); + } catch (e) { + debugExternalApi(e.message); + throw new Error(e.message); + } + }, + + async conversationsChangeStatusFacebookComment(_root, doc: IReplyFacebookComment, { dataSources }: IContext) { + const requestName = 'replyFacebookPost'; + const type = 'facebook'; + const action = 'change-status-comment'; + const conversationId = doc.commentId; + doc.content = ''; + + try { + await sendConversationToIntegrations(type, '', conversationId, requestName, doc, dataSources, action); + } catch (e) { + debugExternalApi(e.message); + throw new Error(e.message); + } + }, + /** * Assign employee to conversation */ async conversationsAssign( _root, { conversationIds, assignedUserId }: { conversationIds: string[]; assignedUserId: string }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { - const updatedConversations: IConversationDocument[] = await Conversations.assignUserConversation( + const conversations: IConversationDocument[] = await Conversations.assignUserConversation( conversationIds, assignedUserId, ); @@ -209,113 +379,159 @@ const conversationMutations = { // notify graphl subscription publishConversationsChanged(conversationIds, 'assigneeChanged'); - for (const conversation of updatedConversations) { - const content = 'Assigned user has changed'; - - // send notification - utils.sendNotification({ - createdUser: user._id, - notifType: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE, - title: content, - content, - link: `/inbox?_id=${conversation._id}`, - receivers: conversationNotifReceivers(conversation, user._id), - }); - } + await sendNotifications({ user, conversations, type: NOTIFICATION_TYPES.CONVERSATION_ASSIGNEE_CHANGE }); - return updatedConversations; + return conversations; }, /** * Unassign employee from conversation */ - async conversationsUnassign(_root, { _ids }: { _ids: string[] }) { - const conversations = await Conversations.unassignUserConversation(_ids); + async conversationsUnassign(_root, { _ids }: { _ids: string[] }, { user }: IContext) { + const oldConversations = await Conversations.find({ _id: { $in: _ids } }); + const updatedConversations = await Conversations.unassignUserConversation(_ids); + + await sendNotifications({ + user, + conversations: oldConversations, + type: 'unassign', + }); // notify graphl subscription publishConversationsChanged(_ids, 'assigneeChanged'); - return conversations; + return updatedConversations; }, /** * Change conversation status */ - async conversationsChangeStatus( - _root, - { _ids, status }: { _ids: string[]; status: string }, - { user }: { user: IUserDocument }, - ) { - const { conversations } = await Conversations.checkExistanceConversations(_ids); - + async conversationsChangeStatus(_root, { _ids, status }: { _ids: string[]; status: string }, { user }: IContext) { await Conversations.changeStatusConversation(_ids, status, user._id); // notify graphl subscription publishConversationsChanged(_ids, status); - for (const conversation of conversations) { - if (status === CONVERSATION_STATUSES.CLOSED) { - const customer = await Customers.findOne({ - _id: conversation.customerId, - }); - - if (!customer) { - throw new Error('Customer not found'); - } - - const integration = await Integrations.findOne({ - _id: conversation.integrationId, - }); - - if (!integration) { - throw new Error('Integration not found'); - } - - const messengerData: IMessengerData = integration.messengerData || {}; - const notifyCustomer = messengerData.notifyCustomer || false; - - if (notifyCustomer && customer.primaryEmail) { - // send email to customer - utils.sendEmail({ - toEmails: [customer.primaryEmail], - title: 'Conversation detail', - template: { - name: 'conversationDetail', - data: { - conversationDetail: { - title: 'Conversation detail', - messages: await ConversationMessages.find({ - conversationId: conversation._id, - }), - date: new Date(), - }, - }, - }, - }); - } - } - - const content = 'Conversation status has changed.'; - - utils.sendNotification({ - createdUser: user._id, - notifType: NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE, - title: content, - content, - link: `/inbox?_id=${conversation._id}`, - receivers: conversationNotifReceivers(conversation, user._id), - }); - } + const updatedConversations = await Conversations.find({ _id: { $in: _ids } }); - return Conversations.find({ _id: { $in: _ids } }); + await sendNotifications({ + user, + conversations: updatedConversations, + type: NOTIFICATION_TYPES.CONVERSATION_STATE_CHANGE, + }); + + return updatedConversations; + }, + + /** + * Resolve all conversations + */ + async conversationResolveAll(_root, params: IListArgs, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder(params, { _id: user._id }); + + await qb.buildAllQueries(); + const query = qb.mainQuery(); + + const updated = await Conversations.resolveAllConversation(query, user._id); + + return updated.nModified || 0; }, /** * Conversation mark as read */ - async conversationMarkAsRead(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async conversationMarkAsRead(_root, { _id }: { _id: string }, { user }: IContext) { return Conversations.markAsReadConversation(_id, user._id); }, + + async conversationDeleteVideoChatRoom(_root, { name }, { dataSources }: IContext) { + try { + return await dataSources.IntegrationsAPI.deleteDailyVideoChatRoom(name); + } catch (e) { + debugExternalApi(e.message); + + throw new Error(e.message); + } + }, + + async conversationCreateVideoChatRoom(_root, { _id }, { dataSources, user }: IContext) { + let message; + + try { + const doc = { + conversationId: _id, + internal: false, + contentType: MESSAGE_TYPES.VIDEO_CALL, + }; + + message = await ConversationMessages.addMessage(doc, user._id); + + const videoCallData = await dataSources.IntegrationsAPI.createDailyVideoChatRoom({ + erxesApiConversationId: _id, + erxesApiMessageId: message._id, + }); + + const updatedMessage = { ...message._doc, videoCallData }; + + // publish new message to conversation detail + publishMessage(updatedMessage); + + return videoCallData; + } catch (e) { + debugExternalApi(e.message); + + await ConversationMessages.deleteOne({ _id: message._id }); + + throw new Error(e.message); + } + }, + + async conversationCreateProductBoardNote(_root, { _id }, { dataSources, user }: IContext) { + const conversation = await Conversations.findOne({ _id }) + .select('customerId userId tagIds, integrationId') + .lean(); + const tags = await Tags.find({ _id: { $in: conversation.tagIds } }).select('name'); + const customer = await Customers.findOne({ _id: conversation.customerId }); + const messages = await ConversationMessages.find({ conversationId: _id }).sort({ + createdAt: 1, + }); + const integrationId = conversation.integrationId; + + try { + const productBoardLink = await dataSources.IntegrationsAPI.createProductBoardNote({ + erxesApiConversationId: _id, + tags, + customer, + messages, + user, + integrationId, + }); + + return productBoardLink; + } catch (e) { + debugExternalApi(e.message); + + throw new Error(e.message); + } + }, + + async changeConversationOperator(_root, { _id, operatorStatus }: { _id: string; operatorStatus: string }) { + const message = await Messages.createMessage({ + conversationId: _id, + botData: [ + { + type: 'text', + text: AUTO_BOT_MESSAGES.CHANGE_OPERATOR, + }, + ], + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: message }); + graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: message }); + + return Conversations.updateOne({ _id }, { $set: { operatorStatus } }); + }, }; requireLogin(conversationMutations, 'conversationMarkAsRead'); @@ -324,5 +540,6 @@ checkPermission(conversationMutations, 'conversationMessageAdd', 'conversationMe checkPermission(conversationMutations, 'conversationsAssign', 'assignConversation'); checkPermission(conversationMutations, 'conversationsUnassign', 'assignConversation'); checkPermission(conversationMutations, 'conversationsChangeStatus', 'changeConversationStatus'); +checkPermission(conversationMutations, 'conversationResolveAll', 'conversationResolveAll'); export default conversationMutations; diff --git a/src/data/resolvers/mutations/customers.ts b/src/data/resolvers/mutations/customers.ts index ca2b1bd8d..475686ba5 100644 --- a/src/data/resolvers/mutations/customers.ts +++ b/src/data/resolvers/mutations/customers.ts @@ -1,9 +1,12 @@ -import { Customers } from '../../../db/models'; - +import { ActivityLogs, Customers } from '../../../db/models'; import { ICustomer } from '../../../db/models/definitions/customers'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import messageBroker from '../../../messageBroker'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; +import { registerOnboardHistory } from '../../utils'; +import { validateBulk } from '../../verifierUtils'; interface ICustomersEdit extends ICustomer { _id: string; @@ -13,87 +16,105 @@ const customerMutations = { /** * Create new customer also adds Customer registration log */ - async customersAdd(_root, doc: ICustomer, { user }: { user: IUserDocument }) { - const customer = await Customers.createCustomer(doc, user); + async customersAdd(_root, doc: ICustomer, { user, docModifier }: IContext) { + const modifiedDoc = docModifier(doc); + + const customer = await Customers.createCustomer(modifiedDoc, user); await putCreateLog( { - type: 'customer', - newData: JSON.stringify(doc), + type: MODULE_NAMES.CUSTOMER, + newData: modifiedDoc, object: customer, - description: `${customer.firstName} has been created`, }, user, ); + await registerOnboardHistory({ type: `${customer.state}Create`, user }); + return customer; }, /** - * Update customer + * Updates a customer */ - async customersEdit(_root, { _id, ...doc }: ICustomersEdit, { user }: { user: IUserDocument }) { - const customer = await Customers.findOne({ _id }); + async customersEdit(_root, { _id, ...doc }: ICustomersEdit, { user }: IContext) { + const customer = await Customers.getCustomer(_id); const updated = await Customers.updateCustomer(_id, doc); - if (customer) { - await putUpdateLog( - { - type: 'customer', - object: customer, - newData: JSON.stringify(doc), - description: `${customer.firstName} has been updated`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.CUSTOMER, + object: customer, + newData: doc, + updatedDocument: updated, + }, + user, + ); return updated; }, /** - * Update customer Companies + * Change state */ - async customersEditCompanies(_root, { _id, companyIds }: { _id: string; companyIds: string[] }) { - return Customers.updateCompanies(_id, companyIds); + async customersChangeState(_root, args: { _id: string; value: string }) { + return Customers.changeState(args._id, args.value); }, /** * Merge customers */ - async customersMerge(_root, { customerIds, customerFields }: { customerIds: string[]; customerFields: ICustomer }) { - return Customers.mergeCustomers(customerIds, customerFields); + async customersMerge( + _root, + { customerIds, customerFields }: { customerIds: string[]; customerFields: ICustomer }, + { user }: IContext, + ) { + return Customers.mergeCustomers(customerIds, customerFields, user); }, /** * Remove customers */ - async customersRemove(_root, { customerIds }: { customerIds: string[] }, { user }: { user: IUserDocument }) { - for (const customerId of customerIds) { - // Removing every customer and modules associated with - const customer = await Customers.findOne({ _id: customerId }); - const removed = await Customers.removeCustomer(customerId); - - if (customer && removed) { - await putDeleteLog( - { - type: 'customer', - object: customer, - description: `${customer.firstName} has been deleted`, - }, - user, - ); + async customersRemove(_root, { customerIds }: { customerIds: string[] }, { user }: IContext) { + const customers = await Customers.find({ _id: { $in: customerIds } }).lean(); + + await Customers.removeCustomers(customerIds); + + await messageBroker().sendMessage('erxes-api:integrations-notification', { + type: 'removeCustomers', + customerIds, + }); + + for (const customer of customers) { + await ActivityLogs.removeActivityLog(customer._id); + + await putDeleteLog({ type: MODULE_NAMES.CUSTOMER, object: customer }, user); + + if (customer.mergedIds) { + await messageBroker().sendMessage('erxes-api:integrations-notification', { + type: 'removeCustomers', + customerIds: customer.mergedIds, + }); } } return customerIds; }, + + async customersVerify(_root, { verificationType }: { verificationType: string }) { + await validateBulk(verificationType); + }, + + async customersChangeVerificationStatus(_root, args: { customerIds: [string]; type: string; status: string }) { + return Customers.updateVerificationStatus(args.customerIds, args.type, args.status); + }, }; checkPermission(customerMutations, 'customersAdd', 'customersAdd'); checkPermission(customerMutations, 'customersEdit', 'customersEdit'); -checkPermission(customerMutations, 'customersEditCompanies', 'customersEditCompanies'); checkPermission(customerMutations, 'customersMerge', 'customersMerge'); checkPermission(customerMutations, 'customersRemove', 'customersRemove'); +checkPermission(customerMutations, 'customersChangeState', 'customersChangeState'); export default customerMutations; diff --git a/src/data/resolvers/mutations/deals.ts b/src/data/resolvers/mutations/deals.ts index a3cee043f..cbd41f403 100644 --- a/src/data/resolvers/mutations/deals.ts +++ b/src/data/resolvers/mutations/deals.ts @@ -1,11 +1,11 @@ +import * as _ from 'underscore'; import { Deals } from '../../../db/models'; -import { IOrderInput } from '../../../db/models/definitions/boards'; -import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { IItemDragCommonFields } from '../../../db/models/definitions/boards'; import { IDeal } from '../../../db/models/definitions/deals'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; -import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; +import { IContext } from '../../types'; +import { checkUserIds } from '../../utils'; +import { itemsAdd, itemsArchive, itemsChange, itemsCopy, itemsEdit, itemsRemove } from './boardUtils'; interface IDealsEdit extends IDeal { _id: string; @@ -13,151 +13,86 @@ interface IDealsEdit extends IDeal { const dealMutations = { /** - * Create new deal + * Creates a new deal */ - async dealsAdd(_root, doc: IDeal, { user }: { user: IUserDocument }) { - doc.initialStageId = doc.stageId; - const deal = await Deals.createDeal({ - ...doc, - modifiedBy: user._id, - }); - - await sendNotifications( - deal.stageId || '', - user, - NOTIFICATION_TYPES.DEAL_ADD, - deal.assignedUserIds || [], - `'{userName}' invited you to the '${deal.name}'.`, - 'deal', - ); - - await putCreateLog( - { - type: 'deal', - newData: JSON.stringify(doc), - object: deal, - description: `${deal.name} has been created`, - }, - user, - ); - - return deal; + async dealsAdd(_root, doc: IDeal & { proccessId: string; aboveItemId: string }, { user, docModifier }: IContext) { + return itemsAdd(doc, 'deal', user, docModifier, Deals.createDeal); }, /** - * Edit deal + * Edits a deal */ - async dealsEdit(_root, { _id, ...doc }: IDealsEdit, { user }: { user: IUserDocument }) { - const deal = await Deals.findOne({ _id }); - const updated = await Deals.updateDeal(_id, { - ...doc, - modifiedAt: new Date(), - modifiedBy: user._id, - }); - - await manageNotifications(Deals, updated, user, 'deal'); - - if (deal) { - await putUpdateLog( - { - type: 'deal', - object: deal, - newData: JSON.stringify(doc), - description: `${deal.name} has been edited`, - }, - user, - ); + async dealsEdit(_root, { _id, proccessId, ...doc }: IDealsEdit & { proccessId: string }, { user }: IContext) { + const oldDeal = await Deals.getDeal(_id); + + if (doc.assignedUserIds) { + const { removedUserIds } = checkUserIds(oldDeal.assignedUserIds, doc.assignedUserIds); + const oldAssignedUserPdata = (oldDeal.productsData || []) + .filter(pdata => pdata.assignUserId) + .map(pdata => pdata.assignUserId || ''); + const cantRemoveUserIds = removedUserIds.filter(userId => oldAssignedUserPdata.includes(userId)); + + if (cantRemoveUserIds.length > 0) { + throw new Error('Cannot remove the team member, it is assigned in the product / service section'); + } } - return updated; - }, + if (doc.productsData) { + const assignedUsersPdata = doc.productsData + .filter(pdata => pdata.assignUserId) + .map(pdata => pdata.assignUserId || ''); - /** - * Change deal - */ - async dealsChange( - _root, - { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, - ) { - const deal = await Deals.updateDeal(_id, { - modifiedAt: new Date(), - modifiedBy: user._id, - stageId: destinationStageId, - }); - - const content = await itemsChange(Deals, deal, 'deal', destinationStageId); - - await sendNotifications( - deal.stageId || '', - user, - NOTIFICATION_TYPES.DEAL_CHANGE, - await notifiedUserIds(deal), - content, - 'deal', - ); - - return deal; + const oldAssignedUserPdata = (oldDeal.productsData || []) + .filter(pdata => pdata.assignUserId) + .map(pdata => pdata.assignUserId || ''); + + const { addedUserIds, removedUserIds } = checkUserIds(oldAssignedUserPdata, assignedUsersPdata); + + if (addedUserIds.length > 0 || removedUserIds.length > 0) { + let assignedUserIds = doc.assignedUserIds || oldDeal.assignedUserIds || []; + assignedUserIds = [...new Set(assignedUserIds.concat(addedUserIds))]; + assignedUserIds = assignedUserIds.filter(userId => !removedUserIds.includes(userId)); + doc.assignedUserIds = assignedUserIds; + } + } + + return itemsEdit(_id, 'deal', oldDeal, doc, proccessId, user, Deals.updateDeal); }, /** - * Update deal orders (not sendNotifaction, ordered card to change) + * Change deal */ - dealsUpdateOrder(_root, { stageId, orders }: { stageId: string; orders: IOrderInput[] }) { - return Deals.updateOrder(stageId, orders); + async dealsChange(_root, doc: IItemDragCommonFields, { user }: IContext) { + return itemsChange(doc, 'deal', user, Deals.updateDeal); }, /** * Remove deal */ - async dealsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const deal = await Deals.findOne({ _id }); - - if (!deal) { - throw new Error('Deal not found'); - } - - await sendNotifications( - deal.stageId || '', - user, - NOTIFICATION_TYPES.DEAL_DELETE, - await notifiedUserIds(deal), - `'{userName}' deleted deal: '${deal.name}'`, - 'deal', - ); - - const removed = await Deals.removeDeal(_id); - - await putDeleteLog( - { - type: 'deal', - object: deal, - description: `${deal.name} has been removed`, - }, - user, - ); - - return removed; + async dealsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + return itemsRemove(_id, 'deal', user); }, /** * Watch deal */ - async dealsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { - const deal = await Deals.findOne({ _id }); + async dealsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { + return Deals.watchDeal(_id, isAdd, user._id); + }, - if (!deal) { - throw new Error('Deal not found'); - } + async dealsCopy(_root, { _id, proccessId }: { _id: string; proccessId: string }, { user }: IContext) { + return itemsCopy(_id, proccessId, 'deal', user, ['productsData', 'paymentsData'], Deals.createDeal); + }, - return Deals.watchDeal(_id, isAdd, user._id); + async dealsArchive(_root, { stageId, proccessId }: { stageId: string; proccessId: string }, { user }: IContext) { + return itemsArchive(stageId, 'deal', proccessId, user); }, }; checkPermission(dealMutations, 'dealsAdd', 'dealsAdd'); checkPermission(dealMutations, 'dealsEdit', 'dealsEdit'); -checkPermission(dealMutations, 'dealsUpdateOrder', 'dealsUpdateOrder'); checkPermission(dealMutations, 'dealsRemove', 'dealsRemove'); checkPermission(dealMutations, 'dealsWatch', 'dealsWatch'); +checkPermission(dealMutations, 'dealsArchive', 'dealsArchive'); export default dealMutations; diff --git a/src/data/resolvers/mutations/emailTemplates.ts b/src/data/resolvers/mutations/emailTemplates.ts index 5b22a6a22..8b7fe0786 100644 --- a/src/data/resolvers/mutations/emailTemplates.ts +++ b/src/data/resolvers/mutations/emailTemplates.ts @@ -1,8 +1,9 @@ import { EmailTemplates } from '../../../db/models'; import { IEmailTemplate } from '../../../db/models/definitions/emailTemplates'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface IEmailTemplatesEdit extends IEmailTemplate { _id: string; @@ -10,22 +11,19 @@ interface IEmailTemplatesEdit extends IEmailTemplate { const emailTemplateMutations = { /** - * Create new email template + * Creates a new email template */ - async emailTemplatesAdd(_root, doc: IEmailTemplate, { user }: { user: IUserDocument }) { - const template = await EmailTemplates.create(doc); + async emailTemplatesAdd(_root, doc: IEmailTemplate, { user, docModifier }: IContext) { + const template = await EmailTemplates.create(docModifier(doc)); - if (template) { - await putCreateLog( - { - type: 'emailTemplate', - newData: JSON.stringify(doc), - object: template, - description: `${template.name} has been created`, - }, - user, - ); - } + await putCreateLog( + { + type: MODULE_NAMES.EMAIL_TEMPLATE, + newData: doc, + object: template, + }, + user, + ); return template; }, @@ -33,21 +31,18 @@ const emailTemplateMutations = { /** * Update email template */ - async emailTemplatesEdit(_root, { _id, ...fields }: IEmailTemplatesEdit, { user }: { user: IUserDocument }) { - const template = await EmailTemplates.findOne({ _id }); + async emailTemplatesEdit(_root, { _id, ...fields }: IEmailTemplatesEdit, { user }: IContext) { + const template = await EmailTemplates.getEmailTemplate(_id); const updated = await EmailTemplates.updateEmailTemplate(_id, fields); - if (template) { - await putUpdateLog( - { - type: 'emailTemplate', - object: template, - newData: JSON.stringify(fields), - description: `${template.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.EMAIL_TEMPLATE, + object: template, + newData: fields, + }, + user, + ); return updated; }, @@ -55,20 +50,11 @@ const emailTemplateMutations = { /** * Delete email template */ - async emailTemplatesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const template = await EmailTemplates.findOne({ _id }); + async emailTemplatesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const template = await EmailTemplates.getEmailTemplate(_id); const removed = await EmailTemplates.removeEmailTemplate(_id); - if (template) { - await putDeleteLog( - { - type: 'emailTemplate', - object: template, - description: `${template.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.EMAIL_TEMPLATE, object: template }, user); return removed; }, diff --git a/src/data/resolvers/mutations/engageUtils.ts b/src/data/resolvers/mutations/engageUtils.ts index 246be5723..c60de7ecc 100644 --- a/src/data/resolvers/mutations/engageUtils.ts +++ b/src/data/resolvers/mutations/engageUtils.ts @@ -1,4 +1,5 @@ import * as Random from 'meteor-random'; +import { Transform, Writable } from 'stream'; import { ConversationMessages, Conversations, @@ -8,145 +9,79 @@ import { Segments, Users, } from '../../../db/models'; -import { METHODS } from '../../../db/models/definitions/constants'; +import { CONVERSATION_STATUSES, KIND_CHOICES, METHODS } from '../../../db/models/definitions/constants'; import { ICustomerDocument } from '../../../db/models/definitions/customers'; import { IEngageMessageDocument } from '../../../db/models/definitions/engages'; import { IUserDocument } from '../../../db/models/definitions/users'; -import { INTEGRATION_KIND_CHOICES, MESSAGE_KINDS } from '../../constants'; -import QueryBuilder from '../../modules/segments/queryBuilder'; -import { createTransporter, getEnv } from '../../utils'; - -/** - * Dynamic content tags - */ -export const replaceKeys = ({ - content, - customer, - user, -}: { - content: string; - customer: ICustomerDocument; +import { debugBase } from '../../../debuggers'; +import messageBroker from '../../../messageBroker'; +import { MESSAGE_KINDS } from '../../constants'; +import { fetchBySegments } from '../../modules/segments/queryBuilder'; +import { chunkArray, replaceEditorAttributes } from '../../utils'; + +interface IEngageParams { + engageMessage: IEngageMessageDocument; + customersSelector: any; user: IUserDocument; -}): string => { - let result = content; - - let customerName = customer.firstName || customer.lastName || 'Customer'; - - if (customer.firstName && customer.lastName) { - customerName = `${customer.firstName} ${customer.lastName}`; - } +} - const details = user.details ? user.details.toJSON() : {}; - - // replace customer fields - result = result.replace(/{{\s?customer.name\s?}}/gi, customerName); - result = result.replace(/{{\s?customer.email\s?}}/gi, customer.primaryEmail || ''); - - // replace user fields - result = result.replace(/{{\s?user.fullName\s?}}/gi, details.fullName || ''); - result = result.replace(/{{\s?user.position\s?}}/gi, details.position || ''); - result = result.replace(/{{\s?user.email\s?}}/gi, user.email || ''); - - return result; -}; - -/** - * Find customers - */ -const findCustomers = async ({ +export const generateCustomerSelector = async ({ customerIds, segmentIds = [], tagIds = [], brandIds = [], }: { - customerIds: string[]; + customerIds?: string[]; segmentIds?: string[]; tagIds?: string[]; brandIds?: string[]; -}): Promise => { +}): Promise => { // find matched customers - let customerQuery: any = { _id: { $in: customerIds || [] } }; - const doNotDisturbQuery = [{ doNotDisturb: 'No' }, { doNotDisturb: { $exists: false } }]; + let customerQuery: any = {}; + + if (customerIds && customerIds.length > 0) { + customerQuery = { _id: { $in: customerIds } }; + } if (tagIds.length > 0) { - customerQuery = { $or: doNotDisturbQuery, tagIds: { $in: tagIds || [] } }; + customerQuery = { tagIds: { $in: tagIds } }; } if (brandIds.length > 0) { - const integrationIds = await Integrations.find({ brandId: { $in: brandIds } }).distinct('_id'); + let integrationIds: string[] = []; - customerQuery = { $or: doNotDisturbQuery, integrationId: { $in: integrationIds } }; + for (const brandId of brandIds) { + const integrations = await Integrations.findIntegrations({ brandId }); + + integrationIds = [...integrationIds, ...integrations.map(i => i._id)]; + } + + customerQuery = { integrationId: { $in: integrationIds } }; } if (segmentIds.length > 0) { - const segmentQueries: any = []; - const segments = await Segments.find({ _id: { $in: segmentIds } }); - for (const segment of segments) { - const filter = await QueryBuilder.segments(segment); + let customerIdsBySegments: string[] = []; - filter.$or = doNotDisturbQuery; + for (const segment of segments) { + const cIds = await fetchBySegments(segment); - segmentQueries.push(filter); + customerIdsBySegments = [...customerIdsBySegments, ...cIds]; } - customerQuery = { $or: segmentQueries }; + customerQuery = { _id: { $in: customerIdsBySegments } }; } - return Customers.find(customerQuery); + return { $or: [{ doNotDisturb: 'No' }, { doNotDisturb: { $exists: false } }], ...customerQuery }; }; -const executeSendViaEmail = async ( - userEmail: string, - attachments: any, - customer: ICustomerDocument, - replacedSubject: string, - replacedContent: string, - AWS_SES_CONFIG_SET: string, - messageId: string, - mailMessageId: string, -) => { - const transporter = await createTransporter({ ses: true }); - let mailAttachment = []; - - if (attachments.length > 0) { - mailAttachment = attachments.map(file => { - return { - filename: file.name || '', - path: file.url || '', - }; - }); - } - - transporter.sendMail({ - from: userEmail, - to: customer.primaryEmail, - subject: replacedSubject, - attachments: mailAttachment, - html: replacedContent, - headers: { - 'X-SES-CONFIGURATION-SET': AWS_SES_CONFIG_SET, - EngageMessageId: messageId, - CustomerId: customer._id, - MailMessageId: mailMessageId, - }, - }); +const sendQueueMessage = (args: any) => { + return messageBroker().sendMessage('erxes-api:engages-notification', args); }; -/** - * Send via email - */ -const sendViaEmail = async (message: IEngageMessageDocument) => { - const { fromUserId, tagIds, brandIds, segmentIds, customerIds = [] } = message; - - if (!message.email) { - return; - } - const { subject, content, attachments = [] } = message.email.toJSON(); - - const AWS_SES_CONFIG_SET = getEnv({ name: 'AWS_SES_CONFIG_SET' }); - const AWS_ENDPOINT = getEnv({ name: 'AWS_ENDPOINT' }); +export const send = async (engageMessage: IEngageMessageDocument) => { + const { customerIds, segmentIds, tagIds, brandIds, fromUserId } = engageMessage; const user = await Users.findOne({ _id: fromUserId }); @@ -154,131 +89,284 @@ const sendViaEmail = async (message: IEngageMessageDocument) => { throw new Error('User not found'); } - const userEmail = user.email; - if (!userEmail) { - throw new Error(`email not found with ${userEmail}`); + if (!engageMessage.isLive) { + return; } - // find matched customers - const customers = await findCustomers({ customerIds, segmentIds, tagIds, brandIds }); + const customersSelector = await generateCustomerSelector({ customerIds, segmentIds, tagIds, brandIds }); + + if (engageMessage.method === METHODS.MESSENGER && engageMessage.kind !== MESSAGE_KINDS.VISITOR_AUTO) { + return sendViaMessenger({ engageMessage, customersSelector, user }); + } + + if (engageMessage.method === METHODS.EMAIL) { + return sendEmailOrSms({ engageMessage, customersSelector, user }, 'sendEngage'); + } - // save matched customer ids - EngageMessages.setCustomerIds(message._id, customers); + if (engageMessage.method === METHODS.SMS) { + return sendEmailOrSms({ engageMessage, customersSelector, user }, 'sendEngageSms'); + } +}; - for (const customer of customers) { - let replacedContent = replaceKeys({ content, customer, user }); +// Prepares queue data to engages-email-sender +const sendEmailOrSms = async ( + { engageMessage, customersSelector, user }: IEngageParams, + action: 'sendEngage' | 'sendEngageSms', +) => { + const engageMessageId = engageMessage._id; - // Add unsubscribe link ======== - const unSubscribeUrl = `${AWS_ENDPOINT}/unsubscribe/?cid=${customer._id}`; + await sendQueueMessage({ + action: 'writeLog', + data: { + engageMessageId, + msg: `Run at ${new Date()}`, + }, + }); - replacedContent += `
If you want to use service like this click here to read more. Also you can opt out from our email subscription here.
© 2019 erxes inc Growth Marketing Platform
`; + const customerInfos: Array<{ + _id: string; + primaryEmail?: string; + emailValidationStatus?: string; + phoneValidationStatus?: string; + primaryPhone?: string; + replacers: Array<{ key: string; value: string }>; + }> = []; + const emailConf = engageMessage.email ? engageMessage.email : { content: '' }; + const emailContent = emailConf.content || ''; + + const { customerFields } = await replaceEditorAttributes({ + content: emailContent, + }); - const mailMessageId = Random.id(); + const onFinishPiping = async () => { + if (engageMessage.kind === MESSAGE_KINDS.MANUAL && customerInfos.length === 0) { + await EngageMessages.deleteOne({ _id: engageMessage._id }); + throw new Error('No customers found'); + } + + // save matched customers count + await EngageMessages.setCustomersCount(engageMessage._id, 'totalCustomersCount', customerInfos.length); + + await sendQueueMessage({ + action: 'writeLog', + data: { + engageMessageId, + msg: `Matched ${customerInfos.length} customers`, + }, + }); - // add new delivery report - EngageMessages.addNewDeliveryReport(message._id, mailMessageId, customer._id); + await EngageMessages.setCustomersCount(engageMessage._id, 'validCustomersCount', customerInfos.length); - // send email ========= - utils.executeSendViaEmail( - userEmail, - attachments, - customer, - subject, - replacedContent, - AWS_SES_CONFIG_SET, - message._id, - mailMessageId, - ); + if (customerInfos.length > 0) { + const data: any = { + customers: [], + fromEmail: user.email, + engageMessageId, + shortMessage: engageMessage.shortMessage || {}, + }; + + if (engageMessage.method === METHODS.EMAIL && engageMessage.email) { + const { replacedContent } = await replaceEditorAttributes({ + customerFields, + content: emailContent, + user, + }); + + engageMessage.email.content = replacedContent; + + data.email = engageMessage.email; + } + + const chunks = chunkArray(customerInfos, 3000); + + for (const chunk of chunks) { + data.customers = chunk; + + await sendQueueMessage({ action, data }); + } + } + }; + + const customerTransformerStream = new Transform({ + objectMode: true, + + async transform(customer: ICustomerDocument, _encoding, callback) { + const { replacers } = await replaceEditorAttributes({ + content: emailContent, + customer, + customerFields, + }); + + customerInfos.push({ + _id: customer._id, + primaryEmail: customer.primaryEmail, + emailValidationStatus: customer.emailValidationStatus, + phoneValidationStatus: customer.phoneValidationStatus, + primaryPhone: customer.primaryPhone, + replacers, + }); + + // signal upstream that we are ready to take more data + callback(); + }, + }); + + // generate fields option ======= + const fieldsOption = { + primaryEmail: 1, + emailValidationStatus: 1, + phoneValidationStatus: 1, + primaryPhone: 1, + }; + + for (const field of customerFields || []) { + fieldsOption[field] = 1; } + + const customersStream = (Customers.find(customersSelector, fieldsOption) as any).stream(); + + return new Promise((resolve, reject) => { + const pipe = customersStream.pipe(customerTransformerStream); + + pipe.on('finish', async () => { + try { + await onFinishPiping(); + } catch (e) { + return reject(e); + } + + resolve('done'); + }); + }); }; /** * Send via messenger */ -const sendViaMessenger = async (message: IEngageMessageDocument) => { - const { fromUserId, tagIds, brandIds, segmentIds, customerIds = [] } = message; +const sendViaMessenger = async ({ engageMessage, customersSelector, user }: IEngageParams) => { + const { fromUserId, messenger, _id } = engageMessage; - if (!message.messenger) { + if (!messenger) { return; } - const { brandId, content = '' } = message.messenger; - - const user = await Users.findOne({ _id: fromUserId }); - - if (!user) { - throw new Error('User not found'); - } + const { brandId, content } = messenger; // find integration const integration = await Integrations.findOne({ brandId, - kind: INTEGRATION_KIND_CHOICES.MESSENGER, + kind: KIND_CHOICES.MESSENGER, }); if (integration === null) { throw new Error('Integration not found'); } - // find matched customers - const customers = await findCustomers({ customerIds, segmentIds, tagIds, brandIds }); + const bulkSize = 1000; + + let iteratorCounter = 0; + let conversationsBulk = Conversations.collection.initializeOrderedBulkOp(); + let conversationMessagesBulk = ConversationMessages.collection.initializeOrderedBulkOp(); + + const customerFields = { firstName: 1, lastName: 1, primaryEmail: 1 }; + const customersStream = (Customers.find(customersSelector, customerFields) as any).stream(); - // save matched customer ids - EngageMessages.setCustomerIds(message._id, customers); + const executeBulks = () => { + return new Promise((resolve, reject) => { + /* istanbul ignore next */ + conversationsBulk.execute(err => { + if (err) { + if (err.message === 'Invalid Operation, no operations specified') { + debugBase(`Error during execute bulk ${err.message}`); + return resolve('done'); + } + + return reject(err); + } + + conversationMessagesBulk.execute(msgErr => { + if (msgErr) { + return reject(msgErr); + } + + conversationsBulk = Conversations.collection.initializeOrderedBulkOp(); + conversationMessagesBulk = ConversationMessages.collection.initializeOrderedBulkOp(); + + resolve('done'); + }); + }); + }); + }; + + const createConversations = async (customer: ICustomerDocument) => { + iteratorCounter++; - for (const customer of customers) { // replace keys in content - const replacedContent = replaceKeys({ content, customer, user }); + const { replacedContent } = await replaceEditorAttributes({ content, customer, user }); + + const now = new Date(); + const conversationId = Random.id(); + // create conversation - const conversation = await Conversations.createConversation({ + conversationsBulk.insert({ + _id: conversationId, + status: CONVERSATION_STATUSES.NEW, + createdAt: now, + updatedAt: now, userId: fromUserId, customerId: customer._id, integrationId: integration._id, content: replacedContent, + messageCount: 1, }); // create message - await ConversationMessages.createMessage({ + conversationMessagesBulk.insert({ engageData: { - messageId: message._id, + engageKind: 'auto', + messageId: _id, fromUserId, - ...message.messenger.toJSON(), + ...(messenger ? messenger.toJSON() : {}), }, - conversationId: conversation._id, + internal: false, + conversationId, userId: fromUserId, customerId: customer._id, content: replacedContent, }); - } -}; -/* - * Send engage messages - */ -export const send = (message: IEngageMessageDocument) => { - const { method, kind } = message; + /* istanbul ignore next */ + if (iteratorCounter % bulkSize === 0) { + customersStream.pause(); - if (method === METHODS.EMAIL) { - return sendViaEmail(message); - } + await executeBulks(); - // when kind is visitor auto, do not do anything - if (method === METHODS.MESSENGER && kind !== MESSAGE_KINDS.VISITOR_AUTO) { - return sendViaMessenger(message); - } -}; + customersStream.resume(); + } + }; -/* - * Handle engage unsubscribe request - */ -export const handleEngageUnSubscribe = (query: { cid: string }) => - Customers.updateOne({ _id: query.cid }, { $set: { doNotDisturb: 'Yes' } }); + const streamConversationWriter = new Writable({ + objectMode: true, -export const utils = { - executeSendViaEmail, -}; + async write(data, _encoding, callback) { + await createConversations(data); -export default { - replaceKeys, - send, -}; + callback(); + }, + }); + + return new Promise(resolve => { + const pipe = customersStream.pipe(streamConversationWriter); + + pipe.on('finish', async () => { + // save matched customers count + await EngageMessages.setCustomersCount(_id, 'totalCustomersCount', iteratorCounter); + + if (iteratorCounter % bulkSize !== 0) { + await executeBulks(); + } + + resolve('done'); + }); + }); +}; // end sendViaMessenger() diff --git a/src/data/resolvers/mutations/engages.ts b/src/data/resolvers/mutations/engages.ts index 13e5e08a7..8a30fa0de 100644 --- a/src/data/resolvers/mutations/engages.ts +++ b/src/data/resolvers/mutations/engages.ts @@ -1,57 +1,61 @@ -import { EngageMessages, Users } from '../../../db/models'; +import * as _ from 'underscore'; +import { EngageMessages } from '../../../db/models'; import { METHODS } from '../../../db/models/definitions/constants'; import { IEngageMessage } from '../../../db/models/definitions/engages'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { awsRequests } from '../../../trackers/engageTracker'; -import { MESSAGE_KINDS } from '../../constants'; +import { MESSAGE_KINDS, MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; -import { fetchCronsApi, getEnv, putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; +import { registerOnboardHistory, sendToWebhook } from '../../utils'; import { send } from './engageUtils'; interface IEngageMessageEdit extends IEngageMessage { _id: string; } +/** + * These fields contain too much data & it's inappropriate + * to save such data in each log row + */ +const emptyCustomers = { + customerIds: [], + messengerReceivedCustomerIds: [], +}; + const engageMutations = { /** * Create new message */ - async engageMessageAdd(_root, doc: IEngageMessage, { user }: { user: IUserDocument }) { - const { method, fromUserId } = doc; - - if (method === METHODS.EMAIL) { - // Checking if configs exist - getEnv({ name: 'AWS_SES_CONFIG_SET' }); - getEnv({ name: 'AWS_ENDPOINT' }); - - const fromUser = await Users.findOne({ _id: fromUserId }); - - const { VerifiedEmailAddresses = [] } = await awsRequests.getVerifiedEmails(); + async engageMessageAdd(_root, doc: IEngageMessage, { user, docModifier }: IContext) { + if (doc.kind !== MESSAGE_KINDS.MANUAL && doc.method === METHODS.SMS) { + throw new Error(`SMS engage message of kind ${doc.kind} is not supported`); + } - // If verified creates engagemessage - if (fromUser && !VerifiedEmailAddresses.includes(fromUser.email)) { - throw new Error('Email not verified'); - } + // fromUserId is not required in sms engage, so set it here + if (!doc.fromUserId) { + doc.fromUserId = user._id; } - const engageMessage = await EngageMessages.createEngageMessage(doc); + const engageMessage = await EngageMessages.createEngageMessage(docModifier(doc)); - // if manual and live then send immediately - if (doc.kind === MESSAGE_KINDS.MANUAL && doc.isLive) { - await send(engageMessage); - } + await sendToWebhook('create', 'engageMessages', engageMessage); + + await send(engageMessage); - if (engageMessage) { - await putCreateLog( - { - type: 'engage', - newData: JSON.stringify(doc), - object: engageMessage, - description: `${engageMessage.title} has been created`, + await putCreateLog( + { + type: MODULE_NAMES.ENGAGE, + newData: { + ...doc, + ...emptyCustomers, }, - user, - ); - } + object: { + ...engageMessage.toObject(), + ...emptyCustomers, + }, + }, + user, + ); return engageMessage; }, @@ -59,23 +63,19 @@ const engageMutations = { /** * Edit message */ - async engageMessageEdit(_root, { _id, ...doc }: IEngageMessageEdit, { user }: { user: IUserDocument }) { - const engageMessage = await EngageMessages.findOne({ _id }); + async engageMessageEdit(_root, { _id, ...doc }: IEngageMessageEdit, { user }: IContext) { + const engageMessage = await EngageMessages.getEngageMessage(_id); const updated = await EngageMessages.updateEngageMessage(_id, doc); - await fetchCronsApi({ path: '/update-or-remove-schedule', method: 'POST', body: { _id, update: 'true' } }); - - if (engageMessage) { - await putUpdateLog( - { - type: 'engage', - object: engageMessage, - newData: JSON.stringify(updated), - description: `${engageMessage.title} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.ENGAGE, + object: { ...engageMessage.toObject(), ...emptyCustomers }, + newData: { ...updated.toObject(), ...emptyCustomers }, + updatedDocument: updated, + }, + user, + ); return EngageMessages.findOne({ _id }); }, @@ -83,23 +83,18 @@ const engageMutations = { /** * Remove message */ - async engageMessageRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const engageMessage = await EngageMessages.findOne({ _id }); - - await fetchCronsApi({ path: '/update-or-remove-schedule', method: 'POST', body: { _id } }); + async engageMessageRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const engageMessage = await EngageMessages.getEngageMessage(_id); const removed = await EngageMessages.removeEngageMessage(_id); - if (engageMessage) { - await putDeleteLog( - { - type: 'engage', - object: engageMessage, - description: `${engageMessage.title} has been removed`, - }, - user, - ); - } + await putDeleteLog( + { + type: MODULE_NAMES.ENGAGE, + object: { ...engageMessage.toObject(), ...emptyCustomers }, + }, + user, + ); return removed; }, @@ -107,20 +102,8 @@ const engageMutations = { /** * Engage message set live */ - async engageMessageSetLive(_root, { _id }: { _id: string }) { - const engageMessage = await EngageMessages.engageMessageSetLive(_id); - - const { kind } = engageMessage; - - if (kind === MESSAGE_KINDS.AUTO || kind === MESSAGE_KINDS.VISITOR_AUTO) { - await fetchCronsApi({ - path: '/create-schedule', - method: 'POST', - body: { message: JSON.stringify(engageMessage) }, - }); - } - - return engageMessage; + engageMessageSetLive(_root, { _id }: { _id: string }) { + return EngageMessages.engageMessageSetLive(_id); }, /** @@ -133,12 +116,34 @@ const engageMutations = { /** * Engage message set live manual */ - async engageMessageSetLiveManual(_root, { _id }: { _id: string }) { - const engageMessage = await EngageMessages.engageMessageSetLive(_id); + engageMessageSetLiveManual(_root, { _id }: { _id: string }) { + return EngageMessages.engageMessageSetLive(_id); + }, - await send(engageMessage); + engagesUpdateConfigs(_root, configsMap, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesUpdateConfigs(configsMap); + }, - return engageMessage; + /** + * Engage message verify email + */ + async engageMessageVerifyEmail(_root, { email }: { email: string }, { dataSources, user }: IContext) { + await registerOnboardHistory({ type: 'engageVerifyEmail', user }); + + return dataSources.EngagesAPI.engagesVerifyEmail({ email }); + }, + + /** + * Engage message remove verified email + */ + engageMessageRemoveVerifiedEmail(_root, { email }: { email: string }, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesRemoveVerifiedEmail({ email }); + }, + + async engageMessageSendTestEmail(_root, args, { dataSources, user }: IContext) { + await registerOnboardHistory({ type: 'engageSendTestEmail', user }); + + return dataSources.EngagesAPI.engagesSendTestEmail(args); }, }; @@ -148,5 +153,8 @@ checkPermission(engageMutations, 'engageMessageRemove', 'engageMessageRemove'); checkPermission(engageMutations, 'engageMessageSetLive', 'engageMessageSetLive'); checkPermission(engageMutations, 'engageMessageSetPause', 'engageMessageSetPause'); checkPermission(engageMutations, 'engageMessageSetLiveManual', 'engageMessageSetLiveManual'); +checkPermission(engageMutations, 'engageMessageVerifyEmail', 'engageMessageRemove'); +checkPermission(engageMutations, 'engageMessageRemoveVerifiedEmail', 'engageMessageRemove'); +checkPermission(engageMutations, 'engageMessageSendTestEmail', 'engageMessageRemove'); export default engageMutations; diff --git a/src/data/resolvers/mutations/fields.ts b/src/data/resolvers/mutations/fields.ts index dcb2c3c67..fee9c1353 100644 --- a/src/data/resolvers/mutations/fields.ts +++ b/src/data/resolvers/mutations/fields.ts @@ -1,8 +1,10 @@ import { Fields, FieldsGroups } from '../../../db/models'; import { IField, IFieldGroup } from '../../../db/models/definitions/fields'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { IOrderInput } from '../../../db/models/Fields'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; interface IFieldsEdit extends IField { _id: string; @@ -16,14 +18,25 @@ const fieldMutations = { /** * Adds field object */ - fieldsAdd(_root, args: IField, { user }: { user: IUserDocument }) { - return Fields.createField({ ...args, lastUpdatedUserId: user._id }); + async fieldsAdd(_root, args: IField, { user }: IContext) { + const field = await Fields.createField({ ...args, lastUpdatedUserId: user._id }); + + await putCreateLog( + { + type: MODULE_NAMES.FIELD, + newData: args, + object: field, + }, + user, + ); + + return field; }, /** * Updates field object */ - fieldsEdit(_root, { _id, ...doc }: IFieldsEdit, { user }: { user: IUserDocument }) { + fieldsEdit(_root, { _id, ...doc }: IFieldsEdit, { user }: IContext) { return Fields.updateField(_id, { ...doc, lastUpdatedUserId: user._id }); }, @@ -44,11 +57,7 @@ const fieldMutations = { /** * Update field's visible */ - fieldsUpdateVisible( - _root, - { _id, isVisible }: { _id: string; isVisible: boolean }, - { user }: { user: IUserDocument }, - ) { + fieldsUpdateVisible(_root, { _id, isVisible }: { _id: string; isVisible: boolean }, { user }: IContext) { return Fields.updateFieldsVisible(_id, isVisible, user._id); }, }; @@ -57,14 +66,14 @@ const fieldsGroupsMutations = { /** * Create a new group for fields */ - fieldsGroupsAdd(_root, doc: IFieldGroup, { user }: { user: IUserDocument }) { - return FieldsGroups.createGroup({ ...doc, lastUpdatedUserId: user._id }); + fieldsGroupsAdd(_root, doc: IFieldGroup, { user, docModifier }: IContext) { + return FieldsGroups.createGroup(docModifier({ ...doc, lastUpdatedUserId: user._id })); }, /** * Update group for fields */ - fieldsGroupsEdit(_root, { _id, ...doc }: IFieldsGroupsEdit, { user }: { user: IUserDocument }) { + fieldsGroupsEdit(_root, { _id, ...doc }: IFieldsGroupsEdit, { user }: IContext) { return FieldsGroups.updateGroup(_id, { ...doc, lastUpdatedUserId: user._id, @@ -81,16 +90,12 @@ const fieldsGroupsMutations = { /** * Update field group's visible */ - fieldsGroupsUpdateVisible( - _root, - { _id, isVisible }: { _id: string; isVisible: boolean }, - { user }: { user: IUserDocument }, - ) { + fieldsGroupsUpdateVisible(_root, { _id, isVisible }: { _id: string; isVisible: boolean }, { user }: IContext) { return FieldsGroups.updateGroupVisible(_id, isVisible, user._id); }, }; -moduleCheckPermission(fieldMutations, 'manageFields'); -moduleCheckPermission(fieldsGroupsMutations, 'manageFieldsGroups'); +moduleCheckPermission(fieldMutations, 'manageForms'); +moduleCheckPermission(fieldsGroupsMutations, 'manageForms'); export { fieldsGroupsMutations, fieldMutations }; diff --git a/src/data/resolvers/mutations/forms.ts b/src/data/resolvers/mutations/forms.ts index aaf003495..41eaeb7a1 100644 --- a/src/data/resolvers/mutations/forms.ts +++ b/src/data/resolvers/mutations/forms.ts @@ -1,26 +1,62 @@ -import { Forms } from '../../../db/models'; +import { Fields, Forms, FormSubmissions } from '../../../db/models'; import { IForm } from '../../../db/models/definitions/forms'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; interface IFormsEdit extends IForm { _id: string; } +interface IFormSubmission { + contentType: string; + contentTypeId: string; + formSubmissions: { [key: string]: JSON }; + formId: string; +} + const formMutations = { /** * Create a new form */ - formsAdd(_root, doc: IForm, { user }: { user: IUserDocument }) { - return Forms.createForm(doc, user._id); + formsAdd(_root, doc: IForm, { user, docModifier }: IContext) { + return Forms.createForm(docModifier(doc), user._id); }, /** - * Update form data + * Update a form data */ formsEdit(_root, { _id, ...doc }: IFormsEdit) { return Forms.updateForm(_id, doc); }, + + /** + * Create a form submission data + */ + async formSubmissionsSave(_root, { formId, contentTypeId, contentType, formSubmissions }: IFormSubmission) { + const cleanedFormSubmissions = await Fields.cleanMulti(formSubmissions || {}); + + for (const formFieldId of Object.keys(cleanedFormSubmissions)) { + const formSubmission = await FormSubmissions.findOne({ contentTypeId, contentType, formFieldId }); + + if (formSubmission) { + formSubmission.value = cleanedFormSubmissions[formFieldId]; + + formSubmission.save(); + } else { + const doc = { + contentTypeId, + contentType, + formFieldId, + formId, + value: formSubmissions[formFieldId], + }; + + FormSubmissions.createFormSubmission(doc); + } + } + + return true; + }, }; moduleCheckPermission(formMutations, 'manageForms'); diff --git a/src/data/resolvers/mutations/growthHacks.ts b/src/data/resolvers/mutations/growthHacks.ts new file mode 100644 index 000000000..1280a9016 --- /dev/null +++ b/src/data/resolvers/mutations/growthHacks.ts @@ -0,0 +1,83 @@ +import { GrowthHacks } from '../../../db/models'; +import { IItemDragCommonFields } from '../../../db/models/definitions/boards'; +import { IGrowthHack } from '../../../db/models/definitions/growthHacks'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { itemsAdd, itemsArchive, itemsChange, itemsCopy, itemsEdit, itemsRemove } from './boardUtils'; + +interface IGrowthHacksEdit extends IGrowthHack { + _id: string; +} + +const growthHackMutations = { + /** + * Create new growth hack + */ + async growthHacksAdd( + _root, + doc: IGrowthHack & { proccessId: string; aboveItemId: string }, + { user, docModifier }: IContext, + ) { + return itemsAdd(doc, 'growthHack', user, docModifier, GrowthHacks.createGrowthHack); + }, + + /** + * Edit a growth hack + */ + async growthHacksEdit(_root, { _id, proccessId, ...doc }: IGrowthHacksEdit & { proccessId: string }, { user }) { + const oldGrowthHack = await GrowthHacks.getGrowthHack(_id); + + return itemsEdit(_id, 'growthHack', oldGrowthHack, doc, proccessId, user, GrowthHacks.updateGrowthHack); + }, + + /** + * Change a growth hack + */ + async growthHacksChange(_root, doc: IItemDragCommonFields, { user }: IContext) { + return itemsChange(doc, 'growthHack', user, GrowthHacks.updateGrowthHack); + }, + + /** + * Remove a growth hack + */ + async growthHacksRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + return itemsRemove(_id, 'growthHack', user); + }, + + /** + * Watch a growth hack + */ + growthHacksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { + return GrowthHacks.watchGrowthHack(_id, isAdd, user._id); + }, + + /** + * Vote a growth hack + */ + growthHacksVote(_root, { _id, isVote }: { _id: string; isVote: boolean }, { user }: { user: IUserDocument }) { + return GrowthHacks.voteGrowthHack(_id, isVote, user._id); + }, + + async growthHacksCopy(_root, { _id, proccessId }: { _id: string; proccessId: string }, { user }: IContext) { + const extraDocs = ['votedUserIds', 'voteCount', 'hackStages', 'reach', 'impact', 'confidence', 'ease']; + + return itemsCopy(_id, proccessId, 'growthHack', user, extraDocs, GrowthHacks.createGrowthHack); + }, + + async growthHacksArchive( + _root, + { stageId, proccessId }: { stageId: string; proccessId: string }, + { user }: IContext, + ) { + return itemsArchive(stageId, 'growthHack', proccessId, user); + }, +}; + +checkPermission(growthHackMutations, 'growthHacksAdd', 'growthHacksAdd'); +checkPermission(growthHackMutations, 'growthHacksEdit', 'growthHacksEdit'); +checkPermission(growthHackMutations, 'growthHacksRemove', 'growthHacksRemove'); +checkPermission(growthHackMutations, 'growthHacksWatch', 'growthHacksWatch'); +checkPermission(growthHackMutations, 'growthHacksArchive', 'growthHacksArchive'); + +export default growthHackMutations; diff --git a/src/data/resolvers/mutations/importHistory.ts b/src/data/resolvers/mutations/importHistory.ts index ec664bbd9..b1a189ff3 100644 --- a/src/data/resolvers/mutations/importHistory.ts +++ b/src/data/resolvers/mutations/importHistory.ts @@ -1,40 +1,31 @@ import { ImportHistory } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import messageBroker from '../../../messageBroker'; +import { MODULE_NAMES, RABBITMQ_QUEUES } from '../../constants'; +import { putDeleteLog } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; -import { fetchWorkersApi, putDeleteLog } from '../../utils'; +import { IContext } from '../../types'; const importHistoryMutations = { /** * Removes a history * @param {string} param1._id ImportHistory id */ - async importHistoriesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const importHistory = await ImportHistory.findOne({ _id }); - - if (!importHistory) { - throw new Error('History not found'); - } + async importHistoriesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const importHistory = await ImportHistory.getImportHistory(_id); await ImportHistory.updateOne({ _id: importHistory._id }, { $set: { status: 'Removing' } }); - await fetchWorkersApi({ - path: '/import-remove', - method: 'POST', - body: { - targetIds: JSON.stringify(importHistory.ids || []), - contentType: importHistory.contentType, - importHistoryId: importHistory._id, - }, + const response = await messageBroker().sendRPCMessage(RABBITMQ_QUEUES.RPC_API_TO_WORKERS, { + action: 'removeImport', + contentType: importHistory.contentType, + importHistoryId: importHistory._id, }); - await putDeleteLog( - { - type: 'importHistory', - object: importHistory, - description: `${importHistory._id}-${importHistory.date} has been removed`, - }, - user, - ); + if (response.status === 'ok') { + await putDeleteLog({ type: MODULE_NAMES.IMPORT_HISTORY, object: importHistory }, user); + } else { + throw new Error(response.message); + } return ImportHistory.findOne({ _id: importHistory._id }); }, @@ -42,14 +33,10 @@ const importHistoryMutations = { /** * Cancel uploading process */ - async importHistoriesCancel(_root, { _id }: { _id: string }) { - const importHistory = await ImportHistory.findOne({ _id }); - - if (!importHistory) { - throw new Error('History not found'); - } - - await fetchWorkersApi({ path: '/import-cancel', method: 'POST' }); + async importHistoriesCancel(_root) { + await messageBroker().sendMessage(RABBITMQ_QUEUES.WORKERS, { + type: 'cancelImport', + }); return true; }, diff --git a/src/data/resolvers/mutations/index.ts b/src/data/resolvers/mutations/index.ts index 544e2e44d..81d02ede3 100644 --- a/src/data/resolvers/mutations/index.ts +++ b/src/data/resolvers/mutations/index.ts @@ -1,8 +1,10 @@ import boards from './boards'; import brands from './brands'; import channels from './channels'; +import checklists from './checklists'; import companies from './companies'; import configs from './configs'; +import conformity from './conformities'; import conversations from './conversations'; import customers from './customers'; import deals from './deals'; @@ -10,6 +12,7 @@ import emailTemplates from './emailTemplates'; import engages from './engages'; import { fieldMutations as fields, fieldsGroupsMutations as fieldsgroups } from './fields'; import forms from './forms'; +import growthHacks from './growthHacks'; import importHistory from './importHistory'; import integrations from './integrations'; import internalNotes from './internalNotes'; @@ -17,14 +20,19 @@ import knowledgeBase from './knowledgeBase'; import messengerApps from './messengerApps'; import notifications from './notifications'; import { permissionMutations as permissions, usersGroupMutations as usersGroups } from './permissions'; +import pipelineLabels from './pipelineLabels'; +import pipelineTemplates from './pipelineTemplates'; import products from './products'; import responseTemplates from './responseTemplates'; +import robot from './robot'; import scripts from './scripts'; import segments from './segments'; import tags from './tags'; import tasks from './tasks'; import tickets from './tickets'; import users from './users'; +import webhooks from './webhooks'; +import widgets from './widgets'; export default { ...users, @@ -49,6 +57,7 @@ export default { ...boards, ...products, ...configs, + ...conformity, ...fieldsgroups, ...importHistory, ...messengerApps, @@ -56,4 +65,11 @@ export default { ...usersGroups, ...tickets, ...tasks, + ...growthHacks, + ...pipelineLabels, + ...pipelineTemplates, + ...checklists, + ...robot, + ...widgets, + ...webhooks, }; diff --git a/src/data/resolvers/mutations/integrations.ts b/src/data/resolvers/mutations/integrations.ts index ab5e28903..40ceac4c2 100644 --- a/src/data/resolvers/mutations/integrations.ts +++ b/src/data/resolvers/mutations/integrations.ts @@ -1,63 +1,82 @@ -import { Integrations } from '../../../db/models'; +import * as telemetry from 'erxes-telemetry'; +import { getUniqueValue } from '../../../db/factories'; +import { Channels, Customers, EmailDeliveries, Integrations } from '../../../db/models'; +import { KIND_CHOICES } from '../../../db/models/definitions/constants'; import { IIntegration, IMessengerData, IUiOptions } from '../../../db/models/definitions/integrations'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { IExternalIntegrationParams, IMessengerIntegration } from '../../../db/models/Integrations'; +import { IExternalIntegrationParams } from '../../../db/models/Integrations'; +import { debugExternalApi } from '../../../debuggers'; +import messageBroker from '../../../messageBroker'; +import { MODULE_NAMES, RABBITMQ_QUEUES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { checkPermission } from '../../permissions/wrappers'; -import { fetchIntegrationApi, putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; +import { registerOnboardHistory, replaceEditorAttributes } from '../../utils'; -interface IEditMessengerIntegration extends IMessengerIntegration { +interface IEditIntegration extends IIntegration { _id: string; } -interface IEditFormIntegration extends IIntegration { +interface IArchiveParams { _id: string; + status: boolean; +} + +interface ISmsParams { + integrationId: string; + content: string; + to: string; } const integrationMutations = { /** - * Create a new messenger integration + * Creates a new messenger integration */ - async integrationsCreateMessengerIntegration(_root, doc: IMessengerIntegration, { user }: { user: IUserDocument }) { - const integration = await Integrations.createMessengerIntegration(doc); + async integrationsCreateMessengerIntegration(_root, doc: IIntegration, { user }: IContext) { + const integration = await Integrations.createMessengerIntegration(doc, user._id); - if (integration) { - await putCreateLog( - { - type: 'integration', - newData: JSON.stringify(doc), - object: integration, - description: `${integration.name} has been created`, - }, - user, - ); + if (doc.channelIds) { + await Channels.updateMany({ _id: { $in: doc.channelIds } }, { $push: { integrationIds: integration._id } }); } + await putCreateLog( + { + type: MODULE_NAMES.INTEGRATION, + newData: { ...doc, createdUserId: user._id, isActive: true }, + object: integration, + }, + user, + ); + + telemetry.trackCli('integration_created', { type: 'messenger' }); + + await registerOnboardHistory({ type: 'messengerIntegrationCreate', user }); + return integration; }, /** - * Update messenger integration + * Updates a messenger integration */ - async integrationsEditMessengerIntegration( - _root, - { _id, ...fields }: IEditMessengerIntegration, - { user }: { user: IUserDocument }, - ) { - const integration = await Integrations.findOne({ _id }); + async integrationsEditMessengerIntegration(_root, { _id, ...fields }: IEditIntegration, { user }: IContext) { + const integration = await Integrations.getIntegration(_id); const updated = await Integrations.updateMessengerIntegration(_id, fields); - if (integration) { - await putUpdateLog( - { - type: 'integration', - object: integration, - newData: JSON.stringify(fields), - description: `${integration.name} has been edited`, - }, - user, - ); + await Channels.updateMany({ integrationIds: integration._id }, { $pull: { integrationIds: integration._id } }); + + if (fields.channelIds) { + await Channels.updateMany({ _id: { $in: fields.channelIds } }, { $push: { integrationIds: integration._id } }); } + await putUpdateLog( + { + type: MODULE_NAMES.INTEGRATION, + object: integration, + newData: fields, + updatedDocument: updated, + }, + user, + ); + return updated; }, @@ -78,33 +97,91 @@ const integrationMutations = { /** * Create a new messenger integration */ - integrationsCreateFormIntegration(_root, doc: IIntegration) { - return Integrations.createFormIntegration(doc); + async integrationsCreateLeadIntegration(_root, doc: IIntegration, { user }: IContext) { + const integration = await Integrations.createLeadIntegration(doc, user._id); + + await putCreateLog( + { + type: MODULE_NAMES.INTEGRATION, + newData: { ...doc, createdUserId: user._id, isActive: true }, + object: integration, + }, + user, + ); + + telemetry.trackCli('integration_created', { type: 'lead' }); + + await registerOnboardHistory({ type: 'leadIntegrationCreate', user }); + + return integration; }, /** - * Edit a form integration + * Edit a lead integration */ - integrationsEditFormIntegration(_root, { _id, ...doc }: IEditFormIntegration) { - return Integrations.updateFormIntegration(_id, doc); + integrationsEditLeadIntegration(_root, { _id, ...doc }: IEditIntegration) { + return Integrations.updateLeadIntegration(_id, doc); }, - /* + /** * Create external integrations like twitter, facebook, gmail etc ... */ - async integrationsCreateExternalIntegration(_root, { data, ...doc }: IExternalIntegrationParams & { data: object }) { - const integration = await Integrations.createExternalIntegration(doc); + async integrationsCreateExternalIntegration( + _root, + { data, ...doc }: IExternalIntegrationParams & { data: object }, + { user, dataSources }: IContext, + ) { + const modifiedDoc: any = { ...doc }; + + if (modifiedDoc.kind === KIND_CHOICES.WEBHOOK) { + modifiedDoc.webhookData = { ...data }; + modifiedDoc.webhookData.token = await getUniqueValue(Integrations, 'token'); + } + + const integration = await Integrations.createExternalIntegration(modifiedDoc, user._id); + + if (doc.channelIds) { + await Channels.updateMany({ _id: { $in: doc.channelIds } }, { $push: { integrationIds: integration._id } }); + } + + let kind = doc.kind; + + if (kind.includes('nylas')) { + kind = 'nylas'; + } + + if (kind.includes('facebook')) { + kind = 'facebook'; + } + + if (kind === 'twitter-dm') { + kind = 'twitter'; + } + + if (kind.includes('smooch')) { + kind = 'smooch'; + } try { - await fetchIntegrationApi({ - path: `/${doc.kind}/create-integration`, - method: 'POST', - body: { + if (KIND_CHOICES.WEBHOOK !== kind) { + await dataSources.IntegrationsAPI.createIntegration(kind, { accountId: doc.accountId, + kind: doc.kind, integrationId: integration._id, data: data ? JSON.stringify(data) : '', + }); + } + + telemetry.trackCli('integration_created', { type: doc.kind }); + + await putCreateLog( + { + type: MODULE_NAMES.INTEGRATION, + newData: { ...doc, createdUserId: user._id, isActive: true }, + object: integration, }, - }); + user, + ); } catch (e) { await Integrations.remove({ _id: integration._id }); throw new Error(e); @@ -113,41 +190,166 @@ const integrationMutations = { return integration; }, + async integrationsEditCommonFields(_root, { _id, name, brandId, channelIds, data }, { user }) { + const integration = await Integrations.getIntegration(_id); + + const doc: any = { name, brandId, data }; + + switch (integration.kind) { + case KIND_CHOICES.WEBHOOK: { + doc.webhookData = data; + + break; + } + } + + await Integrations.update({ _id }, { $set: doc }); + const updated = await Integrations.getIntegration(_id); + + await Channels.updateMany({ integrationIds: integration._id }, { $pull: { integrationIds: integration._id } }); + + if (channelIds) { + await Channels.updateMany({ _id: { $in: channelIds } }, { $push: { integrationIds: integration._id } }); + } + + await putUpdateLog( + { + type: MODULE_NAMES.INTEGRATION, + object: { name: integration.name, brandId: integration.brandId }, + newData: { name, brandId }, + updatedDocument: updated, + }, + user, + ); + + return updated; + }, + /** - * Delete an integration + * Deletes an integration */ - async integrationsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const integration = await Integrations.findOne({ _id }); - - if (integration) { - if (integration.kind === 'facebook') { - await fetchIntegrationApi({ - path: '/integrations/remove', - method: 'POST', - body: { - integrationId: _id, - }, - }); + async integrationsRemove(_root, { _id }: { _id: string }, { user, dataSources }: IContext) { + const integration = await Integrations.getIntegration(_id); + + try { + if ( + [ + 'facebook-messenger', + 'facebook-post', + 'gmail', + 'callpro', + 'nylas-gmail', + 'nylas-imap', + 'nylas-office365', + 'nylas-outlook', + 'nylas-exchange', + 'nylas-yahoo', + 'chatfuel', + 'twitter-dm', + 'smooch-viber', + 'smooch-telegram', + 'smooch-line', + 'smooch-twilio', + 'whatsapp', + 'telnyx', + ].includes(integration.kind) + ) { + await dataSources.IntegrationsAPI.removeIntegration({ integrationId: _id }); } - await putDeleteLog( - { - type: 'integration', - object: integration, - description: `${integration.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.INTEGRATION, object: integration }, user); - return Integrations.removeIntegration(_id); + return Integrations.removeIntegration(_id); + } catch (e) { + debugExternalApi(e); + throw e; + } }, /** * Delete an account */ async integrationsRemoveAccount(_root, { _id }: { _id: string }) { - return fetchIntegrationApi({ path: '/accounts/remove', method: 'post', body: { _id } }); + try { + const { erxesApiIds } = await messageBroker().sendRPCMessage(RABBITMQ_QUEUES.RPC_API_TO_INTEGRATIONS, { + action: 'remove-account', + data: { _id }, + }); + + for (const id of erxesApiIds) { + await Integrations.removeIntegration(id); + } + + return 'success'; + } catch (e) { + debugExternalApi(e); + throw e; + } + }, + + /** + * Send mail + */ + async integrationSendMail(_root, args: any, { dataSources, user }: IContext) { + const { erxesApiId, body, customerId, ...doc } = args; + + let kind = doc.kind; + + if (kind.includes('nylas')) { + kind = 'nylas'; + } + + const customer = customerId ? await Customers.findOne({ _id: customerId }) : undefined; + const { replacedContent } = await replaceEditorAttributes({ content: body, user, customer: customer || undefined }); + + doc.body = replacedContent || ''; + + try { + await dataSources.IntegrationsAPI.sendEmail(kind, { + erxesApiId, + data: JSON.stringify(doc), + }); + } catch (e) { + debugExternalApi(e); + throw e; + } + + const customerIds = await Customers.find({ primaryEmail: { $in: doc.to } }).distinct('_id'); + + doc.userId = user._id; + + for (const customerId of customerIds) { + await EmailDeliveries.createEmailDelivery({ ...doc, customerId }); + } + + return; + }, + + async integrationsArchive(_root, { _id, status }: IArchiveParams, { user }: IContext) { + const integration = await Integrations.getIntegration(_id); + + const updated = await Integrations.updateOne({ _id }, { $set: { isActive: !status } }); + + await putUpdateLog( + { + type: MODULE_NAMES.INTEGRATION, + object: integration, + newData: { isActive: !status }, + description: `"${integration.name}" has been ${status === true ? 'archived' : 'unarchived'}.`, + updatedDocument: updated, + }, + user, + ); + + return Integrations.findOne({ _id }); + }, + + async integrationsUpdateConfigs(_root, { configsMap }, { dataSources }: IContext) { + return dataSources.IntegrationsAPI.updateConfigs(configsMap); + }, + + async integrationsSendSms(_root, args: ISmsParams, { dataSources }: IContext) { + return dataSources.IntegrationsAPI.sendSms(args); }, }; @@ -162,8 +364,11 @@ checkPermission( 'integrationsSaveMessengerAppearanceData', ); checkPermission(integrationMutations, 'integrationsSaveMessengerConfigs', 'integrationsSaveMessengerConfigs'); -checkPermission(integrationMutations, 'integrationsCreateFormIntegration', 'integrationsCreateFormIntegration'); -checkPermission(integrationMutations, 'integrationsEditFormIntegration', 'integrationsEditFormIntegration'); +checkPermission(integrationMutations, 'integrationsCreateLeadIntegration', 'integrationsCreateLeadIntegration'); +checkPermission(integrationMutations, 'integrationsEditLeadIntegration', 'integrationsEditLeadIntegration'); checkPermission(integrationMutations, 'integrationsRemove', 'integrationsRemove'); +checkPermission(integrationMutations, 'integrationsArchive', 'integrationsArchive'); +checkPermission(integrationMutations, 'integrationsEditCommonFields', 'integrationsEdit'); +checkPermission(integrationMutations, 'integrationsUpdateConfigs', 'integrationsEdit'); export default integrationMutations; diff --git a/src/data/resolvers/mutations/internalNotes.ts b/src/data/resolvers/mutations/internalNotes.ts index 3abdb5465..5351d2826 100644 --- a/src/data/resolvers/mutations/internalNotes.ts +++ b/src/data/resolvers/mutations/internalNotes.ts @@ -1,96 +1,205 @@ -import { Deals, InternalNotes, Pipelines, Stages } from '../../../db/models'; -import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { + Companies, + Customers, + Deals, + GrowthHacks, + InternalNotes, + Pipelines, + Products, + Stages, + Tasks, + Tickets, + Users, +} from '../../../db/models'; +import { BOARD_TYPES, NOTIFICATION_CONTENT_TYPES, NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { IDealDocument } from '../../../db/models/definitions/deals'; import { IInternalNote } from '../../../db/models/definitions/internalNotes'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { ITaskDocument } from '../../../db/models/definitions/tasks'; +import { ITicketDocument } from '../../../db/models/definitions/tickets'; +import { graphqlPubsub } from '../../../pubsub'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleRequireLogin } from '../../permissions/wrappers'; -import utils, { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; +import utils, { ISendNotification } from '../../utils'; +import { notifiedUserIds } from '../boardUtils'; interface IInternalNotesEdit extends IInternalNote { _id: string; } +const sendNotificationOfItems = async ( + item: IDealDocument | ITicketDocument | ITaskDocument, + doc: ISendNotification, + contentType: string, + excludeUserIds: string[], +) => { + const notifDocItems = { ...doc }; + const relatedReceivers = await notifiedUserIds(item); + notifDocItems.action = `added note in ${contentType}`; + + notifDocItems.receivers = relatedReceivers.filter(id => { + return excludeUserIds.indexOf(id) < 0; + }); + + utils.sendNotification(notifDocItems); + + graphqlPubsub.publish('activityLogsChanged', {}); +}; + const internalNoteMutations = { /** * Adds internalNote object and also adds an activity log */ - async internalNotesAdd(_root, args: IInternalNote, { user }: { user: IUserDocument }) { - switch (args.contentType) { - case 'deal': { - const deal = await Deals.getDeal(args.contentTypeId); - const stage = await Stages.getStage(deal.stageId || ''); - const pipeline = await Pipelines.getPipeline(stage.pipelineId || ''); - - const title = `${user.details ? user.details.fullName : 'Someone'} mentioned you in "${deal.name}" deal`; - - utils.sendNotification({ - createdUser: user._id, - notifType: NOTIFICATION_TYPES.DEAL_EDIT, - title, - content: title, - link: `/deal/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}`, - receivers: args.mentionedUserIds || [], - }); - } - - default: - break; + async internalNotesAdd(_root, args: IInternalNote, { user }: IContext) { + const { contentType, contentTypeId } = args; + const mentionedUserIds = args.mentionedUserIds || []; + + const notifDoc: ISendNotification = { + title: `${contentType.toUpperCase()} updated`, + createdUser: user, + action: `mentioned you in ${contentType}`, + receivers: mentionedUserIds, + content: '', + link: '', + notifType: '', + contentType: '', + contentTypeId: '', + }; + + if (contentType === MODULE_NAMES.DEAL) { + const deal = await Deals.getDeal(contentTypeId); + const stage = await Stages.getStage(deal.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + + notifDoc.notifType = NOTIFICATION_TYPES.DEAL_EDIT; + notifDoc.content = `"${deal.name}"`; + notifDoc.link = `/deal/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}&itemId=${deal._id}`; + notifDoc.contentTypeId = deal._id; + notifDoc.contentType = NOTIFICATION_CONTENT_TYPES.DEAL; + + await sendNotificationOfItems(deal, notifDoc, contentType, [...mentionedUserIds, user._id]); } - const internalNote = await InternalNotes.createInternalNote(args, user); + if (contentType === MODULE_NAMES.CUSTOMER) { + const customer = await Customers.getCustomer(contentTypeId); + + notifDoc.notifType = NOTIFICATION_TYPES.CUSTOMER_MENTION; + notifDoc.content = Customers.getCustomerName(customer); + notifDoc.link = `/contacts/details/${customer._id}`; + notifDoc.contentTypeId = customer._id; + notifDoc.contentType = NOTIFICATION_CONTENT_TYPES.CUSTOMER; + } + + if (contentType === MODULE_NAMES.COMPANY) { + const company = await Companies.getCompany(contentTypeId); + + notifDoc.notifType = NOTIFICATION_TYPES.CUSTOMER_MENTION; + notifDoc.content = Companies.getCompanyName(company); + notifDoc.link = `/companies/details/${company._id}`; + notifDoc.contentTypeId = company._id; + notifDoc.contentType = NOTIFICATION_CONTENT_TYPES.COMPANY; + } + + if (contentType === MODULE_NAMES.TICKET) { + const ticket = await Tickets.getTicket(contentTypeId); + const stage = await Stages.getStage(ticket.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + + notifDoc.notifType = NOTIFICATION_TYPES.TICKET_EDIT; + notifDoc.content = `"${ticket.name}"`; + notifDoc.link = `/inbox/ticket/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}&itemId=${ticket._id}`; + notifDoc.contentTypeId = ticket._id; + notifDoc.contentType = NOTIFICATION_CONTENT_TYPES.TICKET; + + await sendNotificationOfItems(ticket, notifDoc, contentType, [...mentionedUserIds, user._id]); + } + + if (contentType === MODULE_NAMES.TASK) { + const task = await Tasks.getTask(contentTypeId); + const stage = await Stages.getStage(task.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); - if (internalNote) { - await putCreateLog( - { - type: 'internalNote', - newData: JSON.stringify(args), - object: internalNote, - description: `${internalNote.contentType} has been created`, - }, - user, - ); + notifDoc.notifType = NOTIFICATION_TYPES.TASK_EDIT; + notifDoc.content = `"${task.name}"`; + notifDoc.link = `/task/board?id=${pipeline.boardId}&pipelineId=${pipeline._id}&itemId=${task._id}`; + notifDoc.contentTypeId = task._id; + notifDoc.contentType = NOTIFICATION_CONTENT_TYPES.TASK; + + await sendNotificationOfItems(task, notifDoc, contentType, [...mentionedUserIds, user._id]); + } + + if (contentType === MODULE_NAMES.GROWTH_HACK) { + const hack = await GrowthHacks.getGrowthHack(contentTypeId); + + notifDoc.content = `${hack.name}`; + } + + if (contentType === MODULE_NAMES.USER) { + const usr = await Users.getUser(contentTypeId); + + notifDoc.content = `${usr.username || usr.email}`; + } + + if (contentType === MODULE_NAMES.PRODUCT) { + const product = await Products.getProduct({ _id: contentTypeId }); + + notifDoc.content = product.name; + } + + if (notifDoc.contentType) { + await utils.sendNotification(notifDoc); } + const internalNote = await InternalNotes.createInternalNote(args, user); + + await putCreateLog( + { + type: MODULE_NAMES.INTERNAL_NOTE, + newData: { ...args, createdUserId: user._id, createdAt: internalNote.createdAt }, + object: internalNote, + description: `A note for ${internalNote.contentType} "${notifDoc.content}" has been created`, + }, + user, + ); + return internalNote; }, /** * Updates internalNote object */ - async internalNotesEdit(_root, { _id, ...doc }: IInternalNotesEdit, { user }: { user: IUserDocument }) { - const internalNote = await InternalNotes.findOne({ _id }); + async internalNotesEdit(_root, { _id, ...doc }: IInternalNotesEdit, { user }: IContext) { + const internalNote = await InternalNotes.getInternalNote(_id); const updated = await InternalNotes.updateInternalNote(_id, doc); - if (internalNote) { - await putUpdateLog( - { - type: 'internalNote', - object: internalNote, - newData: JSON.stringify(doc), - description: `${internalNote.contentType} written at ${internalNote.createdDate} has been edited`, - }, - user, - ); + await putUpdateLog( + { + type: MODULE_NAMES.INTERNAL_NOTE, + object: internalNote, + newData: doc, + }, + user, + ); + + if (BOARD_TYPES.ALL.includes(updated.contentType)) { + graphqlPubsub.publish('activityLogsChanged', {}); } return updated; }, /** - * Remove a channel + * Removes an internal note */ - async internalNotesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const internalNote = await InternalNotes.findOne({ _id }); + async internalNotesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const internalNote = await InternalNotes.getInternalNote(_id); const removed = await InternalNotes.removeInternalNote(_id); - if (internalNote) { - await putDeleteLog( - { - type: 'internalNote', - object: internalNote, - description: `${internalNote.contentType} written at ${internalNote.createdDate} has been removed`, - }, - user, - ); + await putDeleteLog({ type: MODULE_NAMES.INTERNAL_NOTE, object: internalNote }, user); + + if (BOARD_TYPES.ALL.includes(internalNote.contentType)) { + graphqlPubsub.publish('activityLogsChanged', {}); } return removed; diff --git a/src/data/resolvers/mutations/knowledgeBase.ts b/src/data/resolvers/mutations/knowledgeBase.ts index 46bb270a8..8eaec9ad6 100644 --- a/src/data/resolvers/mutations/knowledgeBase.ts +++ b/src/data/resolvers/mutations/knowledgeBase.ts @@ -1,51 +1,46 @@ import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; - import { ITopic } from '../../../db/models/definitions/knowledgebase'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { IArticleCreate, ICategoryCreate } from '../../../db/models/KnowledgeBase'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; const knowledgeBaseMutations = { /** - * Create topic document + * Creates a topic document */ - async knowledgeBaseTopicsAdd(_root, { doc }: { doc: ITopic }, { user }: { user: IUserDocument }) { - const topic = await KnowledgeBaseTopics.createDoc(doc, user._id); - - if (topic) { - await putCreateLog( - { - type: 'knowledgeBaseTopic', - newData: JSON.stringify(doc), - object: topic, - description: `${topic.title} has been created`, - }, - user, - ); - } + async knowledgeBaseTopicsAdd(_root, { doc }: { doc: ITopic }, { user, docModifier }: IContext) { + const topic = await KnowledgeBaseTopics.createDoc(docModifier(doc), user._id); + + await putCreateLog( + { + type: MODULE_NAMES.KB_TOPIC, + newData: { ...doc, createdBy: user._id, createdDate: topic.createdDate }, + object: topic, + }, + user, + ); return topic; }, /** - * Update topic document + * Updates a topic document */ - async knowledgeBaseTopicsEdit(_root, { _id, doc }: { _id: string; doc: ITopic }, { user }: { user: IUserDocument }) { - const topic = await KnowledgeBaseTopics.findOne({ _id }); + async knowledgeBaseTopicsEdit(_root, { _id, doc }: { _id: string; doc: ITopic }, { user }: IContext) { + const topic = await KnowledgeBaseTopics.getTopic(_id); const updated = await KnowledgeBaseTopics.updateDoc(_id, doc, user._id); - if (topic) { - await putUpdateLog( - { - type: 'knowledgeBaseTopic', - object: topic, - newData: JSON.stringify(doc), - description: `${topic.title} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.KB_TOPIC, + object: topic, + newData: { ...doc, modifiedBy: user._id, modifiedDate: updated.modifiedDate }, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -53,20 +48,11 @@ const knowledgeBaseMutations = { /** * Remove topic document */ - async knowledgeBaseTopicsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const topic = await KnowledgeBaseTopics.findOne({ _id }); + async knowledgeBaseTopicsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const topic = await KnowledgeBaseTopics.getTopic(_id); const removed = await KnowledgeBaseTopics.removeDoc(_id); - if (topic) { - await putDeleteLog( - { - type: 'knowledgeBaseTopic', - object: topic, - description: `${topic.title} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.KB_TOPIC, object: topic }, user); return removed; }, @@ -74,14 +60,13 @@ const knowledgeBaseMutations = { /** * Create category document */ - async knowledgeBaseCategoriesAdd(_root, { doc }: { doc: ICategoryCreate }, { user }: { user: IUserDocument }) { + async knowledgeBaseCategoriesAdd(_root, { doc }: { doc: ICategoryCreate }, { user }: IContext) { const kbCategory = await KnowledgeBaseCategories.createDoc(doc, user._id); await putCreateLog( { - type: 'knowledgeBaseCategory', - newData: JSON.stringify(doc), - description: `${kbCategory.title} has been created`, + type: MODULE_NAMES.KB_CATEGORY, + newData: { ...doc, createdBy: user._id, createdDate: kbCategory.createdDate }, object: kbCategory, }, user, @@ -93,25 +78,19 @@ const knowledgeBaseMutations = { /** * Update category document */ - async knowledgeBaseCategoriesEdit( - _root, - { _id, doc }: { _id: string; doc: ICategoryCreate }, - { user }: { user: IUserDocument }, - ) { - const kbCategory = await KnowledgeBaseCategories.findOne({ _id }); + async knowledgeBaseCategoriesEdit(_root, { _id, doc }: { _id: string; doc: ICategoryCreate }, { user }: IContext) { + const kbCategory = await KnowledgeBaseCategories.getCategory(_id); const updated = await KnowledgeBaseCategories.updateDoc(_id, doc, user._id); - if (kbCategory) { - await putUpdateLog( - { - type: 'knowledgeBaseCategory', - object: kbCategory, - newData: JSON.stringify(doc), - description: `${kbCategory.title} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.KB_CATEGORY, + object: kbCategory, + newData: { ...doc, modifiedBy: user._id, modifiedDate: updated.modifiedDate }, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -119,20 +98,12 @@ const knowledgeBaseMutations = { /** * Remove category document */ - async knowledgeBaseCategoriesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const kbCategory = await KnowledgeBaseCategories.findOne({ _id }); + async knowledgeBaseCategoriesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const kbCategory = await KnowledgeBaseCategories.getCategory(_id); + const removed = await KnowledgeBaseCategories.removeDoc(_id); - if (kbCategory) { - await putDeleteLog( - { - type: 'knowledgeBaseCategory', - object: kbCategory, - description: `${kbCategory.title} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.KB_CATEGORY, object: kbCategory }, user); return removed; }, @@ -140,14 +111,13 @@ const knowledgeBaseMutations = { /** * Create article document */ - async knowledgeBaseArticlesAdd(_root, { doc }: { doc: IArticleCreate }, { user }: { user: IUserDocument }) { + async knowledgeBaseArticlesAdd(_root, { doc }: { doc: IArticleCreate }, { user }: IContext) { const kbArticle = await KnowledgeBaseArticles.createDoc(doc, user._id); await putCreateLog( { - type: 'knowledgeBaseArticle', - newData: JSON.stringify(doc), - description: `${kbArticle.title} has been created`, + type: MODULE_NAMES.KB_ARTICLE, + newData: { ...doc, createdBy: user._id, createdDate: kbArticle.createdDate }, object: kbArticle, }, user, @@ -159,25 +129,19 @@ const knowledgeBaseMutations = { /** * Update article document */ - async knowledgeBaseArticlesEdit( - _root, - { _id, doc }: { _id: string; doc: IArticleCreate }, - { user }: { user: IUserDocument }, - ) { - const kbArticle = await KnowledgeBaseArticles.findOne({ _id }); + async knowledgeBaseArticlesEdit(_root, { _id, doc }: { _id: string; doc: IArticleCreate }, { user }: IContext) { + const kbArticle = await KnowledgeBaseArticles.getArticle(_id); const updated = await KnowledgeBaseArticles.updateDoc(_id, doc, user._id); - if (kbArticle) { - await putUpdateLog( - { - type: 'knowledgeBaseArticle', - object: kbArticle, - newData: JSON.stringify(doc), - description: `${kbArticle.title} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.KB_ARTICLE, + object: kbArticle, + newData: { ...doc, modifiedBy: user._id, modifiedDate: updated.modifiedDate }, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -185,20 +149,11 @@ const knowledgeBaseMutations = { /** * Remove article document */ - async knowledgeBaseArticlesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const kbArticle = await KnowledgeBaseArticles.findOne({ _id }); + async knowledgeBaseArticlesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const kbArticle = await KnowledgeBaseArticles.getArticle(_id); const removed = await KnowledgeBaseArticles.removeDoc(_id); - if (kbArticle) { - await putDeleteLog( - { - type: 'knowledgeBaseArticle', - object: kbArticle, - description: `${kbArticle.title} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.KB_ARTICLE, object: kbArticle }, user); return removed; }, diff --git a/src/data/resolvers/mutations/messengerApps.ts b/src/data/resolvers/mutations/messengerApps.ts index 964e641eb..2eba20a47 100644 --- a/src/data/resolvers/mutations/messengerApps.ts +++ b/src/data/resolvers/mutations/messengerApps.ts @@ -1,104 +1,63 @@ -import { Forms, MessengerApps } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MessengerApps } from '../../../db/models'; import { requireLogin } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog } from '../../utils'; +import { IContext } from '../../types'; const messengerAppMutations = { - /** - * Creates a messenger app knowledgebase - * @param {string} params.name Name - * @param {string} params.integrationId Integration - * @param {string} params.topicId Topic - */ - async messengerAppsAddKnowledgebase(_root, params, { user }: { user: IUserDocument }) { - const { name, integrationId, topicId } = params; - const kb = await MessengerApps.createApp({ - name, - kind: 'knowledgebase', - showInInbox: false, - credentials: { - integrationId, - topicId, - }, - }); - - if (kb) { - await putCreateLog( - { - type: 'messengerAppKb', - newData: JSON.stringify(params), - object: kb, - description: `${name} has been created`, - }, - user, - ); - } - - return kb; - }, - - /** - * Creates a messenger app lead - * @param {string} params.name Name - * @param {string} params.integrationId Integration - * @param {string} params.formId Form - */ - async messengerAppsAddLead(_root, params, { user }: { user: IUserDocument }) { - const { name, integrationId, formId } = params; - const form = await Forms.findOne({ _id: formId }); - - if (!form) { - throw new Error('Form not found'); + async messengerAppSave( + _root, + { integrationId, messengerApps }: { integrationId: string; messengerApps: any }, + { docModifier }: IContext, + ) { + await MessengerApps.deleteMany({ 'credentials.integrationId': integrationId }); + + if (messengerApps.websites) { + for (const website of messengerApps.websites) { + const doc = { + kind: 'website', + credentials: { + integrationId, + description: website.description, + buttonText: website.buttonText, + url: website.url, + }, + }; + + await MessengerApps.createApp(docModifier(doc)); + } } - const lead = await MessengerApps.createApp({ - name, - kind: 'lead', - showInInbox: false, - credentials: { - integrationId, - formCode: form.code || '', - }, - }); - - if (lead) { - await putCreateLog( - { - type: 'messengerAppLead', - newData: JSON.stringify(params), - object: lead, - description: `${name} has been created`, - }, - user, - ); + if (messengerApps.knowledgebases) { + for (const knowledgebase of messengerApps.knowledgebases) { + const doc = { + kind: 'knowledgebase', + credentials: { + integrationId, + topicId: knowledgebase.topicId, + }, + }; + + await MessengerApps.createApp(docModifier(doc)); + } } - return lead; - }, - - /* - * Remove app - */ - async messengerAppsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const messengerApp = await MessengerApps.findOne({ _id }); - const removed = await MessengerApps.deleteOne({ _id }); - - if (messengerApp) { - await putDeleteLog( - { - type: 'messengerApp', - object: messengerApp, - description: `${messengerApp.name} has been removed`, - }, - user, - ); + if (messengerApps.leads) { + for (const lead of messengerApps.leads) { + const doc = { + kind: 'lead', + credentials: { + integrationId, + formCode: lead.formCode, + }, + }; + + await MessengerApps.createApp(docModifier(doc)); + } } - return removed; + return 'success'; }, }; -requireLogin(messengerAppMutations, 'messengerAppsAddKnowledgebase'); -requireLogin(messengerAppMutations, 'messengerAppsAddLead'); +requireLogin(messengerAppMutations, 'messengerAppSave'); export default messengerAppMutations; diff --git a/src/data/resolvers/mutations/notifications.ts b/src/data/resolvers/mutations/notifications.ts index d145e0b46..28e7b9ded 100644 --- a/src/data/resolvers/mutations/notifications.ts +++ b/src/data/resolvers/mutations/notifications.ts @@ -1,25 +1,37 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; import { IConfig } from '../../../db/models/definitions/notifications'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { graphqlPubsub } from '../../../pubsub'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const notificationMutations = { /** * Save notification configuration */ - notificationsSaveConfig(_root, doc: IConfig, { user }: { user: IUserDocument }) { + notificationsSaveConfig(_root, doc: IConfig, { user }: IContext) { return NotificationConfigurations.createOrUpdateConfiguration(doc, user); }, /** * Marks notification as read */ - notificationsMarkAsRead(_root, { _ids }: { _ids: string[] }, { user }: { user: IUserDocument }) { + async notificationsMarkAsRead( + _root, + { _ids, contentTypeId }: { _ids: string[]; contentTypeId: string }, + { user }: IContext, + ) { // notify subscription graphqlPubsub.publish('notificationsChanged', ''); - return Notifications.markAsRead(_ids, user._id); + let notificationIds = _ids; + + if (contentTypeId) { + const notifications = await Notifications.find({ contentTypeId }); + + notificationIds = notifications.map(notification => notification._id); + } + + return Notifications.markAsRead(notificationIds, user._id); }, }; diff --git a/src/data/resolvers/mutations/permissions.ts b/src/data/resolvers/mutations/permissions.ts index 78df0a1a3..4c453ab4b 100644 --- a/src/data/resolvers/mutations/permissions.ts +++ b/src/data/resolvers/mutations/permissions.ts @@ -1,9 +1,68 @@ +import * as _ from 'underscore'; import { Permissions, Users, UsersGroups } from '../../../db/models'; -import { IPermissionParams, IUserGroup } from '../../../db/models/definitions/permissions'; +import { IPermissionParams, IUserGroup, IUserGroupDocument } from '../../../db/models/definitions/permissions'; import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { resetPermissionsCache } from '../../permissions/utils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; + +interface IParams { + memberIds?: string[]; + oldUsers: IUserDocument[]; + group: IUserGroupDocument; + currentUser: IUserDocument; +} + +const writeUserLog = async (params: IParams) => { + const { memberIds = [], oldUsers = [], group, currentUser } = params; + + for (const oldUser of oldUsers) { + const exists = memberIds.find(id => id === oldUser._id); + + if (!exists) { + const groupIds = oldUser.groupIds ? oldUser.groupIds.filter(item => item !== group._id) : []; + // user has been removed from the group + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: { _id: oldUser._id, groupIds: oldUser.groupIds }, + newData: { groupIds }, + description: `User "${oldUser.email}" has been removed from group "${group.name}"`, + updatedDocument: { groupIds }, + }, + currentUser, + ); + } + } // end oldUser loop + + for (const memberId of memberIds) { + const exists = oldUsers.find(usr => usr._id === memberId); + + // user has been added to the group + if (!exists) { + // already updated user row + const addedUser = await Users.findOne({ _id: memberId }); + + if (addedUser) { + // previous data was like this + const groupIds = (addedUser.groupIds || []).filter(groupId => groupId !== group._id); + + await putUpdateLog( + { + type: MODULE_NAMES.USER, + object: { _id: memberId, groupIds }, + newData: { groupIds: addedUser.groupIds }, + description: `User "${addedUser.email}" has been added to group ${group.name}`, + updatedDocument: { groupIds: addedUser.groupIds }, + }, + currentUser, + ); + } + } + } // end new user loop +}; const permissionMutations = { /** @@ -14,42 +73,21 @@ const permissionMutations = { * @param {Boolean} doc.allowed * @return {Promise} newly created permission object */ - async permissionsAdd(_root, doc: IPermissionParams, { user }: { user: IUserDocument }) { + async permissionsAdd(_root, doc: IPermissionParams, { user }: IContext) { const result = await Permissions.createPermission(doc); - if (result && result.length > 0) { - for (const perm of result) { - let description = `Permission of module "${perm.module}", action "${perm.action}" assigned to `; - - if (perm.groupId) { - const group = await UsersGroups.findOne({ _id: perm.groupId }); - - if (group && group.name) { - description = `${description} user group "${group.name}" `; - } - } - - if (perm.userId) { - const permUser = await Users.findOne({ _id: perm.userId }); - - if (permUser) { - description = `${description} user "${permUser.email}" has been created`; - } - } - - await putCreateLog( - { - type: 'permission', - object: perm, - newData: JSON.stringify(perm), - description, - }, - user, - ); - } // end for loop - } // end result checking + for (const perm of result) { + await putCreateLog( + { + type: MODULE_NAMES.PERMISSION, + object: perm, + newData: perm, + }, + user, + ); + } - resetPermissionsCache(); + await resetPermissionsCache(); return result; }, @@ -59,42 +97,15 @@ const permissionMutations = { * @param {[String]} ids * @return {Promise} */ - async permissionsRemove(_root, { ids }: { ids: string[] }, { user }: { user: IUserDocument }) { + async permissionsRemove(_root, { ids }: { ids: string[] }, { user }: IContext) { const permissions = await Permissions.find({ _id: { $in: ids } }); const result = await Permissions.removePermission(ids); for (const perm of permissions) { - let description = `Permission of module "${perm.module}", action "${perm.action}" assigned to `; - - // prepare user group related description - if (perm.groupId) { - const group = await UsersGroups.findOne({ _id: perm.groupId }); - - if (group && group.name) { - description = `${description} user group "${group.name}" has been removed`; - } - } - - // prepare user related description - if (perm.userId) { - const permUser = await Users.findOne({ _id: perm.userId }); - - if (permUser && permUser.email) { - description = `${description} user "${permUser.email}" has been removed`; - } - } - - await putDeleteLog( - { - type: 'permission', - object: perm, - description, - }, - user, - ); - } // end for loop + await putDeleteLog({ type: MODULE_NAMES.PERMISSION, object: perm }, user); + } - resetPermissionsCache(); + await resetPermissionsCache(); return result; }, @@ -107,28 +118,40 @@ const usersGroupMutations = { * @param {String} doc.description * @return {Promise} newly created group object */ - async usersGroupsAdd( - _root, - { memberIds, ...doc }: IUserGroup & { memberIds?: string[] }, - { user }: { user: IUserDocument }, - ) { - const result = await UsersGroups.createGroup(doc, memberIds); + async usersGroupsAdd(_root, { memberIds, ...doc }: IUserGroup & { memberIds?: string[] }, { user }: IContext) { + // users before updating + const oldUsers = await Users.find({ _id: { $in: memberIds || [] } }); - if (result) { - await putCreateLog( + const group = await UsersGroups.createGroup(doc, memberIds); + + await putCreateLog( + { + type: MODULE_NAMES.USER_GROUP, + object: group, + newData: doc, + description: `"${group.name}" has been created`, + }, + user, + ); + + for (const oldUser of oldUsers) { + const updatedDocument = { groupIds: [...(oldUser.groupIds || []), group._id] }; + + await putUpdateLog( { - type: 'userGroup', - object: result, - newData: JSON.stringify(doc), - description: `${result.name} has been created`, + type: MODULE_NAMES.USER, + object: oldUser, + newData: updatedDocument, + description: `User "${oldUser.email}" has been added to group ${group.name}`, + updatedDocument, }, user, ); } - resetPermissionsCache(); + await resetPermissionsCache(); - return result; + return group; }, /** @@ -140,24 +163,33 @@ const usersGroupMutations = { async usersGroupsEdit( _root, { _id, memberIds, ...doc }: { _id: string; memberIds?: string[] } & IUserGroup, - { user }: { user: IUserDocument }, + { user }: IContext, ) { - const group = await UsersGroups.findOne({ _id }); + const group = await UsersGroups.getGroup(_id); + const oldUsers = await Users.find({ groupIds: { $in: [_id] } }); const result = await UsersGroups.updateGroup(_id, doc, memberIds); - if (group) { + // don't write unnecessary log when nothing is changed + if (group.name !== doc.name) { await putUpdateLog( { - type: 'userGroup', + type: MODULE_NAMES.USER_GROUP, object: group, - newData: JSON.stringify(doc), - description: `${group.name} has been edited`, + newData: doc, + description: `"${group.name}" has been edited`, }, user, ); } - resetPermissionsCache(); + await writeUserLog({ + currentUser: user, + memberIds, + oldUsers, + group, + }); + + await resetPermissionsCache(); return result; }, @@ -167,25 +199,57 @@ const usersGroupMutations = { * @param {String} _id * @return {Promise} */ - async usersGroupsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const group = await UsersGroups.findOne({ _id }); + async usersGroupsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const group = await UsersGroups.getGroup(_id); + const members = await Users.find({ groupIds: { $in: [group._id] } }); const result = await UsersGroups.removeGroup(_id); - if (group && result) { - await putDeleteLog( + await putDeleteLog( + { + type: MODULE_NAMES.USER_GROUP, + object: group, + description: `"${group.name}" has been removed`, + }, + user, + ); + + for (const member of members) { + const groupIds = member.groupIds ? member.groupIds.filter(id => id !== group._id) : []; + + await putUpdateLog( { - type: 'userGroup', - object: group, - description: `${group.name} has been removed`, + type: MODULE_NAMES.USER, + object: { _id: member._id, groupIds: member.groupIds }, + newData: { groupIds }, + updatedDocument: { groupIds }, + description: `User ${member.email} has been removed from group ${group.name}`, }, user, ); } - resetPermissionsCache(); + await resetPermissionsCache(); return result; }, + + async usersGroupsCopy(_root, { _id, memberIds }: { _id: string; memberIds: string[] }, { user }: IContext) { + const group = await UsersGroups.getGroup(_id); + + const clone = await UsersGroups.copyGroup(group._id, memberIds); + + await putCreateLog( + { + type: 'userGroup', + object: clone, + newData: { name: clone.name, description: clone.description }, + description: `"${group.name}" has been copied`, + }, + user, + ); + + return clone; + }, }; moduleCheckPermission(permissionMutations, 'managePermissions'); diff --git a/src/data/resolvers/mutations/pipelineLabels.ts b/src/data/resolvers/mutations/pipelineLabels.ts new file mode 100644 index 000000000..96722cad0 --- /dev/null +++ b/src/data/resolvers/mutations/pipelineLabels.ts @@ -0,0 +1,72 @@ +import { PipelineLabels } from '../../../db/models'; +import { IPipelineLabel } from '../../../db/models/definitions/pipelineLabels'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { IContext } from '../../types'; + +interface IPipelineLabelsEdit extends IPipelineLabel { + _id: string; +} + +const pipelineLabelMutations = { + /** + * Creates a new pipeline label + */ + async pipelineLabelsAdd(_root, { ...doc }: IPipelineLabel, { user }: IContext) { + const pipelineLabel = await PipelineLabels.createPipelineLabel({ createdBy: user._id, ...doc }); + + await putCreateLog( + { + type: MODULE_NAMES.PIPELINE_LABEL, + newData: { ...doc, createdBy: user._id, createdAt: pipelineLabel.createdAt }, + object: pipelineLabel, + }, + user, + ); + + return pipelineLabel; + }, + + /** + * Edit pipeline label + */ + async pipelineLabelsEdit(_root, { _id, ...doc }: IPipelineLabelsEdit, { user }: IContext) { + const pipelineLabel = await PipelineLabels.getPipelineLabel(_id); + const updated = await PipelineLabels.updatePipelineLabel(_id, doc); + + await putUpdateLog( + { + type: MODULE_NAMES.PIPELINE_LABEL, + newData: doc, + object: pipelineLabel, + }, + user, + ); + + return updated; + }, + + /** + * Remove pipeline label + */ + async pipelineLabelsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const pipelineLabel = await PipelineLabels.getPipelineLabel(_id); + const removed = await PipelineLabels.removePipelineLabel(_id); + + await putDeleteLog({ type: MODULE_NAMES.PIPELINE_LABEL, object: pipelineLabel }, user); + + return removed; + }, + + /** + * Attach a label + */ + pipelineLabelsLabel( + _root, + { pipelineId, targetId, labelIds }: { pipelineId: string; targetId: string; labelIds: string[] }, + ) { + return PipelineLabels.labelsLabel(pipelineId, targetId, labelIds); + }, +}; + +export default pipelineLabelMutations; diff --git a/src/data/resolvers/mutations/pipelineTemplates.ts b/src/data/resolvers/mutations/pipelineTemplates.ts new file mode 100644 index 000000000..7a0c2a7d2 --- /dev/null +++ b/src/data/resolvers/mutations/pipelineTemplates.ts @@ -0,0 +1,90 @@ +import * as _ from 'underscore'; +import { PipelineTemplates } from '../../../db/models'; +import { IPipelineTemplate, IPipelineTemplateStage } from '../../../db/models/definitions/pipelineTemplates'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { IContext } from '../../types'; +import { registerOnboardHistory } from '../../utils'; +import { checkPermission } from '../boardUtils'; + +interface IPipelineTemplatesEdit extends IPipelineTemplate { + _id: string; + stages: IPipelineTemplateStage[]; +} + +const pipelineTemplateMutations = { + /** + * Create new pipeline template + */ + async pipelineTemplatesAdd(_root, { stages, ...doc }: IPipelineTemplate, { user, docModifier }: IContext) { + await checkPermission(doc.type, user, 'templatesAdd'); + + const pipelineTemplate = await PipelineTemplates.createPipelineTemplate( + docModifier({ createdBy: user._id, ...doc }), + stages, + ); + + await putCreateLog( + { + type: MODULE_NAMES.PIPELINE_TEMPLATE, + newData: { ...doc, stages: pipelineTemplate.stages }, + object: pipelineTemplate, + }, + user, + ); + + return pipelineTemplate; + }, + + /** + * Edit pipeline template + */ + async pipelineTemplatesEdit(_root, { _id, stages, ...doc }: IPipelineTemplatesEdit, { user }: IContext) { + await checkPermission(doc.type, user, 'templatesEdit'); + + const pipelineTemplate = await PipelineTemplates.getPipelineTemplate(_id); + const updated = await PipelineTemplates.updatePipelineTemplate(_id, doc, stages); + + await putUpdateLog( + { + type: MODULE_NAMES.PIPELINE_TEMPLATE, + newData: { ...doc, stages: updated.stages }, + object: pipelineTemplate, + updatedDocument: updated, + }, + user, + ); + + return updated; + }, + + /** + * Duplicate pipeline template + */ + async pipelineTemplatesDuplicate(_root, { _id }: { _id: string }, { user }: IContext) { + const pipelineTemplate = await PipelineTemplates.getPipelineTemplate(_id); + + await checkPermission(pipelineTemplate.type, user, 'templatesDuplicate'); + + await registerOnboardHistory({ type: `${pipelineTemplate.type}TemplatesDuplicate`, user }); + + return PipelineTemplates.duplicatePipelineTemplate(_id); + }, + + /** + * Remove pipeline template + */ + async pipelineTemplatesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const pipelineTemplate = await PipelineTemplates.getPipelineTemplate(_id); + + await checkPermission(pipelineTemplate.type, user, 'templatesRemove'); + + const removed = await PipelineTemplates.removePipelineTemplate(_id); + + await putDeleteLog({ type: MODULE_NAMES.PIPELINE_TEMPLATE, object: pipelineTemplate }, user); + + return removed; + }, +}; + +export default pipelineTemplateMutations; diff --git a/src/data/resolvers/mutations/products.ts b/src/data/resolvers/mutations/products.ts index 94167d3d0..e05c48f48 100644 --- a/src/data/resolvers/mutations/products.ts +++ b/src/data/resolvers/mutations/products.ts @@ -1,32 +1,38 @@ -import { Products } from '../../../db/models'; -import { IProduct } from '../../../db/models/definitions/deals'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { ProductCategories, Products } from '../../../db/models'; +import { IProduct, IProductCategory, IProductDocument } from '../../../db/models/definitions/deals'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface IProductsEdit extends IProduct { _id: string; } +interface IProductCategoriesEdit extends IProductCategory { + _id: string; +} + const productMutations = { /** * Creates a new product * @param {Object} doc Product document */ - async productsAdd(_root, doc: IProduct, { user }: { user: IUserDocument }) { - const product = await Products.createProduct(doc); - - if (product) { - await putCreateLog( - { - type: 'product', - newData: JSON.stringify(doc), - object: product, - description: `${product.name} has been created`, + async productsAdd(_root, doc: IProduct, { user, docModifier }: IContext) { + const product = await Products.createProduct(docModifier(doc)); + + await putCreateLog( + { + type: MODULE_NAMES.PRODUCT, + newData: { + ...doc, + categoryId: product.categoryId, + customFieldsData: product.customFieldsData, }, - user, - ); - } + object: product, + }, + user, + ); return product; }, @@ -36,21 +42,19 @@ const productMutations = { * @param {string} param2._id Product id * @param {Object} param2.doc Product info */ - async productsEdit(_root, { _id, ...doc }: IProductsEdit, { user }: { user: IUserDocument }) { - const product = await Products.findOne({ _id }); + async productsEdit(_root, { _id, ...doc }: IProductsEdit, { user }: IContext) { + const product = await Products.getProduct({ _id }); const updated = await Products.updateProduct(_id, doc); - if (product) { - await putUpdateLog( - { - type: 'product', - object: product, - newData: JSON.stringify(doc), - description: `${product.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.PRODUCT, + object: product, + newData: { ...doc, customFieldsData: updated.customFieldsData }, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -59,21 +63,69 @@ const productMutations = { * Removes a product * @param {string} param1._id Product id */ - async productsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const product = await Products.findOne({ _id }); - const removed = await Products.removeProduct(_id); - - if (product) { - await putDeleteLog( - { - type: 'product', - object: product, - description: `${product.name} has been removed`, - }, - user, - ); + async productsRemove(_root, { productIds }: { productIds: string[] }, { user }: IContext) { + const products: IProductDocument[] = await Products.find({ _id: { $in: productIds } }).lean(); + + await Products.removeProducts(productIds); + + for (const product of products) { + await putDeleteLog({ type: MODULE_NAMES.PRODUCT, object: product }, user); } + return productIds; + }, + + /** + * Creates a new product category + * @param {Object} doc Product category document + */ + async productCategoriesAdd(_root, doc: IProductCategory, { user, docModifier }: IContext) { + const productCategory = await ProductCategories.createProductCategory(docModifier(doc)); + + await putCreateLog( + { + type: MODULE_NAMES.PRODUCT_CATEGORY, + newData: { ...doc, order: productCategory.order }, + object: productCategory, + }, + user, + ); + + return productCategory; + }, + + /** + * Edits a product category + * @param {string} param2._id ProductCategory id + * @param {Object} param2.doc ProductCategory info + */ + async productCategoriesEdit(_root, { _id, ...doc }: IProductCategoriesEdit, { user }: IContext) { + const productCategory = await ProductCategories.getProductCatogery({ _id }); + const updated = await ProductCategories.updateProductCategory(_id, doc); + + await putUpdateLog( + { + type: MODULE_NAMES.PRODUCT_CATEGORY, + object: productCategory, + newData: doc, + updatedDocument: updated, + }, + user, + ); + + return updated; + }, + + /** + * Removes a product category + * @param {string} param1._id ProductCategory id + */ + async productCategoriesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const productCategory = await ProductCategories.getProductCatogery({ _id }); + const removed = await ProductCategories.removeProductCategory(_id); + + await putDeleteLog({ type: MODULE_NAMES.PRODUCT_CATEGORY, object: productCategory }, user); + return removed; }, }; diff --git a/src/data/resolvers/mutations/responseTemplates.ts b/src/data/resolvers/mutations/responseTemplates.ts index cac7b02cd..31a7c659a 100644 --- a/src/data/resolvers/mutations/responseTemplates.ts +++ b/src/data/resolvers/mutations/responseTemplates.ts @@ -1,8 +1,9 @@ import { ResponseTemplates } from '../../../db/models'; import { IResponseTemplate } from '../../../db/models/definitions/responseTemplates'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface IResponseTemplatesEdit extends IResponseTemplate { _id: string; @@ -10,65 +11,51 @@ interface IResponseTemplatesEdit extends IResponseTemplate { const responseTemplateMutations = { /** - * Create new response template + * Creates a new response template */ - async responseTemplatesAdd(_root, doc: IResponseTemplate, { user }: { user: IUserDocument }) { - const template = await ResponseTemplates.create(doc); + async responseTemplatesAdd(_root, doc: IResponseTemplate, { user, docModifier }: IContext) { + const template = await ResponseTemplates.create(docModifier(doc)); - if (template) { - await putCreateLog( - { - type: 'responseTemplate', - newData: JSON.stringify(doc), - object: template, - description: `${template.name} has been created`, - }, - user, - ); - } + await putCreateLog( + { + type: MODULE_NAMES.RESPONSE_TEMPLATE, + newData: doc, + object: template, + }, + user, + ); return template; }, /** - * Update response template + * Updates a response template */ - async responseTemplatesEdit(_root, { _id, ...fields }: IResponseTemplatesEdit, { user }: { user: IUserDocument }) { - const template = await ResponseTemplates.findOne({ _id }); + async responseTemplatesEdit(_root, { _id, ...fields }: IResponseTemplatesEdit, { user }: IContext) { + const template = await ResponseTemplates.getResponseTemplate(_id); const updated = await ResponseTemplates.updateResponseTemplate(_id, fields); - if (template) { - await putUpdateLog( - { - type: 'responseTemplate', - object: template, - newData: JSON.stringify(fields), - description: `${template.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.RESPONSE_TEMPLATE, + object: template, + newData: fields, + updatedDocument: updated, + }, + user, + ); return updated; }, /** - * Delete response template + * Deletes a response template */ - async responseTemplatesRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const template = await ResponseTemplates.findOne({ _id }); + async responseTemplatesRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const template = await ResponseTemplates.getResponseTemplate(_id); const removed = await ResponseTemplates.removeResponseTemplate(_id); - if (template) { - await putDeleteLog( - { - type: 'responseTemplate', - object: template, - description: `${template.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.RESPONSE_TEMPLATE, object: template }, user); return removed; }, diff --git a/src/data/resolvers/mutations/robot.ts b/src/data/resolvers/mutations/robot.ts new file mode 100644 index 000000000..b7e0ca98f --- /dev/null +++ b/src/data/resolvers/mutations/robot.ts @@ -0,0 +1,34 @@ +import { OnboardingHistories, RobotEntries } from '../../../db/models/Robot'; +import { graphqlPubsub } from '../../../pubsub'; +import { IContext } from '../../types'; + +const robotMutations = { + robotEntriesMarkAsNotified(_root, { _id }: { _id: string }) { + return RobotEntries.markAsNotified(_id); + }, + + async onboardingCheckStatus(_root, _args, { user }: IContext) { + const status = await OnboardingHistories.userStatus(user._id); + + if (status !== 'completed') { + graphqlPubsub.publish('onboardingChanged', { + onboardingChanged: { + userId: user._id, + type: status, + }, + }); + } + + return status; + }, + + onboardingForceComplete(_root, _args, { user }: IContext) { + return OnboardingHistories.forceComplete(user._id); + }, + + onboardingCompleteShowStep(_root, { step }: { step: string }, { user }: IContext) { + return OnboardingHistories.completeShowStep(step, user._id); + }, +}; + +export default robotMutations; diff --git a/src/data/resolvers/mutations/scripts.ts b/src/data/resolvers/mutations/scripts.ts index c1f0a8b2b..8b3e17c3f 100644 --- a/src/data/resolvers/mutations/scripts.ts +++ b/src/data/resolvers/mutations/scripts.ts @@ -1,8 +1,9 @@ import { Scripts } from '../../../db/models'; import { IScript } from '../../../db/models/definitions/scripts'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface IScriptsEdit extends IScript { _id: string; @@ -10,65 +11,52 @@ interface IScriptsEdit extends IScript { const scriptMutations = { /** - * Create new script + * Creates a new script */ - async scriptsAdd(_root, doc: IScript, { user }: { user: IUserDocument }) { - const script = await Scripts.createScript(doc); + async scriptsAdd(_root, doc: IScript, { user, docModifier }: IContext) { + const modifiedDoc = docModifier(doc); + const script = await Scripts.createScript(modifiedDoc); - if (script) { - await putCreateLog( - { - type: 'script', - newData: JSON.stringify(doc), - object: script, - description: `${script.name} has been created`, - }, - user, - ); - } + await putCreateLog( + { + type: MODULE_NAMES.SCRIPT, + newData: modifiedDoc, + object: script, + }, + user, + ); return script; }, /** - * Update script + * Updates a script */ - async scriptsEdit(_root, { _id, ...fields }: IScriptsEdit, { user }: { user: IUserDocument }) { - const script = await Scripts.findOne({ _id }); + async scriptsEdit(_root, { _id, ...fields }: IScriptsEdit, { user }: IContext) { + const script = await Scripts.getScript(_id); const updated = await Scripts.updateScript(_id, fields); - if (script) { - await putUpdateLog( - { - type: 'script', - object: script, - newData: JSON.stringify(fields), - description: `${script.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.SCRIPT, + object: script, + newData: fields, + updatedDocument: updated, + }, + user, + ); return updated; }, /** - * Delete script + * Deletes a script */ - async scriptsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const script = await Scripts.findOne({ _id }); + async scriptsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const script = await Scripts.getScript(_id); const removed = await Scripts.removeScript(_id); - if (script) { - await putDeleteLog( - { - type: 'script', - object: script, - description: `${script.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.SCRIPT, object: script }, user); return removed; }, diff --git a/src/data/resolvers/mutations/segments.ts b/src/data/resolvers/mutations/segments.ts index 5adf23665..cf4f3791b 100644 --- a/src/data/resolvers/mutations/segments.ts +++ b/src/data/resolvers/mutations/segments.ts @@ -1,8 +1,9 @@ import { Segments } from '../../../db/models'; import { ISegment } from '../../../db/models/definitions/segments'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { moduleCheckPermission } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; interface ISegmentsEdit extends ISegment { _id: string; @@ -12,20 +13,18 @@ const segmentMutations = { /** * Create new segment */ - async segmentsAdd(_root, doc: ISegment, { user }: { user: IUserDocument }) { - const segment = await Segments.createSegment(doc); + async segmentsAdd(_root, doc: ISegment, { user, docModifier }: IContext) { + const extendedDoc = docModifier(doc); + const segment = await Segments.createSegment(extendedDoc); - if (segment) { - await putCreateLog( - { - type: 'segment', - newData: JSON.stringify(doc), - object: segment, - description: `${segment.name} has been created`, - }, - user, - ); - } + await putCreateLog( + { + type: MODULE_NAMES.SEGMENT, + newData: doc, + object: segment, + }, + user, + ); return segment; }, @@ -33,21 +32,19 @@ const segmentMutations = { /** * Update segment */ - async segmentsEdit(_root, { _id, ...doc }: ISegmentsEdit, { user }: { user: IUserDocument }) { - const segment = await Segments.findOne({ _id }); + async segmentsEdit(_root, { _id, ...doc }: ISegmentsEdit, { user }: IContext) { + const segment = await Segments.getSegment(_id); const updated = await Segments.updateSegment(_id, doc); - if (segment) { - await putUpdateLog( - { - type: 'segment', - object: segment, - newData: JSON.stringify(doc), - description: `${segment.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.SEGMENT, + object: segment, + newData: doc, + updatedDocument: updated, + }, + user, + ); return updated; }, @@ -55,20 +52,11 @@ const segmentMutations = { /** * Delete segment */ - async segmentsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const segment = await Segments.findOne({ _id }); + async segmentsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const segment = await Segments.getSegment(_id); const removed = await Segments.removeSegment(_id); - if (segment) { - await putDeleteLog( - { - type: 'segment', - object: segment, - description: `${segment.name} has been removed`, - }, - user, - ); - } + await putDeleteLog({ type: MODULE_NAMES.SEGMENT, object: segment }, user); return removed; }, diff --git a/src/data/resolvers/mutations/tags.ts b/src/data/resolvers/mutations/tags.ts index f84a824cc..8fa27b3ee 100644 --- a/src/data/resolvers/mutations/tags.ts +++ b/src/data/resolvers/mutations/tags.ts @@ -1,8 +1,9 @@ import { Tags } from '../../../db/models'; import { ITag } from '../../../db/models/definitions/tags'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { putCreateLog, putDeleteLog, putUpdateLog } from '../../utils'; +import { IContext } from '../../types'; import { publishConversationsChanged } from './conversations'; interface ITagsEdit extends ITag { @@ -11,61 +12,57 @@ interface ITagsEdit extends ITag { const tagMutations = { /** - * Create new tag + * Creates a new tag */ - async tagsAdd(_root, doc: ITag, { user }: { user: IUserDocument }) { - const tag = await Tags.createTag(doc); + async tagsAdd(_root, doc: ITag, { user, docModifier }: IContext) { + const tag = await Tags.createTag(docModifier(doc)); - if (tag) { - await putCreateLog( - { - type: 'tag', - newData: JSON.stringify(tag), - object: tag, - description: `${tag.name} has been created`, - }, - user, - ); - } + await putCreateLog( + { + type: MODULE_NAMES.TAG, + newData: tag, + object: tag, + description: `"${tag.name}" has been created`, + }, + user, + ); return tag; }, /** - * Edit tag + * Edits a tag */ - async tagsEdit(_root, { _id, ...doc }: ITagsEdit, { user }: { user: IUserDocument }) { - const tag = await Tags.findOne({ _id }); + async tagsEdit(_root, { _id, ...doc }: ITagsEdit, { user }: IContext) { + const tag = await Tags.getTag(_id); const updated = await Tags.updateTag(_id, doc); - if (tag) { - await putUpdateLog( - { - type: 'tag', - object: tag, - newData: JSON.stringify(doc), - description: `${tag.name} has been edited`, - }, - user, - ); - } + await putUpdateLog( + { + type: MODULE_NAMES.TAG, + object: tag, + newData: doc, + description: `"${tag.name}" has been edited`, + }, + user, + ); return updated; }, /** - * Remove tag + * Removes a tag */ - async tagsRemove(_root, { ids }: { ids: string[] }, { user }: { user: IUserDocument }) { + async tagsRemove(_root, { ids }: { ids: string[] }, { user }: IContext) { const tags = await Tags.find({ _id: { $in: ids } }); const removed = await Tags.removeTag(ids); for (const tag of tags) { await putDeleteLog( { - type: 'tag', + type: MODULE_NAMES.TAG, object: tag, - description: `${tag.name} has been removed`, + description: `"${tag.name}" has been removed`, }, user, ); @@ -79,7 +76,7 @@ const tagMutations = { */ tagsTag(_root, { type, targetIds, tagIds }: { type: string; targetIds: string[]; tagIds: string[] }) { if (type === 'conversation') { - publishConversationsChanged(targetIds, 'tag'); + publishConversationsChanged(targetIds, MODULE_NAMES.TAG); } return Tags.tagsTag(type, targetIds, tagIds); diff --git a/src/data/resolvers/mutations/tasks.ts b/src/data/resolvers/mutations/tasks.ts index a6e37385a..de23664e3 100644 --- a/src/data/resolvers/mutations/tasks.ts +++ b/src/data/resolvers/mutations/tasks.ts @@ -1,10 +1,9 @@ import { Tasks } from '../../../db/models'; -import { IOrderInput } from '../../../db/models/definitions/boards'; -import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; -import { ITask } from '../../../db/models/definitions/tasks'; -import { IUserDocument } from '../../../db/models/definitions/users'; +import { IItemCommonFields as ITask, IItemDragCommonFields } from '../../../db/models/definitions/boards'; import { checkPermission } from '../../permissions/wrappers'; -import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; +import { IContext } from '../../types'; +import { registerOnboardHistory } from '../../utils'; +import { itemsAdd, itemsArchive, itemsChange, itemsCopy, itemsEdit, itemsRemove } from './boardUtils'; interface ITasksEdit extends ITask { _id: string; @@ -12,105 +11,69 @@ interface ITasksEdit extends ITask { const taskMutations = { /** - * Create new task + * Creates a new task */ - async tasksAdd(_root, doc: ITask, { user }: { user: IUserDocument }) { - return Tasks.createTask({ - ...doc, - modifiedBy: user._id, - }); + async tasksAdd(_root, doc: ITask & { proccessId: string; aboveItemId: string }, { user, docModifier }: IContext) { + return itemsAdd(doc, 'deal', user, docModifier, Tasks.createTask); }, /** * Edit task */ - async tasksEdit(_root, { _id, ...doc }: ITasksEdit, { user }) { - const task = await Tasks.updateTask(_id, { - ...doc, - modifiedAt: new Date(), - modifiedBy: user._id, - }); + async tasksEdit(_root, { _id, proccessId, ...doc }: ITasksEdit & { proccessId: string }, { user }: IContext) { + const oldTask = await Tasks.getTask(_id); - await manageNotifications(Tasks, task, user, 'task'); + const updatedTask = await itemsEdit(_id, 'task', oldTask, doc, proccessId, user, Tasks.updateTask); - return task; - }, - - /** - * Change task - */ - async tasksChange( - _root, - { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, - ) { - const task = await Tasks.updateTask(_id, { - modifiedAt: new Date(), - modifiedBy: user._id, - stageId: destinationStageId, - }); - - const content = await itemsChange(Tasks, task, 'task', destinationStageId); - - await sendNotifications( - task.stageId || '', - user, - NOTIFICATION_TYPES.TASK_CHANGE, - await notifiedUserIds(task), - content, - 'task', - ); + if (updatedTask.assignedUserIds) { + await registerOnboardHistory({ type: 'taskAssignUser', user }); + } - return task; + return updatedTask; }, /** - * Update task orders (not sendNotifaction, ordered card to change) + * Change task */ - tasksUpdateOrder(_root, { stageId, orders }: { stageId: string; orders: IOrderInput[] }) { - return Tasks.updateOrder(stageId, orders); + async tasksChange(_root, doc: IItemDragCommonFields, { user }: IContext) { + return itemsChange(doc, 'task', user, Tasks.updateTask); }, /** * Remove task */ - async tasksRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const task = await Tasks.findOne({ _id }); - - if (!task) { - throw new Error('Task not found'); - } - - await sendNotifications( - task.stageId || '', - user, - NOTIFICATION_TYPES.TASK_DELETE, - await notifiedUserIds(task), - `'{userName}' deleted task: '${task.name}'`, - 'task', - ); - - return Tasks.removeTask(_id); + async tasksRemove(_root, { _id }: { _id: string }, { user }: IContext) { + return itemsRemove(_id, 'task', user); }, /** * Watch task */ - async tasksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { - const task = await Tasks.findOne({ _id }); + async tasksWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { + return Tasks.watchTask(_id, isAdd, user._id); + }, - if (!task) { - throw new Error('Task not found'); - } + async tasksCopy(_root, { _id, proccessId }: { _id: string; proccessId: string }, { user }: IContext) { + return itemsCopy(_id, proccessId, 'task', user, [], Tasks.createTask); + }, - return Tasks.watchTask(_id, isAdd, user._id); + async tasksArchive(_root, { stageId, proccessId }: { stageId: string; proccessId: string }, { user }: IContext) { + return itemsArchive(stageId, 'task', proccessId, user); + }, + + async taskUpdateTimeTracking( + _root, + { _id, status, timeSpent, startDate }: { _id: string; status: string; timeSpent: number; startDate: string }, + ) { + return Tasks.updateTimeTracking(_id, status, timeSpent, startDate); }, }; checkPermission(taskMutations, 'tasksAdd', 'tasksAdd'); checkPermission(taskMutations, 'tasksEdit', 'tasksEdit'); -checkPermission(taskMutations, 'tasksUpdateOrder', 'tasksUpdateOrder'); checkPermission(taskMutations, 'tasksRemove', 'tasksRemove'); checkPermission(taskMutations, 'tasksWatch', 'tasksWatch'); +checkPermission(taskMutations, 'tasksArchive', 'tasksArchive'); +checkPermission(taskMutations, 'taskUpdateTimeTracking', 'taskUpdateTimeTracking'); export default taskMutations; diff --git a/src/data/resolvers/mutations/tickets.ts b/src/data/resolvers/mutations/tickets.ts index 8e513f70a..596dcfe66 100644 --- a/src/data/resolvers/mutations/tickets.ts +++ b/src/data/resolvers/mutations/tickets.ts @@ -1,10 +1,9 @@ import { Tickets } from '../../../db/models'; -import { IOrderInput } from '../../../db/models/definitions/boards'; -import { NOTIFICATION_TYPES } from '../../../db/models/definitions/constants'; +import { IItemDragCommonFields } from '../../../db/models/definitions/boards'; import { ITicket } from '../../../db/models/definitions/tickets'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission } from '../../permissions/wrappers'; -import { itemsChange, manageNotifications, notifiedUserIds, sendNotifications } from '../boardUtils'; +import { IContext } from '../../types'; +import { itemsAdd, itemsArchive, itemsChange, itemsCopy, itemsEdit, itemsRemove } from './boardUtils'; interface ITicketsEdit extends ITicket { _id: string; @@ -14,103 +13,53 @@ const ticketMutations = { /** * Create new ticket */ - async ticketsAdd(_root, doc: ITicket, { user }: { user: IUserDocument }) { - return Tickets.createTicket({ - ...doc, - modifiedBy: user._id, - }); + async ticketsAdd(_root, doc: ITicket & { proccessId: string; aboveItemId: string }, { user, docModifier }: IContext) { + return itemsAdd(doc, 'ticket', user, docModifier, Tickets.createTicket); }, /** * Edit ticket */ - async ticketsEdit(_root, { _id, ...doc }: ITicketsEdit, { user }) { - const ticket = await Tickets.updateTicket(_id, { - ...doc, - modifiedAt: new Date(), - modifiedBy: user._id, - }); + async ticketsEdit(_root, { _id, proccessId, ...doc }: ITicketsEdit & { proccessId: string }, { user }: IContext) { + const oldTicket = await Tickets.getTicket(_id); - await manageNotifications(Tickets, ticket, user, 'ticket'); - - return ticket; + return itemsEdit(_id, 'ticket', oldTicket, doc, proccessId, user, Tickets.updateTicket); }, /** * Change ticket */ - async ticketsChange( - _root, - { _id, destinationStageId }: { _id: string; destinationStageId: string }, - { user }: { user: IUserDocument }, - ) { - const ticket = await Tickets.updateTicket(_id, { - modifiedAt: new Date(), - modifiedBy: user._id, - stageId: destinationStageId, - }); - - const content = await itemsChange(Tickets, ticket, 'ticket', destinationStageId); - - await sendNotifications( - ticket.stageId || '', - user, - NOTIFICATION_TYPES.TICKET_CHANGE, - await notifiedUserIds(ticket), - content, - 'ticket', - ); - - return ticket; - }, - - /** - * Update ticket orders (not sendNotifaction, ordered card to change) - */ - ticketsUpdateOrder(_root, { stageId, orders }: { stageId: string; orders: IOrderInput[] }) { - return Tickets.updateOrder(stageId, orders); + async ticketsChange(_root, doc: IItemDragCommonFields, { user }: IContext) { + return itemsChange(doc, 'ticket', user, Tickets.updateTicket); }, /** * Remove ticket */ - async ticketsRemove(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { - const ticket = await Tickets.findOne({ _id }); - - if (!ticket) { - throw new Error('ticket not found'); - } - - await sendNotifications( - ticket.stageId || '', - user, - NOTIFICATION_TYPES.TICKET_DELETE, - await notifiedUserIds(ticket), - `'{userName}' deleted ticket: '${ticket.name}'`, - 'ticket', - ); - - return Tickets.removeTicket(_id); + async ticketsRemove(_root, { _id }: { _id: string }, { user }: IContext) { + return itemsRemove(_id, 'ticket', user); }, /** * Watch ticket */ - async ticketsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: { user: IUserDocument }) { - const ticket = await Tickets.findOne({ _id }); + async ticketsWatch(_root, { _id, isAdd }: { _id: string; isAdd: boolean }, { user }: IContext) { + return Tickets.watchTicket(_id, isAdd, user._id); + }, - if (!ticket) { - throw new Error('Ticket not found'); - } + async ticketsCopy(_root, { _id, proccessId }: { _id: string; proccessId: string }, { user }: IContext) { + return itemsCopy(_id, proccessId, 'ticket', user, ['source'], Tickets.createTicket); + }, - return Tickets.watchTicket(_id, isAdd, user._id); + async ticketsArchive(_root, { stageId, proccessId }: { stageId: string; proccessId: string }, { user }: IContext) { + return itemsArchive(stageId, 'ticket', proccessId, user); }, }; checkPermission(ticketMutations, 'ticketsAdd', 'ticketsAdd'); checkPermission(ticketMutations, 'ticketsEdit', 'ticketsEdit'); -checkPermission(ticketMutations, 'ticketsUpdateOrder', 'ticketsUpdateOrder'); checkPermission(ticketMutations, 'ticketsRemove', 'ticketsRemove'); checkPermission(ticketMutations, 'ticketsWatch', 'ticketsWatch'); +checkPermission(ticketMutations, 'ticketsArchive', 'ticketsArchive'); export default ticketMutations; diff --git a/src/data/resolvers/mutations/users.ts b/src/data/resolvers/mutations/users.ts index 75b130389..adb0e537e 100644 --- a/src/data/resolvers/mutations/users.ts +++ b/src/data/resolvers/mutations/users.ts @@ -1,14 +1,25 @@ -import { Channels, Users } from '../../../db/models'; -import { IDetail, IEmailSignature, ILink, IUser, IUserDocument } from '../../../db/models/definitions/users'; +import * as telemetry from 'erxes-telemetry'; +import * as express from 'express'; +import { Channels, Configs, Users } from '../../../db/models'; +import { ILink } from '../../../db/models/definitions/common'; +import { IDetail, IEmailSignature, IUser } from '../../../db/models/definitions/users'; +import messageBroker from '../../../messageBroker'; +import { resetPermissionsCache } from '../../permissions/utils'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import utils, { authCookieOptions, getEnv } from '../../utils'; +import { IContext } from '../../types'; +import utils, { authCookieOptions, getEnv, sendRequest } from '../../utils'; interface IUsersEdit extends IUser { channelIds?: string[]; - groupIds?: string[]; _id: string; } +interface ILogin { + email: string; + password: string; + deviceToken?: string; +} + const sendInvitationEmail = ({ email, token }: { email: string; token: string }) => { const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN' }); const confirmationUrl = `${MAIN_APP_DOMAIN}/confirmation?token=${token}`; @@ -22,23 +33,78 @@ const sendInvitationEmail = ({ email, token }: { email: string; token: string }) content: confirmationUrl, domain: MAIN_APP_DOMAIN, }, - isCustom: true, }, }); }; +const login = async (args: ILogin, res: express.Response, secure: boolean) => { + const response = await Users.login(args); + + const { token } = response; + + res.cookie('auth-token', token, authCookieOptions(secure)); + + telemetry.trackCli('logged_in'); + + return 'loggedIn'; +}; + const userMutations = { - /* - * Login - */ - async login(_root, args: { email: string; password: string; deviceToken?: string }, { res }) { - const response = await Users.login(args); + async usersCreateOwner( + _root, + { + email, + password, + firstName, + lastName, + subscribeEmail, + }: { email: string; password: string; firstName: string; lastName?: string; subscribeEmail?: boolean }, + ) { + const userCount = await Users.countDocuments(); - const { token } = response; + if (userCount > 0) { + throw new Error('Access denied'); + } + + const doc: IUser = { + isOwner: true, + email, + password, + details: { + fullName: `${firstName} ${lastName || ''}`, + }, + }; + + const user = await Users.createUser(doc); + + if (subscribeEmail && process.env.NODE_ENV === 'production') { + await sendRequest({ + url: 'https://erxes.io/subscribe', + method: 'POST', + body: { + email, + firstName, + lastName, + }, + }); + } - res.cookie('auth-token', token, authCookieOptions()); + await Configs.createOrUpdateConfig({ code: 'UPLOAD_SERVICE_TYPE', value: 'local' }); - return 'loggedIn'; + await messageBroker().sendMessage('erxes-api:integrations-notification', { + type: 'addUserId', + payload: { + _id: user._id, + }, + }); + + return 'success'; + }, + /* + * Login + */ + async login(_root, args: ILogin, { res, requestInfo }: IContext) { + return login(args, res, requestInfo.secure); }, async logout(_root, _args, { res }) { @@ -57,7 +123,7 @@ const userMutations = { const link = `${MAIN_APP_DOMAIN}/reset-password?token=${token}`; - utils.sendEmail({ + await utils.sendEmail({ toEmails: [email], title: 'Reset password', template: { @@ -68,7 +134,7 @@ const userMutations = { }, }); - return link; + return 'sent'; }, /* @@ -78,14 +144,17 @@ const userMutations = { return Users.resetPassword(args); }, + /* + * Reset member's password + */ + usersResetMemberPassword(_root, args: { _id: string; newPassword: string }) { + return Users.resetMemberPassword(args); + }, + /* * Change user password */ - usersChangePassword( - _root, - args: { currentPassword: string; newPassword: string }, - { user }: { user: IUserDocument }, - ) { + usersChangePassword(_root, args: { currentPassword: string; newPassword: string }, { user }: IContext) { return Users.changePassword({ _id: user._id, ...args }); }, @@ -93,7 +162,7 @@ const userMutations = { * Update user */ async usersEdit(_root, args: IUsersEdit) { - const { _id, username, email, channelIds = [], groupIds = [], details, links } = args; + const { _id, username, email, channelIds, groupIds = [], brandIds = [], details, links } = args; const updatedUser = await Users.updateUser(_id, { username, @@ -101,10 +170,13 @@ const userMutations = { details, links, groupIds, + brandIds, }); // add new user to channels - await Channels.updateUserChannels(channelIds, _id); + await Channels.updateUserChannels(channelIds || [], _id); + + await resetPermissionsCache(); return updatedUser; }, @@ -127,13 +199,9 @@ const userMutations = { details: IDetail; links: ILink; }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { - const userOnDb = await Users.findOne({ _id: user._id }); - - if (!userOnDb) { - throw new Error('User not found'); - } + const userOnDb = await Users.getUser(user._id); const valid = await Users.comparePassword(password, userOnDb.password); @@ -148,7 +216,7 @@ const userMutations = { /* * Set Active or inactive user */ - async usersSetActiveStatus(_root, { _id }: { _id: string }, { user }: { user: IUserDocument }) { + async usersSetActiveStatus(_root, { _id }: { _id: string }, { user }: IContext) { if (user._id === _id) { throw new Error('You can not delete yourself'); } @@ -159,11 +227,11 @@ const userMutations = { /* * Invites users to team members */ - async usersInvite(_root, { entries }: { entries: Array<{ email: string; groupId: string }> }) { + async usersInvite(_root, { entries }: { entries: Array<{ email: string; password: string; groupId: string }> }) { for (const entry of entries) { await Users.checkDuplication({ email: entry.email }); - const token = await Users.createUserWithConfirmation(entry); + const token = await Users.invite(entry); sendInvitationEmail({ email: entry.email, token }); } @@ -180,13 +248,6 @@ const userMutations = { return token; }, - /* - * User has seen onboard - */ - async usersSeenOnBoard(_root, {}, { user }: { user: IUserDocument }) { - return Users.updateOnBoardSeen({ _id: user._id }); - }, - async usersConfirmInvitation( _root, { @@ -203,18 +264,23 @@ const userMutations = { username?: string; }, ) { - return Users.confirmInvitation({ token, password, passwordConfirmation, fullName, username }); + const user = await Users.confirmInvitation({ token, password, passwordConfirmation, fullName, username }); + + await messageBroker().sendMessage('erxes-api:integrations-notification', { + type: 'addUserId', + payload: { + _id: user._id, + }, + }); + + return user; }, - usersConfigEmailSignatures( - _root, - { signatures }: { signatures: IEmailSignature[] }, - { user }: { user: IUserDocument }, - ) { + usersConfigEmailSignatures(_root, { signatures }: { signatures: IEmailSignature[] }, { user }: IContext) { return Users.configEmailSignatures(user._id, signatures); }, - usersConfigGetNotificationByEmail(_root, { isAllowed }: { isAllowed: boolean }, { user }: { user: IUserDocument }) { + usersConfigGetNotificationByEmail(_root, { isAllowed }: { isAllowed: boolean }, { user }: IContext) { return Users.configGetNotificationByEmail(user._id, isAllowed); }, }; @@ -228,5 +294,6 @@ checkPermission(userMutations, 'usersEdit', 'usersEdit'); checkPermission(userMutations, 'usersInvite', 'usersInvite'); checkPermission(userMutations, 'usersResendInvitation', 'usersInvite'); checkPermission(userMutations, 'usersSetActiveStatus', 'usersSetActiveStatus'); +checkPermission(userMutations, 'usersResetMemberPassword', 'usersEdit'); export default userMutations; diff --git a/src/data/resolvers/mutations/webhooks.ts b/src/data/resolvers/mutations/webhooks.ts new file mode 100644 index 000000000..8ba09e8ec --- /dev/null +++ b/src/data/resolvers/mutations/webhooks.ts @@ -0,0 +1,90 @@ +import { Webhooks } from '../../../db/models'; +import { WEBHOOK_STATUS } from '../../../db/models/definitions/constants'; +import { IWebhook } from '../../../db/models/definitions/webhook'; +import { MODULE_NAMES } from '../../constants'; +import { putCreateLog, putDeleteLog, putUpdateLog } from '../../logUtils'; +import { moduleCheckPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { sendRequest } from '../../utils'; + +interface IWebhookEdit extends IWebhook { + _id: string; +} + +const webhookMutations = { + /** + * Creates a new webhook + */ + async webhooksAdd(_root, doc: IWebhook, { user, docModifier }: IContext) { + const webhook = await Webhooks.createWebhook(docModifier(doc)); + + sendRequest({ + url: webhook.url, + headers: { + 'Erxes-token': webhook.token || '', + }, + method: 'post', + }) + .then(async () => { + await Webhooks.updateStatus(webhook._id, WEBHOOK_STATUS.AVAILABLE); + }) + .catch(async () => { + await Webhooks.updateStatus(webhook._id, WEBHOOK_STATUS.UNAVAILABLE); + }); + + await putCreateLog( + { + type: MODULE_NAMES.WEBHOOK, + newData: webhook, + object: webhook, + description: `${webhook.url} has been created`, + }, + user, + ); + + return webhook; + }, + + /** + * Edits a webhook + */ + async webhooksEdit(_root, { _id, ...doc }: IWebhookEdit, { user }: IContext) { + const webhook = await Webhooks.getWebHook(_id); + const updated = await Webhooks.updateWebhook(_id, doc); + + await putUpdateLog( + { + type: MODULE_NAMES.WEBHOOK, + object: webhook, + newData: doc, + description: `${webhook.url} has been edited`, + }, + user, + ); + + return updated; + }, + + /** + * Removes a webhook + */ + async webhooksRemove(_root, { _id }: { _id: string }, { user }: IContext) { + const webhook = await Webhooks.getWebHook(_id); + const removed = await Webhooks.removeWebhooks(_id); + + await putDeleteLog( + { + type: MODULE_NAMES.WEBHOOK, + object: webhook, + description: `${webhook.url} has been removed`, + }, + user, + ); + + return removed; + }, +}; + +moduleCheckPermission(webhookMutations, 'manageWebhooks'); + +export default webhookMutations; diff --git a/src/data/resolvers/mutations/widgets.ts b/src/data/resolvers/mutations/widgets.ts new file mode 100644 index 000000000..62a5d8c33 --- /dev/null +++ b/src/data/resolvers/mutations/widgets.ts @@ -0,0 +1,718 @@ +import * as strip from 'strip'; +import { + Brands, + Companies, + Conformities, + Conversations, + Customers, + EngageMessages, + Forms, + FormSubmissions, + Integrations, + KnowledgeBaseArticles, + MessengerApps, + Users, +} from '../../../db/models'; +import Messages from '../../../db/models/ConversationMessages'; +import { IBrowserInfo, IVisitorContactInfoParams } from '../../../db/models/Customers'; +import { CONVERSATION_OPERATOR_STATUS, CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { IIntegrationDocument, IMessengerDataMessagesItem } from '../../../db/models/definitions/integrations'; +import { IKnowledgebaseCredentials, ILeadCredentials } from '../../../db/models/definitions/messengerApps'; +import { debugBase } from '../../../debuggers'; +import { trackViewPageEvent } from '../../../events'; +import memoryStorage from '../../../inmemoryStorage'; +import { graphqlPubsub } from '../../../pubsub'; +import { AUTO_BOT_MESSAGES, BOT_MESSAGE_TYPES } from '../../constants'; +import { registerOnboardHistory, sendEmail, sendMobileNotification, sendRequest, sendToWebhook } from '../../utils'; +import { conversationNotifReceivers } from './conversations'; + +interface ISubmission { + _id: string; + value: any; + type?: string; + validation?: string; +} + +interface IWidgetEmailParams { + toEmails: string[]; + fromEmail: string; + title: string; + content: string; +} + +export const getMessengerData = async (integration: IIntegrationDocument) => { + let messagesByLanguage: IMessengerDataMessagesItem | null = null; + let messengerData = integration.messengerData; + + if (messengerData) { + messengerData = messengerData.toJSON(); + + const languageCode = integration.languageCode || 'en'; + const messages = (messengerData || {}).messages; + + if (messages) { + messagesByLanguage = messages[languageCode]; + } + } + + // knowledgebase app ======= + const kbApp = await MessengerApps.findOne({ + kind: 'knowledgebase', + 'credentials.integrationId': integration._id, + }); + + const topicId = kbApp && kbApp.credentials ? (kbApp.credentials as IKnowledgebaseCredentials).topicId : null; + + // lead app ========== + const leadApp = await MessengerApps.findOne({ kind: 'lead', 'credentials.integrationId': integration._id }); + + const formCode = leadApp && leadApp.credentials ? (leadApp.credentials as ILeadCredentials).formCode : null; + + // website app ============ + const websiteApps = await MessengerApps.find({ + kind: 'website', + 'credentials.integrationId': integration._id, + }); + + return { + ...(messengerData || {}), + messages: messagesByLanguage, + knowledgeBaseTopicId: topicId, + websiteApps, + formCode, + }; +}; + +const widgetMutations = { + // Find integrationId by brandCode + async widgetsLeadConnect(_root, args: { brandCode: string; formCode: string; cachedCustomerId?: string }) { + const brand = await Brands.findOne({ code: args.brandCode }); + const form = await Forms.findOne({ code: args.formCode }); + + if (!brand || !form) { + throw new Error('Invalid configuration'); + } + + // find integration by brandId & formId + const integ = await Integrations.findOne({ + brandId: brand._id, + formId: form._id, + }); + + if (!integ) { + throw new Error('Integration not found'); + } + + if (integ.leadData && integ.leadData.loadType === 'embedded') { + await Integrations.increaseViewCount(form._id); + } + + if (integ.createdUserId) { + const user = await Users.getUser(integ.createdUserId); + + registerOnboardHistory({ type: 'leadIntegrationInstalled', user }); + } + + if (integ.leadData?.isRequireOnce && args.cachedCustomerId) { + const conversation = await Conversations.findOne({ customerId: args.cachedCustomerId, integrationId: integ.id }); + if (conversation) { + return null; + } + } + + // return integration details + return { + integration: integ, + form, + }; + }, + + // create new conversation using form data + async widgetsSaveLead( + _root, + args: { + integrationId: string; + formId: string; + submissions: ISubmission[]; + browserInfo: any; + cachedCustomerId?: string; + }, + ) { + const { integrationId, formId, submissions, browserInfo, cachedCustomerId } = args; + + const form = await Forms.findOne({ _id: formId }); + + if (!form) { + throw new Error('Form not found'); + } + + const errors = await Forms.validate(formId, submissions); + + if (errors.length > 0) { + return { status: 'error', errors }; + } + + const content = form.title; + + let email; + let phone; + let firstName = ''; + let lastName = ''; + + submissions.forEach(submission => { + if (submission.type === 'email') { + email = submission.value; + } + + if (submission.type === 'phone') { + phone = submission.value; + } + + if (submission.type === 'firstName') { + firstName = submission.value; + } + + if (submission.type === 'lastName') { + lastName = submission.value; + } + }); + + // get or create customer + let customer = await Customers.getWidgetCustomer({ integrationId, email, phone, cachedCustomerId }); + + if (!customer) { + customer = await Customers.createCustomer({ + integrationId, + primaryEmail: email, + emails: [email], + firstName, + lastName, + primaryPhone: phone, + }); + } + + const customerDoc = { + location: browserInfo, + firstName: customer.firstName || firstName, + lastName: customer.lastName || lastName, + ...(customer.primaryEmail + ? {} + : { + emails: [email], + primaryEmail: email, + }), + ...(customer.primaryPhone + ? {} + : { + phones: [phone], + primaryPhone: phone, + }), + }; + + // update location info and missing fields + await Customers.updateCustomer(customer._id, customerDoc); + + // Inserting customer id into submitted customer ids + const doc = { + formId, + customerId: customer._id, + submittedAt: new Date(), + }; + + await FormSubmissions.createFormSubmission(doc); + + // create conversation + const conversation = await Conversations.createConversation({ + integrationId, + customerId: customer._id, + content, + }); + + // create message + const message = await Messages.createMessage({ + conversationId: conversation._id, + customerId: customer._id, + content, + formWidgetData: submissions, + }); + + // increasing form submitted count + await Integrations.increaseContactsGathered(formId); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + await sendToWebhook('create', 'popupSubmitted', { + formId: args.formId, + submissions: args.submissions, + customer: customerDoc, + cachedCustomerId: args.cachedCustomerId, + }); + + return { status: 'ok', messageId: message._id }; + }, + + widgetsLeadIncreaseViewCount(_root, { formId }: { formId: string }) { + return Integrations.increaseViewCount(formId); + }, + + widgetsKnowledgebaseIncReactionCount( + _root, + { articleId, reactionChoice }: { articleId: string; reactionChoice: string }, + ) { + return KnowledgeBaseArticles.incReactionCount(articleId, reactionChoice); + }, + + /* + * Create a new customer or update existing customer info + * when connection established + */ + async widgetsMessengerConnect( + _root, + args: { + brandCode: string; + email?: string; + phone?: string; + code?: string; + isUser?: boolean; + companyData?: any; + data?: any; + cachedCustomerId?: string; + deviceToken?: string; + }, + ) { + const { brandCode, email, phone, code, isUser, companyData, data, cachedCustomerId, deviceToken } = args; + + const customData = data; + + // find brand + const brand = await Brands.findOne({ code: brandCode }); + + if (!brand) { + throw new Error('Brand not found'); + } + + // find integration + const integration = await Integrations.getWidgetIntegration(brandCode, 'messenger'); + + if (!integration) { + throw new Error('Integration not found'); + } + + let customer = await Customers.getWidgetCustomer({ + integrationId: integration._id, + cachedCustomerId, + email, + phone, + code, + }); + + const doc = { + integrationId: integration._id, + email, + phone, + code, + isUser, + deviceToken, + }; + + customer = customer + ? await Customers.updateMessengerCustomer({ _id: customer._id, doc, customData }) + : await Customers.createMessengerCustomer({ doc, customData }); + + // get or create company + if (companyData && companyData.name) { + let company = await Companies.findOne({ + $or: [{ names: { $in: [companyData.name] } }, { primaryName: companyData.name }], + }); + + if (!company) { + companyData.primaryName = companyData.name; + companyData.names = [companyData.name]; + + company = await Companies.createCompany({ ...companyData, scopeBrandIds: [brand._id] }); + } + + // add company to customer's companyIds list + await Conformities.create({ + mainType: 'customer', + mainTypeId: customer._id, + relType: 'company', + relTypeId: company._id, + }); + } + + if (integration.createdUserId) { + const user = await Users.getUser(integration.createdUserId); + + registerOnboardHistory({ type: 'messengerIntegrationInstalled', user }); + } + + return { + integrationId: integration._id, + uiOptions: integration.uiOptions, + languageCode: integration.languageCode, + messengerData: await getMessengerData(integration), + customerId: customer._id, + brand, + }; + }, + + /* + * Create a new message + */ + async widgetsInsertMessage( + _root, + args: { + integrationId: string; + customerId: string; + conversationId?: string; + message: string; + attachments?: any[]; + contentType: string; + }, + ) { + const { integrationId, customerId, conversationId, message, attachments, contentType } = args; + + const conversationContent = strip(message || '').substring(0, 100); + + // customer can write a message + // to the closed conversation even if it's closed + let conversation; + + const integration = await Integrations.findOne({ _id: integrationId }).lean(); + + const messengerData = integration.messengerData || {}; + + const { botEndpointUrl } = messengerData; + + let HAS_BOTENDPOINT_URL = (botEndpointUrl || '').length > 0; + + const getConversationStatus = (IS_CONVERSATION_OPERATOR?: boolean) => { + const { OPEN, CLOSED } = CONVERSATION_STATUSES; + + if (IS_CONVERSATION_OPERATOR) { + HAS_BOTENDPOINT_URL = false; + } + + return !HAS_BOTENDPOINT_URL || IS_CONVERSATION_OPERATOR ? OPEN : CLOSED; + }; + + if (conversationId) { + conversation = await Conversations.findOne({ _id: conversationId }).lean(); + + conversation = await Conversations.findByIdAndUpdate( + conversationId, + { + // mark this conversation as unread + readUserIds: [], + + // reopen this conversation if it's closed + status: getConversationStatus(conversation.operatorStatus !== CONVERSATION_OPERATOR_STATUS.BOT), + }, + { new: true }, + ); + // create conversation + } else { + conversation = await Conversations.createConversation({ + customerId, + integrationId, + operatorStatus: HAS_BOTENDPOINT_URL ? CONVERSATION_OPERATOR_STATUS.BOT : CONVERSATION_OPERATOR_STATUS.OPERATOR, + status: getConversationStatus(), + content: conversationContent, + }); + } + + // create message + const msg = await Messages.createMessage({ + conversationId: conversation._id, + customerId, + attachments, + contentType, + content: message, + }); + + await Conversations.updateOne( + { _id: msg.conversationId }, + { + $set: { + // Reopen its conversation if it's closed + status: getConversationStatus(), + + // setting conversation's content to last message + content: conversationContent, + + // Mark as unread + readUserIds: [], + }, + }, + ); + + // mark customer as active + await Customers.markCustomerAsActive(conversation.customerId); + + graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: msg }); + graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: msg }); + + // bot message ================ + if (HAS_BOTENDPOINT_URL) { + graphqlPubsub.publish('conversationBotTypingStatus', { + conversationBotTypingStatus: { conversationId: msg.conversationId, typing: true }, + }); + + const botRequest = await sendRequest({ + method: 'POST', + url: botEndpointUrl, + body: { + type: 'text', + text: message, + }, + }); + + const { responses } = botRequest; + + const botData = + responses.length !== 0 + ? responses + : [ + { + type: 'text', + text: AUTO_BOT_MESSAGES.NO_RESPONSE, + }, + ]; + + const botMessage = await Messages.createMessage({ + conversationId: conversation._id, + customerId, + contentType, + botData, + }); + + graphqlPubsub.publish('conversationBotTypingStatus', { + conversationBotTypingStatus: { conversationId: msg.conversationId, typing: false }, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: botMessage }); + graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: botMessage }); + } + + const customerLastStatus = await memoryStorage().get(`customer_last_status_${customerId}`, 'left'); + + if (customerLastStatus === 'left') { + memoryStorage().set(`customer_last_status_${customerId}`, 'joined'); + + // customer has joined + time + const conversationMessages = await Conversations.changeCustomerStatus( + 'joined', + customerId, + conversation.integrationId, + ); + + for (const mg of conversationMessages) { + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: mg, + }); + } + + // notify as connected + graphqlPubsub.publish('customerConnectionChanged', { + customerConnectionChanged: { + _id: customerId, + status: 'connected', + }, + }); + } + + if (!HAS_BOTENDPOINT_URL) { + sendMobileNotification({ + title: 'You have a new message', + body: conversationContent, + customerId, + conversationId: conversation._id, + receivers: conversationNotifReceivers(conversation, customerId), + }); + } + + await sendToWebhook('create', 'customerMessages', msg); + + return msg; + }, + + /* + * Mark given conversation's messages as read + */ + async widgetsReadConversationMessages(_root, args: { conversationId: string }) { + await Messages.updateMany( + { + conversationId: args.conversationId, + userId: { $exists: true }, + isCustomerRead: { $ne: true }, + }, + { isCustomerRead: true }, + { multi: true }, + ); + + return args.conversationId; + }, + + widgetsSaveCustomerGetNotified(_root, args: IVisitorContactInfoParams) { + return Customers.saveVisitorContactInfo(args); + }, + + /* + * Update customer location field + */ + async widgetsSaveBrowserInfo(_root, { customerId, browserInfo }: { customerId: string; browserInfo: IBrowserInfo }) { + // update location + await Customers.updateLocation(customerId, browserInfo); + + try { + await trackViewPageEvent({ customerId, attributes: { url: browserInfo.url } }); + } catch (e) { + /* istanbul ignore next */ + debugBase(`Error occurred during widgets save browser info ${e.message}`); + } + + // update messenger session data + const customer = await Customers.updateSession(customerId); + + // Preventing from displaying non messenger integrations like form's messages + // as last unread message + const integration = await Integrations.findOne({ + _id: customer.integrationId, + kind: 'messenger', + }); + + if (!integration) { + throw new Error('Integration not found'); + } + + const brand = await Brands.findOne({ _id: integration.brandId }); + + if (!brand) { + throw new Error('Brand not found'); + } + + // try to create engage chat auto messages + if (!customer.primaryEmail) { + await EngageMessages.createVisitorMessages({ + brand, + integration, + customer, + browserInfo, + }); + } + + // find conversations + const convs = await Conversations.find({ + integrationId: integration._id, + customerId: customer._id, + }); + + return Messages.findOne(Conversations.widgetsUnreadMessagesQuery(convs)); + }, + + widgetsSendTypingInfo(_root, args: { conversationId: string; text?: string }) { + graphqlPubsub.publish('conversationClientTypingStatusChanged', { + conversationClientTypingStatusChanged: args, + }); + + return 'ok'; + }, + + async widgetsSendEmail(_root, args: IWidgetEmailParams) { + const { toEmails, fromEmail, title, content } = args; + + await sendEmail({ + toEmails, + fromEmail, + title, + template: { data: { content } }, + }); + }, + + async widgetBotRequest( + _root, + { + integrationId, + conversationId, + customerId, + message, + payload, + type, + }: { + conversationId: string; + customerId: string; + integrationId: string; + message: string; + payload: string; + type: string; + }, + ) { + const integration = await Integrations.findOne({ _id: integrationId }).lean(); + + const { botEndpointUrl } = integration.messengerData; + + // create customer message + const msg = await Messages.createMessage({ + conversationId, + customerId, + content: message, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: msg }); + graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: msg }); + + let botMessage; + let botData; + + if (type !== BOT_MESSAGE_TYPES.SAY_SOMETHING) { + const botRequest = await sendRequest({ + method: 'POST', + url: botEndpointUrl, + body: { + type: 'text', + text: payload, + }, + }); + + const { responses } = botRequest; + + botData = + responses.length !== 0 + ? responses + : [ + { + type: 'text', + text: AUTO_BOT_MESSAGES.NO_RESPONSE, + }, + ]; + } else { + botData = [ + { + type: 'text', + text: payload, + }, + ]; + } + + // create bot message + botMessage = await Messages.createMessage({ + conversationId, + customerId, + botData, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { conversationClientMessageInserted: botMessage }); + graphqlPubsub.publish('conversationMessageInserted', { conversationMessageInserted: botMessage }); + + return botMessage; + }, +}; + +export default widgetMutations; diff --git a/src/data/resolvers/pipeline.ts b/src/data/resolvers/pipeline.ts index d75521586..667894d09 100644 --- a/src/data/resolvers/pipeline.ts +++ b/src/data/resolvers/pipeline.ts @@ -1,7 +1,13 @@ -import { Users } from '../../db/models'; +import { Deals, GrowthHacks, Tasks, Tickets, Users } from '../../db/models'; import { IPipelineDocument } from '../../db/models/definitions/boards'; -import { PIPELINE_VISIBLITIES } from '../../db/models/definitions/constants'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { BOARD_TYPES, PIPELINE_VISIBLITIES } from '../../db/models/definitions/constants'; +import { IContext } from '../types'; +import { + generateDealCommonFilters, + generateGrowthHackCommonFilters, + generateTaskCommonFilters, + generateTicketCommonFilters, +} from './queries/boardUtils'; export default { members(pipeline: IPipelineDocument, {}) { @@ -12,7 +18,7 @@ export default { return []; }, - isWatched(pipeline: IPipelineDocument, _args, { user }: { user: IUserDocument }) { + isWatched(pipeline: IPipelineDocument, _args, { user }: IContext) { const watchedUserIds = pipeline.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { @@ -21,4 +27,48 @@ export default { return false; }, + + state(pipeline: IPipelineDocument) { + if (pipeline.startDate && pipeline.endDate) { + const now = new Date().getTime(); + + const startDate = new Date(pipeline.startDate).getTime(); + const endDate = new Date(pipeline.endDate).getTime(); + + if (now > endDate) { + return 'Completed'; + } else if (now < endDate && now > startDate) { + return 'In progress'; + } else { + return 'Not started'; + } + } + + return ''; + }, + + async itemsTotalCount(pipeline: IPipelineDocument, _args, { user }: IContext) { + switch (pipeline.type) { + case BOARD_TYPES.DEAL: { + const filter = await generateDealCommonFilters(user._id, { pipelineId: pipeline._id }); + + return Deals.find(filter).countDocuments(); + } + case BOARD_TYPES.TICKET: { + const filter = await generateTicketCommonFilters(user._id, { pipelineId: pipeline._id }); + + return Tickets.find(filter).countDocuments(); + } + case BOARD_TYPES.TASK: { + const filter = await generateTaskCommonFilters(user._id, { pipelineId: pipeline._id }); + + return Tasks.find(filter).countDocuments(); + } + case BOARD_TYPES.GROWTH_HACK: { + const filter = await generateGrowthHackCommonFilters(user._id, { pipelineId: pipeline._id }); + + return GrowthHacks.find(filter).countDocuments(); + } + } + }, }; diff --git a/src/data/resolvers/product.ts b/src/data/resolvers/product.ts new file mode 100644 index 000000000..de02e66e5 --- /dev/null +++ b/src/data/resolvers/product.ts @@ -0,0 +1,12 @@ +import { ProductCategories, Tags } from '../../db/models'; +import { IProductDocument } from '../../db/models/definitions/deals'; + +export default { + category(product: IProductDocument) { + return ProductCategories.findOne({ _id: product.categoryId }); + }, + + getTags(product: IProductDocument) { + return Tags.find({ _id: { $in: product.tagIds || [] } }); + }, +}; diff --git a/src/data/resolvers/productCategory.ts b/src/data/resolvers/productCategory.ts new file mode 100644 index 000000000..dea69af82 --- /dev/null +++ b/src/data/resolvers/productCategory.ts @@ -0,0 +1,12 @@ +import { Products } from '../../db/models'; +import { IProductCategoryDocument } from '../../db/models/definitions/deals'; + +export default { + isRoot(category: IProductCategoryDocument, {}) { + return category.parentId ? false : true; + }, + + async productCount(category: IProductCategoryDocument, {}) { + return Products.countDocuments({ categoryId: category._id }); + }, +}; diff --git a/src/data/resolvers/queries/activityLogs.ts b/src/data/resolvers/queries/activityLogs.ts index 1b7e87a1d..25bc939c6 100644 --- a/src/data/resolvers/queries/activityLogs.ts +++ b/src/data/resolvers/queries/activityLogs.ts @@ -1,28 +1,157 @@ -import { ActivityLogs } from '../../../db/models'; -import { IActivityLog } from '../../../db/models/definitions/activityLogs'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -const activityLogQueries = { - /** - * Get activity log list - */ - activityLogs(_root, doc: IActivityLog) { - const { contentType, contentId, activityType, limit } = doc; - - const query = { 'contentType.type': contentType, 'contentType.id': contentId }; - - if (activityType) { - query['activity.type'] = activityType; - } - - const sort = { createdAt: -1 }; - - return ActivityLogs.find(query) - .sort(sort) - .limit(limit); - }, -}; - -moduleRequireLogin(activityLogQueries); - -export default activityLogQueries; +import { + ActivityLogs, + Conformities, + Conversations, + EmailDeliveries, + EngageMessages, + InternalNotes, + Tasks, +} from '../../../db/models'; +import { IActivityLogDocument } from '../../../db/models/definitions/activityLogs'; +import { debugExternalApi } from '../../../debuggers'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IListArgs { + contentType: string; + contentId: string; + activityType: string; +} + +const activityLogQueries = { + /** + * Get activity log list + */ + async activityLogs(_root, doc: IListArgs, { dataSources }: IContext) { + const { contentType, contentId, activityType } = doc; + + const activities: IActivityLogDocument[] = []; + + const relatedItemIds = await Conformities.savedConformity({ + mainType: contentType, + mainTypeId: contentId, + relTypes: contentType !== 'task' ? ['deal', 'ticket'] : ['deal', 'ticket', 'task'], + }); + + const relatedTaskIds = await Conformities.savedConformity({ + mainType: contentType, + mainTypeId: contentId, + relTypes: ['task'], + }); + + const collectItems = (items: any, type?: string) => { + if (items) { + items.map(item => { + let result: IActivityLogDocument = {} as any; + + item = item.toJSON(); + + if (!type) { + result = item; + } + + if (type && type !== 'taskDetail') { + result._id = item._id; + result.contentType = type; + result.contentId = contentId; + result.createdAt = item.createdAt; + } + + if (type === 'taskDetail') { + result._id = item._id; + result.contentType = type; + result.createdAt = item.closeDate || item.createdAt; + } + activities.push(result); + }); + } + }; + + const collectConversations = async () => { + collectItems( + await Conversations.find({ $or: [{ customerId: contentId }, { participatedUserIds: contentId }] }).limit(25), + 'conversation', + ); + + if (contentType === 'customer') { + let conversationIds; + + try { + conversationIds = await dataSources.IntegrationsAPI.fetchApi('/facebook/get-customer-posts', { + customerId: contentId, + }); + collectItems(await Conversations.find({ _id: { $in: conversationIds } }), 'comment'); + } catch (e) { + debugExternalApi(e); + } + } + }; + + const collectActivityLogs = async () => { + collectItems(await ActivityLogs.find({ contentId: { $in: [...relatedItemIds, contentId] } })); + }; + + const collectInternalNotes = async () => { + collectItems(await InternalNotes.find({ contentTypeId: contentId }).sort({ createdAt: -1 }), 'note'); + }; + + const collectEngageMessages = async () => { + collectItems(await EngageMessages.find({ customerIds: contentId, method: 'email' }), 'engage-email'); + collectItems(await EmailDeliveries.find({ customerId: contentId }), 'email'); + }; + + const collectTasks = async () => { + if (contentType !== 'task') { + collectItems( + await Tasks.find({ $and: [{ _id: { $in: relatedTaskIds } }, { status: { $ne: 'archived' } }] }).sort({ + closeDate: 1, + }), + 'taskDetail', + ); + } + + const contentIds = activities.filter(activity => activity.action === 'convert').map(activity => activity.content); + + if (Array.isArray(contentIds)) { + collectItems(await Conversations.find({ _id: { $in: contentIds } }).limit(25), 'conversation'); + } + }; + + switch (activityType) { + case 'conversation': + await collectConversations(); + break; + + case 'internal_note': + await collectInternalNotes(); + break; + + case 'task': + await collectTasks(); + break; + + case 'email': + await collectEngageMessages(); + break; + + default: + await collectConversations(); + await collectActivityLogs(); + await collectInternalNotes(); + await collectEngageMessages(); + await collectTasks(); + + break; + } + + activities.sort((a, b) => { + return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); + }); + + return activities; + }, +}; + +moduleRequireLogin(activityLogQueries); + +export default activityLogQueries; diff --git a/src/data/resolvers/queries/boardUtils.ts b/src/data/resolvers/queries/boardUtils.ts index 4ae97ca4d..93ebc539f 100644 --- a/src/data/resolvers/queries/boardUtils.ts +++ b/src/data/resolvers/queries/boardUtils.ts @@ -1,128 +1,199 @@ import * as moment from 'moment'; -import { Stages } from '../../../db/models'; -import { getNextMonth, getToday } from '../../utils'; - -export const contains = (values: string[] = [], empty = false) => { - if (empty) { - return []; - } +import { Conformities, Pipelines, Stages } from '../../../db/models'; +import { IItemCommonFields } from '../../../db/models/definitions/boards'; +import { BOARD_STATUSES } from '../../../db/models/definitions/constants'; +import { getNextMonth, getToday, regexSearchText } from '../../utils'; +import { IListParams } from './boards'; + +export interface IArchiveArgs { + pipelineId: string; + search: string; + page?: number; + perPage?: number; +} +const contains = (values: string[]) => { return { $in: values }; }; -export const generateCommonFilters = async (args: any) => { +export const generateCommonFilters = async (currentUserId: string, args: any) => { const { - $and, - date, pipelineId, stageId, search, - overdue, - nextMonth, - nextDay, - nextWeek, - noCloseDate, + closeDateType, assignedUserIds, customerIds, companyIds, - order, - probability, + conformityMainType, + conformityMainTypeId, + conformityIsRelated, + conformityIsSaved, initialStageId, + type, + labelIds, + priority, + userIds, } = args; - const assignedToNoOne = value => { + const isListEmpty = value => { return value.length === 1 && value[0].length === 0; }; - const filter: any = {}; + const filter: any = { status: { $ne: BOARD_STATUSES.ARCHIVED } }; + + let filterIds: string[] = []; if (assignedUserIds) { // Filter by assigned to no one - const notAssigned = assignedToNoOne(assignedUserIds); + const notAssigned = isListEmpty(assignedUserIds); - filter.assignedUserIds = notAssigned ? contains([], true) : contains(assignedUserIds); + filter.assignedUserIds = notAssigned ? [] : contains(assignedUserIds); } - if ($and) { - filter.$and = $and; - } + if (customerIds && type) { + const relIds = await Conformities.filterConformity({ + mainType: 'customer', + mainTypeIds: customerIds, + relType: type, + }); - if (customerIds) { - filter.customerIds = contains(customerIds); + filterIds = relIds; } - if (companyIds) { - filter.companyIds = contains(companyIds); + if (companyIds && type) { + const relIds = await Conformities.filterConformity({ + mainType: 'company', + mainTypeIds: companyIds, + relType: type, + }); + + filterIds = filterIds.length ? filterIds.filter(id => relIds.includes(id)) : relIds; } - if (order) { - filter.order = order; + if (customerIds || companyIds) { + filter._id = contains(filterIds || []); } - if (probability) { - filter.probability = probability; + if (conformityMainType && conformityMainTypeId) { + if (conformityIsSaved) { + const relIds = await Conformities.savedConformity({ + mainType: conformityMainType, + mainTypeId: conformityMainTypeId, + relTypes: [type], + }); + + filter._id = contains(relIds || []); + } + + if (conformityIsRelated) { + const relIds = await Conformities.relatedConformity({ + mainType: conformityMainType, + mainTypeId: conformityMainTypeId, + relType: type, + }); + + filter._id = contains(relIds); + } } if (initialStageId) { filter.initialStageId = initialStageId; } - if (nextDay) { - const tommorrow = moment().add(1, 'days'); - - filter.closeDate = { - $gte: new Date(tommorrow.startOf('day').format('YYYY-MM-DD')), - $lte: new Date(tommorrow.endOf('day').format('YYYY-MM-DD')), - }; + if (closeDateType) { + if (closeDateType === 'nextDay') { + const tommorrow = moment().add(1, 'days'); + + filter.closeDate = { + $gte: new Date(tommorrow.startOf('day').toISOString()), + $lte: new Date(tommorrow.endOf('day').toISOString()), + }; + } + + if (closeDateType === 'nextWeek') { + const monday = moment() + .day(1 + 7) + .format('YYYY-MM-DD'); + const nextSunday = moment() + .day(7 + 7) + .format('YYYY-MM-DD'); + + filter.closeDate = { + $gte: new Date(monday), + $lte: new Date(nextSunday), + }; + } + + if (closeDateType === 'nextMonth') { + const now = new Date(); + const { start, end } = getNextMonth(now); + + filter.closeDate = { + $gte: new Date(start), + $lte: new Date(end), + }; + } + + if (closeDateType === 'noCloseDate') { + filter.closeDate = { $exists: false }; + } + + if (closeDateType === 'overdue') { + const now = new Date(); + const today = getToday(now); + + filter.closeDate = { $lt: today }; + } } - if (nextWeek) { - const monday = moment() - .day(1 + 7) - .format('YYYY-MM-DD'); - const nextSunday = moment() - .day(7 + 7) - .format('YYYY-MM-DD'); + if (search) { + Object.assign(filter, regexSearchText(search)); + } - filter.closeDate = { - $gte: new Date(monday), - $lte: new Date(nextSunday), - }; + if (stageId) { + filter.stageId = stageId; } - if (nextMonth) { - const now = new Date(); - const { start, end } = getNextMonth(now); + if (labelIds) { + const isEmpty = isListEmpty(labelIds); - filter.closeDate = { - $gte: new Date(start), - $lte: new Date(end), - }; + filter.labelIds = isEmpty ? { $in: [null, []] } : { $in: labelIds }; } - if (noCloseDate) { - filter.closeDate = { $exists: false }; + if (priority) { + filter.priority = contains(priority); } - if (overdue) { - const now = new Date(); - const today = getToday(now); - - filter.closeDate = { $lt: today }; + if (pipelineId) { + const pipeline = await Pipelines.getPipeline(pipelineId); + if (pipeline.isCheckUser && !(pipeline.excludeCheckUserIds || []).includes(currentUserId)) { + Object.assign(filter, { $or: [{ assignedUserIds: { $in: [currentUserId] } }, { userId: currentUserId }] }); + } } - if (search) { - filter.$or = [ - { name: new RegExp(`.*${search || ''}.*`, 'i') }, - { description: new RegExp(`.*${search || ''}.*`, 'i') }, - ]; + if (userIds) { + const isEmpty = isListEmpty(userIds); + + filter.userId = isEmpty ? { $in: [null, []] } : { $in: userIds }; } - if (stageId) { - filter.stageId = stageId; + return filter; +}; + +export const generateDealCommonFilters = async (currentUserId: string, args: any, extraParams?: any) => { + args.type = 'deal'; + + const filter = await generateCommonFilters(currentUserId, args); + const { productIds } = extraParams || args; + + if (productIds) { + filter['productsData.productId'] = contains(productIds); } // Calendar monthly date + const { date, pipelineId } = args; + if (date) { const stageIds = await Stages.find({ pipelineId }).distinct('_id'); @@ -133,38 +204,52 @@ export const generateCommonFilters = async (args: any) => { return filter; }; -export const generateDealCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { productIds } = extraParams || args; +export const generateTicketCommonFilters = async (currentUserId: string, args: any, extraParams?: any) => { + args.type = 'ticket'; - if (productIds) { - filter['productsData.productId'] = contains(productIds); + const filter = await generateCommonFilters(currentUserId, args); + const { source } = extraParams || args; + + if (source) { + filter.source = contains(source); } return filter; }; -export const generateTicketCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { priority, source } = extraParams || args; +export const generateTaskCommonFilters = async (currentUserId: string, args: any) => { + args.type = 'task'; - if (priority) { - filter.priority = contains(priority); - } + return generateCommonFilters(currentUserId, args); +}; - if (source) { - filter.source = contains(source); +export const generateSort = (args: IListParams) => { + let sort: any = { order: 1, createdAt: -1 }; + + const { sortField, sortDirection } = args; + + if (sortField && sortDirection) { + sort = { [sortField]: sortDirection }; } - return filter; + return sort; }; -export const generateTaskCommonFilters = async (args: any, extraParams?: any) => { - const filter = await generateCommonFilters(args); - const { priority } = extraParams || args; +export const generateGrowthHackCommonFilters = async (currentUserId: string, args: any, extraParams?: any) => { + args.type = 'growthHack'; - if (priority) { - filter.priority = contains(priority); + const { hackStage, pipelineId, stageId } = extraParams || args; + + const filter = await generateCommonFilters(currentUserId, args); + + if (hackStage) { + filter.hackStages = contains(hackStage); + } + + if (!stageId && pipelineId) { + const stageIds = await Stages.find({ pipelineId }).distinct('_id'); + + filter.stageId = { $in: stageIds }; } return filter; @@ -175,15 +260,84 @@ interface IDate { year: number; } -export const dateSelector = (date: IDate) => { +const dateSelector = (date: IDate) => { const { year, month } = date; - const currentDate = new Date(); - const start = currentDate.setFullYear(year, month, 1); - const end = currentDate.setFullYear(year, month + 1, 0); + const start = new Date(Date.UTC(year, month, 1, 0, 0, 0)); + const end = new Date(Date.UTC(year, month + 1, 1, 0, 0, 0)); return { - $gte: new Date(start), - $lte: new Date(end), + $gte: start, + $lte: end, }; }; + +export const checkItemPermByUser = async (currentUserId: string, item: IItemCommonFields) => { + const stage = await Stages.getStage(item.stageId); + + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + + if (pipeline.visibility === 'private' && !(pipeline.memberIds || []).includes(currentUserId)) { + throw new Error('You do not have permission to view.'); + } + + // pipeline is Show only the users assigned(created) cards checked + // and current user nothing dominant users + // current user hans't this carts assigned and created + if ( + pipeline.isCheckUser && + !(pipeline.excludeCheckUserIds || []).includes(currentUserId) && + !((item.assignedUserIds || []).includes(currentUserId) || item.userId === currentUserId) + ) { + throw new Error('You do not have permission to view.'); + } + + return item; +}; + +export const archivedItems = async (params: IArchiveArgs, collection: any) => { + const { pipelineId, search, ...listArgs } = params; + + const filter: any = { status: BOARD_STATUSES.ARCHIVED }; + const { page = 0, perPage = 0 } = listArgs; + + const stages = await Stages.find({ pipelineId }); + + if (stages.length > 0) { + filter.stageId = { $in: stages.map(stage => stage._id) }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return collection + .find(filter) + .sort({ + modifiedAt: -1, + }) + .skip(page || 0) + .limit(perPage || 20); + } + + return []; +}; + +export const archivedItemsCount = async (params: IArchiveArgs, collection: any) => { + const { pipelineId, search } = params; + + const filter: any = { status: BOARD_STATUSES.ARCHIVED }; + + const stages = await Stages.find({ pipelineId }); + + if (stages.length > 0) { + filter.stageId = { $in: stages.map(stage => stage._id) }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return collection.countDocuments(filter); + } + + return 0; +}; diff --git a/src/data/resolvers/queries/boards.ts b/src/data/resolvers/queries/boards.ts index 4fc3fe75c..f6b3191ee 100644 --- a/src/data/resolvers/queries/boards.ts +++ b/src/data/resolvers/queries/boards.ts @@ -1,81 +1,258 @@ -import { Boards, Pipelines, Stages } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -export interface IDate { - month: number; - year: number; -} - -export interface IListParams { - pipelineId?: string; - stageId: string; - skip?: number; - date?: IDate; - search?: string; - customerIds?: [string]; - companyIds?: [string]; - assignedUserIds?: [string]; -} - -const boardQueries = { - /** - * Boards list - */ - boards(_root, { type }: { type: string }) { - return Boards.find({ type }).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Board detail - */ - boardDetail(_root, { _id }: { _id: string }) { - return Boards.findOne({ _id }); - }, - - /** - * Get last board - */ - boardGetLast(_root, { type }: { type: string }) { - return Boards.findOne({ type }).sort({ createdAt: -1 }); - }, - - /** - * Pipelines list - */ - pipelines(_root, { boardId }: { boardId: string; type: string }) { - return Pipelines.find({ boardId }).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Pipeline detail - */ - pipelineDetail(_root, { _id }: { _id: string }) { - return Pipelines.findOne({ _id }); - }, - - /** - * Stages list - */ - stages(_root, { pipelineId, isNotLost }: { pipelineId: string; isNotLost: boolean }) { - const filter: any = {}; - - filter.pipelineId = pipelineId; - - if (isNotLost) { - filter.probability = { $ne: 'Lost' }; - } - - return Stages.find(filter).sort({ order: 1, createdAt: -1 }); - }, - - /** - * Stage detail - */ - stageDetail(_root, { _id }: { _id: string }) { - return Stages.findOne({ _id }); - }, -}; - -moduleRequireLogin(boardQueries); - -export default boardQueries; +import { Boards, Deals, Pipelines, Stages, Tasks, Tickets } from '../../../db/models'; +import { BOARD_STATUSES } from '../../../db/models/definitions/constants'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate, regexSearchText } from '../../utils'; +import { IConformityQueryParams } from './types'; + +export interface IDate { + month: number; + year: number; +} + +export interface IListParams extends IConformityQueryParams { + pipelineId: string; + stageId: string; + skip?: number; + date?: IDate; + search?: string; + customerIds?: string[]; + companyIds?: string[]; + assignedUserIds?: string[]; + sortField?: string; + sortDirection?: number; + labelIds?: string[]; + userIds?: string[]; +} + +const boardQueries = { + /** + * Boards list + */ + boards(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Boards.find({ ...commonQuerySelector, type }).sort({ order: 1, createdAt: -1 }); + }, + + /** + * Boards count + */ + async boardCounts(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + const boards = await Boards.find({ ...commonQuerySelector, type }).sort({ name: 1 }); + + const counts: Array<{ _id: string; name: string; count: number }> = []; + + let allCount = 0; + + for (const board of boards) { + const count = await Pipelines.find({ boardId: board._id }).countDocuments(); + + counts.push({ + _id: board._id, + name: board.name || '', + count, + }); + + allCount += count; + } + + counts.unshift({ _id: '', name: 'All', count: allCount }); + + return counts; + }, + + /** + * Board detail + */ + boardDetail(_root, { _id }: { _id: string }, { commonQuerySelector }: IContext) { + return Boards.findOne({ ...commonQuerySelector, _id }); + }, + + /** + * Get last board + */ + boardGetLast(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Boards.findOne({ ...commonQuerySelector, type }).sort({ createdAt: -1 }); + }, + + /** + * Pipelines list + */ + pipelines( + _root, + { boardId, type, ...queryParams }: { boardId: string; type: string; page: number; perPage: number }, + ) { + const query: any = {}; + const { page, perPage } = queryParams; + + if (boardId) { + query.boardId = boardId; + } + + if (type) { + query.type = type; + } + + if (page && perPage) { + return paginate(Pipelines.find(query).sort({ createdAt: 1 }), queryParams); + } + + return Pipelines.find(query).sort({ order: 1, createdAt: -1 }); + }, + + async pipelineStateCount(_root, { boardId, type }: { boardId: string; type: string }) { + const query: any = {}; + + if (boardId) { + query.boardId = boardId; + } + + if (type) { + query.type = type; + } + + const counts: any = {}; + const now = new Date(); + + const notStartedQuery = { + ...query, + startDate: { $gt: now }, + }; + + const notStartedCount = await Pipelines.find(notStartedQuery).countDocuments(); + + counts['Not started'] = notStartedCount; + + const inProgressQuery = { + ...query, + startDate: { $lt: now }, + endDate: { $gt: now }, + }; + + const inProgressCount = await Pipelines.find(inProgressQuery).countDocuments(); + + counts['In progress'] = inProgressCount; + + const completedQuery = { + ...query, + endDate: { $lt: now }, + }; + + const completedCounted = await Pipelines.find(completedQuery).countDocuments(); + + counts.Completed = completedCounted; + + counts.All = notStartedCount + inProgressCount + completedCounted; + + return counts; + }, + + /** + * Pipeline detail + */ + pipelineDetail(_root, { _id }: { _id: string }) { + return Pipelines.findOne({ _id }); + }, + + /** + * Stages list + */ + stages(_root, { pipelineId, isNotLost, isAll }: { pipelineId: string; isNotLost: boolean; isAll: boolean }) { + const filter: any = {}; + + filter.pipelineId = pipelineId; + + if (isNotLost) { + filter.probability = { $ne: 'Lost' }; + } + + if (!isAll) { + filter.$or = [{ status: null }, { status: BOARD_STATUSES.ACTIVE }]; + } + + return Stages.find(filter).sort({ order: 1, createdAt: -1 }); + }, + + /** + * Stage detail + */ + stageDetail(_root, { _id }: { _id: string }) { + return Stages.findOne({ _id }); + }, + + /** + * Archived stages + */ + + archivedStages( + _root, + { pipelineId, search, ...listArgs }: { pipelineId: string; search?: string; page?: number; perPage?: number }, + ) { + const filter: any = { pipelineId, status: BOARD_STATUSES.ARCHIVED }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return paginate(Stages.find(filter).sort({ createdAt: -1 }), listArgs); + }, + + archivedStagesCount(_root, { pipelineId, search }: { pipelineId: string; search?: string }) { + const filter: any = { pipelineId, status: BOARD_STATUSES.ARCHIVED }; + + if (search) { + Object.assign(filter, regexSearchText(search, 'name')); + } + + return Stages.countDocuments(filter); + }, + + /** + * ConvertTo info + */ + async convertToInfo(_root, { conversationId }: { conversationId: string }) { + const filter = { sourceConversationId: conversationId }; + let dealUrl = ''; + let ticketUrl = ''; + let taskUrl = ''; + + const deal = await Deals.findOne(filter); + + if (deal) { + const stage = await Stages.getStage(deal.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + const board = await Boards.getBoard(pipeline.boardId); + + dealUrl = `/deal/board?_id=${board._id}&pipelineId=${pipeline._id}&itemId=${deal._id}`; + } + + const task = await Tasks.findOne(filter); + + if (task) { + const stage = await Stages.getStage(task.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + const board = await Boards.getBoard(pipeline.boardId); + + taskUrl = `/task/board?_id=${board._id}&pipelineId=${pipeline._id}&itemId=${task._id}`; + } + + const ticket = await Tickets.findOne(filter); + + if (ticket) { + const stage = await Stages.getStage(ticket.stageId); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); + const board = await Boards.getBoard(pipeline.boardId); + + ticketUrl = `/inbox/ticket/board?_id=${board._id}&pipelineId=${pipeline._id}&itemId=${ticket._id}`; + } + + return { + dealUrl, + ticketUrl, + taskUrl, + }; + }, +}; + +moduleRequireLogin(boardQueries); + +export default boardQueries; diff --git a/src/data/resolvers/queries/brands.ts b/src/data/resolvers/queries/brands.ts index 2cb9b70c1..825b66c4c 100644 --- a/src/data/resolvers/queries/brands.ts +++ b/src/data/resolvers/queries/brands.ts @@ -1,42 +1,66 @@ -import { Brands } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const brandQueries = { - /** - * Brands list - */ - brands(_root, args: { page: number; perPage: number }) { - const brands = paginate(Brands.find({}), args); - return brands.sort({ createdAt: -1 }); - }, - - /** - * Get one brand - */ - brandDetail(_root, { _id }: { _id: string }) { - return Brands.findOne({ _id }); - }, - - /** - * Get all brands count. We will use it in pager - */ - brandsTotalCount() { - return Brands.find({}).countDocuments(); - }, - - /** - * Get last brand - */ - brandsGetLast() { - return Brands.findOne({}).sort({ createdAt: -1 }); - }, -}; - -requireLogin(brandQueries, 'brandsTotalCount'); -requireLogin(brandQueries, 'brandsGetLast'); -requireLogin(brandQueries, 'brandDetail'); - -checkPermission(brandQueries, 'brands', 'showBrands', []); - -export default brandQueries; +import { Brands } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { readFile } from '../../utils'; + +interface IListArgs { + page?: number; + perPage?: number; + searchValue?: string; +} + +const queryBuilder = (params: IListArgs, brandIdSelector: any) => { + const selector: any = { ...brandIdSelector }; + + const { searchValue } = params; + + if (searchValue) { + selector.name = new RegExp(`.*${params.searchValue}.*`, 'i'); + } + + return selector; +}; + +const brandQueries = { + /** + * Brands list + */ + brands(_root, args: IListArgs, { brandIdSelector }: IContext) { + const selector = queryBuilder(args, brandIdSelector); + + return Brands.find(selector).sort({ createdAt: -1 }); + }, + + /** + * Get one brand + */ + brandDetail(_root, { _id }: { _id: string }) { + return Brands.findOne({ _id }); + }, + + /** + * Get all brands count. We will use it in pager + */ + brandsTotalCount(_root, _args, { brandIdSelector }: IContext) { + return Brands.find(brandIdSelector).countDocuments(); + }, + + /** + * Get last brand + */ + brandsGetLast() { + return Brands.findOne({}).sort({ createdAt: -1 }); + }, + + brandsGetDefaultEmailConfig() { + return readFile('conversationCron'); + }, +}; + +requireLogin(brandQueries, 'brandsTotalCount'); +requireLogin(brandQueries, 'brandsGetLast'); +requireLogin(brandQueries, 'brandDetail'); + +checkPermission(brandQueries, 'brands', 'showBrands', []); + +export default brandQueries; diff --git a/src/data/resolvers/queries/channels.ts b/src/data/resolvers/queries/channels.ts index 52011f800..9a14f03ca 100644 --- a/src/data/resolvers/queries/channels.ts +++ b/src/data/resolvers/queries/channels.ts @@ -1,58 +1,55 @@ -import { Channels } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface IIn { - $in: string[]; -} - -interface IChannelQuery { - memberIds?: IIn; -} - -const channelQueries = { - /** - * Channels list - */ - channels(_root, { memberIds, ...queryParams }: { page: number; perPage: number; memberIds: string[] }) { - const query: IChannelQuery = {}; - const sort = { createdAt: -1 }; - - if (memberIds) { - query.memberIds = { $in: memberIds }; - } - - const channels = paginate(Channels.find(query), queryParams); - - return channels.sort(sort); - }, - - /** - * Get one channel - */ - channelDetail(_root, { _id }: { _id: string }) { - return Channels.findOne({ _id }); - }, - - /** - * Get all channels count. We will use it in pager - */ - channelsTotalCount() { - return Channels.find({}).countDocuments(); - }, - - /** - * Get last channel - */ - channelsGetLast() { - return Channels.findOne({}).sort({ createdAt: -1 }); - }, -}; - -requireLogin(channelQueries, 'channelsGetLast'); -requireLogin(channelQueries, 'channelsTotalCount'); -requireLogin(channelQueries, 'channelDetail'); - -checkPermission(channelQueries, 'channels', 'showChannels', []); - -export default channelQueries; +import { Channels } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; + +interface IIn { + $in: string[]; +} + +interface IChannelQuery { + memberIds?: IIn; +} + +const channelQueries = { + /** + * Channels list + */ + channels(_root, { memberIds }: { memberIds: string[] }) { + const query: IChannelQuery = {}; + const sort = { createdAt: -1 }; + + if (memberIds) { + query.memberIds = { $in: memberIds }; + } + + return Channels.find(query).sort(sort); + }, + + /** + * Get one channel + */ + channelDetail(_root, { _id }: { _id: string }) { + return Channels.findOne({ _id }); + }, + + /** + * Get all channels count. We will use it in pager + */ + channelsTotalCount() { + return Channels.find({}).countDocuments(); + }, + + /** + * Get last channel + */ + channelsGetLast() { + return Channels.findOne({}).sort({ createdAt: -1 }); + }, +}; + +requireLogin(channelQueries, 'channelsGetLast'); +requireLogin(channelQueries, 'channelsTotalCount'); +requireLogin(channelQueries, 'channelDetail'); + +checkPermission(channelQueries, 'channels', 'showChannels', []); + +export default channelQueries; diff --git a/src/data/resolvers/queries/checklists.ts b/src/data/resolvers/queries/checklists.ts new file mode 100644 index 000000000..189318596 --- /dev/null +++ b/src/data/resolvers/queries/checklists.ts @@ -0,0 +1,25 @@ +import { Checklists } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; + +const checklistQueries = { + /** + * Checklists list + */ + async checklists(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { + return Checklists.find({ contentType, contentTypeId }).sort({ + createdDate: 1, + order: 1, + }); + }, + + /** + * Checklist + */ + async checklistDetail(_root, { _id }: { _id: string }) { + return Checklists.findOne({ _id }).sort({ order: 1 }); + }, +}; + +moduleRequireLogin(checklistQueries); + +export default checklistQueries; diff --git a/src/data/resolvers/queries/companies.ts b/src/data/resolvers/queries/companies.ts index c2c919c60..4b3379374 100644 --- a/src/data/resolvers/queries/companies.ts +++ b/src/data/resolvers/queries/companies.ts @@ -1,157 +1,85 @@ -import { Brands, Companies, Segments, Tags } from '../../../db/models'; -import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; -import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES } from '../../constants'; -import { brandFilter, filter, IListArgs, sortBuilder } from '../../modules/coc/companies'; -import QueryBuilder from '../../modules/segments/queryBuilder'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface ICountArgs extends IListArgs { - only?: string; - byFakeSegment: any; -} - -interface ICountBy { - [index: string]: number; -} - -const count = async (query: any, args: ICountArgs) => { - const selector = await filter(args); - - const findQuery = { ...selector, ...query }; - - return Companies.find(findQuery).countDocuments(); -}; - -const countBySegment = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by segments ========= - const segments = await Segments.find({ - contentType: ACTIVITY_CONTENT_TYPES.COMPANY, - }); - - for (const s of segments) { - counts[s._id] = await count(await QueryBuilder.segments(s), args); - } - - return counts; -}; - -const countByTags = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by tag ========= - const tags = await Tags.find({ type: TAG_TYPES.COMPANY }); - - for (const tag of tags) { - counts[tag._id] = await count({ tagIds: tag._id }, args); - } - - return counts; -}; - -const countByBrands = async (args: ICountArgs): Promise => { - const counts = {}; - - // Count companies by brand ========= - const brands = await Brands.find({}); - - for (const brand of brands) { - counts[brand._id] = await count(await brandFilter(brand._id), args); - } - - return counts; -}; - -const companyQueries = { - /** - * Companies list - */ - async companies(_root, params: IListArgs) { - const selector = await filter(params); - const sort = sortBuilder(params); - - return paginate(Companies.find(selector), params).sort(sort); - }, - - /** - * Companies for only main list - */ - async companiesMain(_root, params: IListArgs) { - const selector = await filter(params); - const sort = sortBuilder(params); - - const list = await paginate(Companies.find(selector).sort(sort), params); - const totalCount = await Companies.find(selector).countDocuments(); - - return { list, totalCount }; - }, - - /** - * Group company counts by segments - */ - async companyCounts(_root, args: ICountArgs) { - const counts = { - bySegment: {}, - byFakeSegment: 0, - byTag: {}, - byBrand: {}, - byLeadStatus: {}, - byLifecycleState: {}, - }; - - const { only } = args; - - switch (only) { - case 'byTag': - counts.byTag = await countByTags(args); - break; - case 'bySegment': - counts.bySegment = await countBySegment(args); - break; - case 'byBrand': - counts.byBrand = await countByBrands(args); - break; - case 'byLeadStatus': - { - // Count companies by lead status ====== - for (const status of COC_LEAD_STATUS_TYPES) { - counts.byLeadStatus[status] = await count({ leadStatus: status }, args); - } - } - break; - case 'byLifecycleState': - { - // Count companies by life cycle state ======= - for (const state of COC_LIFECYCLE_STATE_TYPES) { - counts.byLifecycleState[state] = await count({ lifecycleState: state }, args); - } - } - break; - } - - // Count companies by fake segment - if (args.byFakeSegment) { - counts.byFakeSegment = await count(await QueryBuilder.segments(args.byFakeSegment), args); - } - - return counts; - }, - - /** - * Get one company - */ - companyDetail(_root, { _id }: { _id: string }) { - return Companies.findOne({ _id }); - }, -}; - -requireLogin(companyQueries, 'companiesMain'); -requireLogin(companyQueries, 'companyCounts'); -requireLogin(companyQueries, 'companyDetail'); - -checkPermission(companyQueries, 'companies', 'showCompanies', []); -checkPermission(companyQueries, 'companiesMain', 'showCompanies', { list: [], totalCount: 0 }); - -export default companyQueries; +import { Companies } from '../../../db/models'; +import { TAG_TYPES } from '../../../db/models/definitions/constants'; +import { Builder, IListArgs } from '../../modules/coc/companies'; +import { countByBrand, countBySegment, countByTag } from '../../modules/coc/utils'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface ICountArgs extends IListArgs { + only?: string; +} + +const companyQueries = { + /** + * Companies list + */ + async companies(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const qb = new Builder(params, { commonQuerySelector, commonQuerySelectorElk }); + + await qb.buildAllQueries(); + + const { list } = await qb.runQueries(); + + return list; + }, + + /** + * Companies for only main list + */ + async companiesMain(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const qb = new Builder(params, { commonQuerySelector, commonQuerySelectorElk }); + + await qb.buildAllQueries(); + + const { list, totalCount } = await qb.runQueries(); + + return { list, totalCount }; + }, + + /** + * Group company counts by segments + */ + async companyCounts(_root, args: ICountArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const counts = { + bySegment: {}, + byTag: {}, + byBrand: {}, + byLeadStatus: {}, + }; + + const { only } = args; + + const qb = new Builder(args, { commonQuerySelector, commonQuerySelectorElk }); + + switch (only) { + case 'byTag': + counts.byTag = await countByTag(TAG_TYPES.COMPANY, qb); + break; + + case 'bySegment': + counts.bySegment = await countBySegment('company', qb); + break; + case 'byBrand': + counts.byBrand = await countByBrand(qb); + break; + } + + return counts; + }, + + /** + * Get one company + */ + companyDetail(_root, { _id }: { _id: string }) { + return Companies.findOne({ _id }); + }, +}; + +requireLogin(companyQueries, 'companiesMain'); +requireLogin(companyQueries, 'companyCounts'); +requireLogin(companyQueries, 'companyDetail'); + +checkPermission(companyQueries, 'companies', 'showCompanies', []); +checkPermission(companyQueries, 'companiesMain', 'showCompanies', { list: [], totalCount: 0 }); + +export default companyQueries; diff --git a/src/data/resolvers/queries/configs.ts b/src/data/resolvers/queries/configs.ts index 9bae0e8d6..34ab044e8 100644 --- a/src/data/resolvers/queries/configs.ts +++ b/src/data/resolvers/queries/configs.ts @@ -1,59 +1,94 @@ -import { Configs } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; -import { getEnv, sendRequest } from '../../utils'; - -const configQueries = { - /** - * Config object - */ - configsDetail(_root, { code }: { code: string }) { - return Configs.findOne({ code }); - }, - - async configsVersions(_root) { - const erxesDomain = getEnv({ name: 'MAIN_APP_DOMAIN' }); - const domain = getEnv({ name: 'DOMAIN' }); - const widgetsApiDomain = getEnv({ name: 'WIDGETS_API_DOMAIN' }); - const widgetsDomain = getEnv({ name: 'WIDGETS_DOMAIN' }); - - let erxesVersion; - let apiVersion; - let widgetApiVersion; - let widgetVersion; - - try { - erxesVersion = await sendRequest({ url: `${erxesDomain}/version.json`, method: 'GET' }); - } catch (e) { - erxesVersion = {}; - } - - try { - apiVersion = await sendRequest({ url: `${domain}/static/version.json`, method: 'GET' }); - } catch (e) { - apiVersion = {}; - } - - try { - widgetApiVersion = await sendRequest({ url: `${widgetsApiDomain}/static/version.json`, method: 'GET' }); - } catch (e) { - widgetApiVersion = {}; - } - - try { - widgetVersion = await sendRequest({ url: `${widgetsDomain}/build/version.json`, method: 'GET' }); - } catch (e) { - widgetVersion = {}; - } - - return { - erxesVersion, - apiVersion, - widgetApiVersion, - widgetVersion, - }; - }, -}; - -moduleRequireLogin(configQueries); - -export default configQueries; +import * as mongoose from 'mongoose'; +import * as os from 'os'; +import * as path from 'path'; +import { Configs } from '../../../db/models'; +import { DEFAULT_CONSTANT_VALUES } from '../../../db/models/definitions/constants'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { getEnv, getSubServiceDomain, sendRequest } from '../../utils'; + +const configQueries = { + /** + * Config object + */ + configs(_root) { + return Configs.find({}); + }, + + async configsStatus(_root, _args) { + const status: any = { + erxesApi: {}, + erxesIntegration: {}, + erxes: {}, + }; + + const { version, storageEngine } = await mongoose.connection.db.command({ serverStatus: 1 }); + + status.erxesApi.os = { + type: os.type(), + platform: os.platform(), + arch: os.arch(), + release: os.release(), + uptime: os.uptime(), + loadavg: os.loadavg(), + totalmem: os.totalmem(), + freemem: os.freemem(), + cpuCount: os.cpus().length, + }; + + status.erxesApi.process = { + nodeVersion: process.version, + pid: process.pid, + uptime: process.uptime(), + }; + + status.erxesApi.mongo = { + version, + storageEngine: storageEngine.name, + }; + + const projectPath = process.cwd(); + status.erxesApi.packageVersion = require(path.join(projectPath, 'package.json')).version; + + try { + const erxesDomain = getEnv({ name: 'MAIN_APP_DOMAIN' }); + const erxesVersion = await sendRequest({ url: `${erxesDomain}/version.json`, method: 'GET' }); + + status.erxes.packageVersion = erxesVersion.packageVersion || '-'; + } catch (e) { + status.erxes.packageVersion = '-'; + } + + try { + const erxesIntegrationDomain = getSubServiceDomain({ name: 'INTEGRATIONS_API_DOMAIN' }); + const erxesIntegration = await sendRequest({ + url: `${erxesIntegrationDomain}/system-status`, + method: 'GET', + }); + + status.erxesIntegration = erxesIntegration || '-'; + } catch (e) { + status.erxesIntegration = { + packageVersion: '-', + }; + } + + return status; + }, + + configsGetEnv(_root) { + return { + USE_BRAND_RESTRICTIONS: process.env.USE_BRAND_RESTRICTIONS, + }; + }, + + configsConstants(_root) { + return { + allValues: Configs.constants(), + defaultValues: DEFAULT_CONSTANT_VALUES, + }; + }, +}; + +moduleRequireLogin(configQueries); + +export default configQueries; diff --git a/src/data/resolvers/queries/conversationQueryBuilder.ts b/src/data/resolvers/queries/conversationQueryBuilder.ts index 7dd8642eb..7a8475bef 100644 --- a/src/data/resolvers/queries/conversationQueryBuilder.ts +++ b/src/data/resolvers/queries/conversationQueryBuilder.ts @@ -1,299 +1,311 @@ -import * as _ from 'underscore'; -import { Channels, Integrations } from '../../../db/models'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { fixDate } from '../../utils'; - -interface IIn { - $in: string[]; -} - -interface IExists { - $exists: boolean; -} - -export interface IListArgs { - limit?: number; - channelId?: string; - status?: string; - unassigned?: string; - brandId?: string; - tag?: string; - integrationType?: string; - participating?: string; - starred?: string; - ids?: string[]; - startDate?: string; - endDate?: string; - only?: string; -} - -interface IUserArgs { - _id: string; - starredConversationIds?: string[]; -} - -interface IIntersectIntegrationIds { - integrationId: IIn; -} - -interface IUnassignedFilter { - assignedUserId: IExists; -} - -interface IDateFilter { - createdAt: { - $gte: Date; - $lte: Date; - }; -} - -export default class Builder { - public params: IListArgs; - public user: IUserArgs; - public queries: any; - public unassignedQuery?: IUnassignedFilter; - - constructor(params: IListArgs, user: IUserArgs) { - this.params = params; - this.user = user; - } - - public defaultFilters(): { [index: string]: {} } { - let statusFilter = this.statusFilter([CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN]); - - if (this.params.status === 'closed') { - statusFilter = this.statusFilter([CONVERSATION_STATUSES.CLOSED]); - } - - return { - ...statusFilter, - // exclude engage messages if customer did not reply - $or: [ - { - userId: { $exists: true }, - messageCount: { $gt: 1 }, - }, - { - userId: { $exists: false }, - }, - ], - }; - } - - public intersectIntegrationIds(...queries: any[]): { integrationId: IIn } { - // filter only queries with $in field - const withIn = queries.filter(q => q.integrationId && q.integrationId.$in && q.integrationId.$in.length > 0); - - // [{$in: ['id1', 'id2']}, {$in: ['id3', 'id1', 'id4']}] - const $ins = _.pluck(withIn, 'integrationId'); - - // [['id1', 'id2'], ['id3', 'id1', 'id4']] - const nestedIntegrationIds = _.pluck($ins, '$in'); - - // ['id1'] - const integrationids: any = _.intersection(...nestedIntegrationIds); - - return { - integrationId: { $in: integrationids }, - }; - } - - /* - * find integrationIds from channel && brand - */ - public async integrationsFilter(): Promise { - const channelFilter = { - memberIds: this.user._id, - }; - - // find all posssible integrations - let availIntegrationIds: any = []; - - const channels = await Channels.find(channelFilter); - - channels.forEach(channel => { - availIntegrationIds = _.union(availIntegrationIds, channel.integrationIds || ''); - }); - - const nestedIntegrationIds: any = [{ integrationId: { $in: availIntegrationIds } }]; - - // filter by channel - if (this.params.channelId) { - const channelQuery = await this.channelFilter(this.params.channelId); - nestedIntegrationIds.push(channelQuery); - } - - // filter by brand - if (this.params.brandId) { - const brandQuery = await this.brandFilter(this.params.brandId); - nestedIntegrationIds.push(brandQuery); - } - - return this.intersectIntegrationIds(...nestedIntegrationIds); - } - - // filter by channel - public async channelFilter(channelId: string): Promise<{ integrationId: IIn }> { - const channel = await Channels.findOne({ _id: channelId }); - if (channel && channel.integrationIds) { - return { - integrationId: { $in: channel.integrationIds }, - }; - } else { - return { integrationId: { $in: [] } }; - } - } - - // filter by brand - public async brandFilter(brandId: string): Promise<{ integrationId: IIn }> { - const integrations = await Integrations.find({ brandId }); - const integrationIds = _.pluck(integrations, '_id'); - - return { - integrationId: { $in: integrationIds }, - }; - } - - // filter all unassigned - public unassignedFilter(): IUnassignedFilter { - this.unassignedQuery = { - assignedUserId: { $exists: false }, - }; - - return this.unassignedQuery; - } - - // filter by participating - public participatingFilter(): { participatedUserIds: IIn } { - return { - participatedUserIds: { $in: [this.user._id] }, - }; - } - - // filter by starred - public starredFilter(): { _id: IIn | { $in: any[] } } { - let ids: any = []; - - if (this.user) { - ids = this.user.starredConversationIds || []; - } - - return { - _id: { $in: ids }, - }; - } - - public statusFilter(statusChoices: string[]): { status: IIn } { - return { - status: { $in: statusChoices }, - }; - } - - // filter by integration type - public async integrationTypeFilter(integrationType: string): Promise<{ $and: IIntersectIntegrationIds[] }> { - const integrations = await Integrations.find({ kind: integrationType }); - - return { - $and: [ - // add channel && brand filter - this.queries.integrations, - - // filter by integration type - { integrationId: { $in: _.pluck(integrations, '_id') } }, - ], - }; - } - - // filter by tag - public tagFilter(tagId: string): { tagIds: string[] } { - return { - tagIds: [tagId], - }; - } - - public dateFilter(startDate: string, endDate: string): IDateFilter { - return { - createdAt: { - $gte: fixDate(startDate), - $lte: fixDate(endDate), - }, - }; - } - - /* - * prepare all queries. do not do any action - */ - public async buildAllQueries(): Promise { - this.queries = { - default: this.defaultFilters(), - starred: {}, - status: {}, - unassigned: {}, - tag: {}, - channel: {}, - integrationType: {}, - - // find it using channel && brand - integrations: {}, - - participating: {}, - createdAt: {}, - }; - - // filter by channel - if (this.params.channelId) { - this.queries.channel = await this.channelFilter(this.params.channelId); - } - - // filter by channelId & brandId - this.queries.integrations = await this.integrationsFilter(); - - // unassigned - if (this.params.unassigned) { - this.queries.unassigned = this.unassignedFilter(); - } - - // participating - if (this.params.participating) { - this.queries.participating = this.participatingFilter(); - } - - // starred - if (this.params.starred) { - this.queries.starred = this.starredFilter(); - } - - // filter by status - if (this.params.status) { - this.queries.status = this.statusFilter([this.params.status]); - } - - // filter by tag - if (this.params.tag) { - this.queries.tag = this.tagFilter(this.params.tag); - } - - // filter by integration type - if (this.params.integrationType) { - this.queries.integrationType = await this.integrationTypeFilter(this.params.integrationType); - } - - if (this.params.startDate && this.params.endDate) { - this.queries.createdAt = this.dateFilter(this.params.startDate, this.params.endDate); - } - } - - public mainQuery(): any { - return { - ...this.queries.default, - ...this.queries.integrations, - ...this.queries.integrationType, - ...this.queries.unassigned, - ...this.queries.participating, - ...this.queries.status, - ...this.queries.starred, - ...this.queries.tag, - ...this.queries.createdAt, - }; - } -} +import * as _ from 'underscore'; +import { Channels, Integrations } from '../../../db/models'; +import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; +import { fixDate } from '../../utils'; + +interface IIn { + $in: string[]; +} + +interface IExists { + $exists: boolean; +} + +export interface IListArgs { + limit?: number; + channelId?: string; + status?: string; + unassigned?: string; + awaitingResponse?: string; + brandId?: string; + tag?: string; + integrationType?: string; + participating?: string; + starred?: string; + ids?: string[]; + startDate?: string; + endDate?: string; + only?: string; +} + +interface IUserArgs { + _id: string; + starredConversationIds?: string[]; +} + +interface IIntersectIntegrationIds { + integrationId: IIn; +} + +interface IUnassignedFilter { + assignedUserId: IExists; +} + +interface IDateFilter { + createdAt: { + $gte: Date; + $lte: Date; + }; +} + +export default class Builder { + public params: IListArgs; + public user: IUserArgs; + public queries: any; + public unassignedQuery?: IUnassignedFilter; + public activeIntegrationIds: string[] = []; + + constructor(params: IListArgs, user: IUserArgs) { + this.params = params; + this.user = user; + } + + public async defaultFilters(): Promise { + const activeIntegrations = await Integrations.findIntegrations({}, { _id: 1 }); + this.activeIntegrationIds = activeIntegrations.map(integ => integ._id); + + let statusFilter = this.statusFilter([CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN]); + + if (this.params.status === 'closed') { + statusFilter = this.statusFilter([CONVERSATION_STATUSES.CLOSED]); + } + + return { + ...statusFilter, + // exclude engage messages if customer did not reply + $or: [ + { + userId: { $exists: true }, + messageCount: { $gt: 1 }, + }, + { + userId: { $exists: false }, + }, + ], + }; + } + + public async intersectIntegrationIds(...queries: any[]): Promise<{ integrationId: IIn }> { + // filter only queries with $in field + const withIn = queries.filter(q => q.integrationId && q.integrationId.$in && q.integrationId.$in.length > 0); + + // [{$in: ['id1', 'id2']}, {$in: ['id3', 'id1', 'id4']}] + const $ins = _.pluck(withIn, 'integrationId'); + + // [['id1', 'id2'], ['id3', 'id1', 'id4']] + const nestedIntegrationIds = _.pluck($ins, '$in'); + + // ['id1'] + const integrationids: string[] = _.intersection(...nestedIntegrationIds); + + return { + integrationId: { $in: integrationids }, + }; + } + + /* + * find integrationIds from channel && brand + */ + public async integrationsFilter(): Promise { + // find all posssible integrations + let availIntegrationIds: string[] = []; + + const channels = await Channels.find({ memberIds: this.user._id }); + + channels.forEach(channel => { + availIntegrationIds = _.union( + availIntegrationIds, + (channel.integrationIds || []).filter(id => this.activeIntegrationIds.includes(id)), + ); + }); + + const nestedIntegrationIds: Array<{ integrationId: { $in: string[] } }> = [ + { integrationId: { $in: availIntegrationIds } }, + ]; + + // filter by channel + if (this.params.channelId) { + const channelQuery = await this.channelFilter(this.params.channelId); + nestedIntegrationIds.push(channelQuery); + } + + // filter by brand + if (this.params.brandId) { + const brandQuery = await this.brandFilter(this.params.brandId); + nestedIntegrationIds.push(brandQuery); + } + + return this.intersectIntegrationIds(...nestedIntegrationIds); + } + + // filter by channel + public async channelFilter(channelId: string): Promise<{ integrationId: IIn }> { + const channel = await Channels.getChannel(channelId); + + return { + integrationId: { $in: (channel.integrationIds || []).filter(id => this.activeIntegrationIds.includes(id)) }, + }; + } + + // filter by brand + public async brandFilter(brandId: string): Promise<{ integrationId: IIn }> { + const integrations = await Integrations.findIntegrations({ brandId }); + const integrationIds = _.pluck(integrations, '_id'); + + return { + integrationId: { $in: integrationIds }, + }; + } + + // filter all unassigned + public unassignedFilter(): IUnassignedFilter { + this.unassignedQuery = { + assignedUserId: { $exists: false }, + }; + + return this.unassignedQuery; + } + + // filter by participating + public participatingFilter(): { $or: object[] } { + return { + $or: [{ participatedUserIds: { $in: [this.user._id] } }, { assignedUserId: this.user._id }], + }; + } + + // filter by starred + public starredFilter(): { _id: IIn | { $in: string[] } } { + return { + _id: { + $in: this.user.starredConversationIds || [], + }, + }; + } + + public statusFilter(statusChoices: string[]): { status: IIn } { + return { + status: { $in: statusChoices }, + }; + } + + // filter by awaiting Response + public awaitingResponse(): { isCustomerRespondedLast: boolean } { + return { + isCustomerRespondedLast: true, + }; + } + + // filter by integration type + public async integrationTypeFilter(integrationType: string): Promise<{ $and: IIntersectIntegrationIds[] }> { + const integrations = await Integrations.findIntegrations({ kind: integrationType }); + + return { + $and: [ + // add channel && brand filter + this.queries.integrations, + + // filter by integration type + { integrationId: { $in: _.pluck(integrations, '_id') } }, + ], + }; + } + + // filter by tag + public tagFilter(tagId: string): { tagIds: IIn } { + return { + tagIds: { $in: [tagId] }, + }; + } + + public dateFilter(startDate: string, endDate: string): IDateFilter { + return { + createdAt: { + $gte: fixDate(startDate), + $lte: fixDate(endDate), + }, + }; + } + + /* + * prepare all queries. do not do any action + */ + public async buildAllQueries(): Promise { + this.queries = { + default: await this.defaultFilters(), + starred: {}, + status: {}, + unassigned: {}, + tag: {}, + channel: {}, + integrationType: {}, + + // find it using channel && brand + integrations: {}, + + participating: {}, + createdAt: {}, + }; + + // filter by channel + if (this.params.channelId) { + this.queries.channel = await this.channelFilter(this.params.channelId); + } + + // filter by channelId & brandId + this.queries.integrations = await this.integrationsFilter(); + + // unassigned + if (this.params.unassigned) { + this.queries.unassigned = this.unassignedFilter(); + } + + // participating + if (this.params.participating) { + this.queries.participating = this.participatingFilter(); + } + + // starred + if (this.params.starred) { + this.queries.starred = this.starredFilter(); + } + + // awaiting response + if (this.params.awaitingResponse) { + this.queries.awaitingResponse = this.awaitingResponse(); + } + + // filter by status + if (this.params.status) { + this.queries.status = this.statusFilter([this.params.status]); + } + + // filter by tag + if (this.params.tag) { + this.queries.tag = this.tagFilter(this.params.tag); + } + + // filter by integration type + if (this.params.integrationType) { + this.queries.integrationType = await this.integrationTypeFilter(this.params.integrationType); + } + + if (this.params.startDate && this.params.endDate) { + this.queries.createdAt = this.dateFilter(this.params.startDate, this.params.endDate); + } + } + + public mainQuery(): any { + return { + ...this.queries.default, + ...this.queries.integrations, + ...this.queries.integrationType, + ...this.queries.unassigned, + ...this.queries.participating, + ...this.queries.status, + ...this.queries.starred, + ...this.queries.tag, + ...this.queries.createdAt, + ...this.queries.awaitingResponse, + }; + } +} diff --git a/src/data/resolvers/queries/conversations.ts b/src/data/resolvers/queries/conversations.ts index 32fdd97ce..068c68ffb 100644 --- a/src/data/resolvers/queries/conversations.ts +++ b/src/data/resolvers/queries/conversations.ts @@ -1,284 +1,327 @@ -import { Brands, Channels, ConversationMessages, Conversations, Tags } from '../../../db/models'; -import { CONVERSATION_STATUSES } from '../../../db/models/definitions/constants'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { INTEGRATION_KIND_CHOICES } from '../../constants'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import QueryBuilder, { IListArgs } from './conversationQueryBuilder'; - -interface ICountBy { - [index: string]: number; -} - -interface IConversationRes { - [index: string]: number | ICountBy; -} - -// count helper -const count = async (query: any): Promise => { - const result = await Conversations.find(query).countDocuments(); - - return Number(result); -}; - -const countByChannels = async (qb: any): Promise => { - const byChannels: ICountBy = {}; - const channels = await Channels.find(); - - for (const channel of channels) { - byChannels[channel._id] = await count({ - ...qb.mainQuery(), - ...(await qb.channelFilter(channel._id)), - }); - } - - return byChannels; -}; - -const countByIntegrationTypes = async (qb: any): Promise => { - const byIntegrationTypes: ICountBy = {}; - - for (const intT of INTEGRATION_KIND_CHOICES.ALL) { - byIntegrationTypes[intT] = await count({ - ...qb.mainQuery(), - ...(await qb.integrationTypeFilter(intT)), - }); - } - - return byIntegrationTypes; -}; - -const countByTags = async (qb: any): Promise => { - const byTags: ICountBy = {}; - const queries = qb.queries; - const tags = await Tags.find(); - - for (const tag of tags) { - byTags[tag._id] = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.tagFilter(tag._id), - }); - } - - return byTags; -}; - -const countByBrands = async (qb: any): Promise => { - const byBrands: ICountBy = {}; - const brands = await Brands.find(); - - for (const brand of brands) { - byBrands[brand._id] = await count({ - ...qb.mainQuery(), - ...qb.intersectIntegrationIds(qb.queries.channel, await qb.brandFilter(brand._id)), - }); - } - - return byBrands; -}; - -const conversationQueries = { - /** - * Conversations list - */ - async conversations(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // filter by ids of conversations - if (params && params.ids) { - return Conversations.find({ _id: { $in: params.ids } }).sort({ - createdAt: -1, - }); - } - - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.find(qb.mainQuery()) - .sort({ updatedAt: -1 }) - .limit(params.limit || 0); - }, - - /** - * Get conversation messages - */ - async conversationMessages( - _root, - { - conversationId, - skip, - limit, - }: { - conversationId: string; - skip: number; - limit: number; - }, - ) { - const query = { conversationId }; - - if (limit) { - const messages = await ConversationMessages.find(query) - .sort({ createdAt: -1 }) - .skip(skip || 0) - .limit(limit); - - return messages.reverse(); - } - - return ConversationMessages.find(query) - .sort({ createdAt: 1 }) - .limit(50); - }, - - /** - * Get all conversation messages count. We will use it in pager - */ - async conversationMessagesTotalCount(_root, { conversationId }: { conversationId: string }) { - return ConversationMessages.countDocuments({ conversationId }); - }, - - /** - * Group conversation counts by brands, channels, integrations, status - */ - async conversationCounts(_root, params: IListArgs, { user }: { user: IUserDocument }) { - const { only } = params; - - const response: IConversationRes = {}; - - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - const queries = qb.queries; - - switch (only) { - case 'byChannels': - response.byChannels = await countByChannels(qb); - break; - - case 'byIntegrationTypes': - response.byIntegrationTypes = await countByIntegrationTypes(qb); - break; - - case 'byBrands': - response.byBrands = await countByBrands(qb); - break; - - case 'byTags': - response.byTags = await countByTags(qb); - break; - } - - // unassigned count - response.unassigned = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.unassignedFilter(), - }); - - // participating count - response.participating = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.participatingFilter(), - }); - - // starred count - response.starred = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.starredFilter(), - }); - - // resolved count - response.resolved = await count({ - ...qb.mainQuery(), - ...queries.integrations, - ...queries.integrationType, - ...qb.statusFilter(['closed']), - }); - - return response; - }, - - /** - * Get one conversation - */ - conversationDetail(_root, { _id }: { _id: string }) { - return Conversations.findOne({ _id }); - }, - - /** - * Get all conversations count. We will use it in pager - */ - async conversationsTotalCount(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.find(qb.mainQuery()).countDocuments(); - }, - - /** - * Get last conversation - */ - async conversationsGetLast(_root, params: IListArgs, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder(params, { - _id: user._id, - starredConversationIds: user.starredConversationIds, - }); - - await qb.buildAllQueries(); - - return Conversations.findOne(qb.mainQuery()).sort({ updatedAt: -1 }); - }, - - /** - * Get all unread conversations for logged in user - */ - async conversationsTotalUnreadCount(_root, _args, { user }: { user: IUserDocument }) { - // initiate query builder - const qb = new QueryBuilder({}, { _id: user._id }); - - // get all possible integration ids - const integrationsFilter = await qb.integrationsFilter(); - - return Conversations.find({ - ...integrationsFilter, - status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, - readUserIds: { $ne: user._id }, - - // exclude engage messages if customer did not reply - $or: [ - { - userId: { $exists: true }, - messageCount: { $gt: 1 }, - }, - { - userId: { $exists: false }, - }, - ], - }).countDocuments(); - }, -}; - -moduleRequireLogin(conversationQueries); - -checkPermission(conversationQueries, 'conversations', 'showConversations', []); - -export default conversationQueries; +import { Brands, Channels, ConversationMessages, Conversations, Tags } from '../../../db/models'; +import { CONVERSATION_STATUSES, KIND_CHOICES } from '../../../db/models/definitions/constants'; +import { IMessageDocument } from '../../../db/models/definitions/conversationMessages'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import QueryBuilder, { IListArgs } from './conversationQueryBuilder'; + +interface ICountBy { + [index: string]: number; +} + +interface IConversationRes { + [index: string]: number | ICountBy; +} + +// count helper +const count = async (query: any): Promise => { + const result = await Conversations.find(query).countDocuments(); + + return Number(result); +}; + +const countByChannels = async (qb: any): Promise => { + const byChannels: ICountBy = {}; + const channels = await Channels.find(); + + for (const channel of channels) { + byChannels[channel._id] = await count({ + ...qb.mainQuery(), + ...(await qb.channelFilter(channel._id)), + }); + } + + return byChannels; +}; + +const countByIntegrationTypes = async (qb: any): Promise => { + const byIntegrationTypes: ICountBy = {}; + + for (const intT of KIND_CHOICES.ALL) { + byIntegrationTypes[intT] = await count({ + ...qb.mainQuery(), + ...(await qb.integrationTypeFilter(intT)), + }); + } + + return byIntegrationTypes; +}; + +const countByTags = async (qb: any): Promise => { + const byTags: ICountBy = {}; + const queries = qb.queries; + const tags = await Tags.find(); + + for (const tag of tags) { + byTags[tag._id] = await count({ + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + ...qb.tagFilter(tag._id), + }); + } + + return byTags; +}; + +const countByBrands = async (qb: any): Promise => { + const byBrands: ICountBy = {}; + const brands = await Brands.find(); + + for (const brand of brands) { + byBrands[brand._id] = await count({ + ...qb.mainQuery(), + ...(await qb.intersectIntegrationIds(qb.queries.channel, await qb.brandFilter(brand._id))), + }); + } + + return byBrands; +}; + +const conversationQueries = { + /** + * Conversations list + */ + async conversations(_root, params: IListArgs, { user }: IContext) { + // filter by ids of conversations + if (params && params.ids) { + return Conversations.find({ _id: { $in: params.ids } }).sort({ + createdAt: -1, + }); + } + + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.find(qb.mainQuery()) + .sort({ updatedAt: -1 }) + .limit(params.limit || 0); + }, + + /** + * Get conversation messages + */ + async conversationMessages( + _root, + { + conversationId, + skip, + limit, + getFirst, + }: { + conversationId: string; + skip: number; + limit: number; + getFirst: boolean; + }, + ) { + const query = { conversationId }; + + let messages: IMessageDocument[] = []; + + if (limit) { + const sort = getFirst ? { createdAt: 1 } : { createdAt: -1 }; + + messages = await ConversationMessages.find(query) + .sort(sort) + .skip(skip || 0) + .limit(limit); + + return getFirst ? messages : messages.reverse(); + } + + messages = await ConversationMessages.find(query) + .sort({ createdAt: -1 }) + .limit(50); + + return messages.reverse(); + }, + + /** + * Get all conversation messages count. We will use it in pager + */ + async conversationMessagesTotalCount(_root, { conversationId }: { conversationId: string }) { + return ConversationMessages.countDocuments({ conversationId }); + }, + + async converstationFacebookComments( + _root, + { + postId, + isResolved, + commentId, + limit, + senderId, + }: { commentId: string; isResolved: string; postId: string; senderId: string; limit: number }, + { dataSources }: IContext, + ) { + return dataSources.IntegrationsAPI.fetchApi('/facebook/get-comments', { + postId, + isResolved, + commentId, + senderId, + limit: limit || 10, + }); + }, + + async converstationFacebookCommentsCount( + _root, + { postId, isResolved }: { postId: string; isResolved: string }, + { dataSources }: IContext, + ) { + return dataSources.IntegrationsAPI.fetchApi('/facebook/get-comments-count', { + postId, + isResolved, + }); + }, + /** + * Group conversation counts by brands, channels, integrations, status + */ + async conversationCounts(_root, params: IListArgs, { user }: IContext) { + const { only } = params; + + const response: IConversationRes = {}; + + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + const queries = qb.queries; + + switch (only) { + case 'byChannels': + response.byChannels = await countByChannels(qb); + break; + + case 'byIntegrationTypes': + response.byIntegrationTypes = await countByIntegrationTypes(qb); + break; + + case 'byBrands': + response.byBrands = await countByBrands(qb); + break; + + case 'byTags': + response.byTags = await countByTags(qb); + break; + } + + const mainQuery = { + ...qb.mainQuery(), + ...queries.integrations, + ...queries.integrationType, + }; + + // unassigned count + response.unassigned = await count({ + ...mainQuery, + ...qb.unassignedFilter(), + }); + + // participating count + response.participating = await count({ + ...mainQuery, + ...qb.participatingFilter(), + }); + + // starred count + response.starred = await count({ + ...mainQuery, + ...qb.starredFilter(), + }); + + // resolved count + response.resolved = await count({ + ...mainQuery, + ...qb.statusFilter(['closed']), + }); + + // awaiting response count + response.awaitingResponse = await count({ + ...mainQuery, + ...qb.awaitingResponse(), + }); + + return response; + }, + + /** + * Get one conversation + */ + conversationDetail(_root, { _id }: { _id: string }) { + return Conversations.findOne({ _id }); + }, + + /** + * Get all conversations count. We will use it in pager + */ + async conversationsTotalCount(_root, params: IListArgs, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.find(qb.mainQuery()).countDocuments(); + }, + + /** + * Get last conversation + */ + async conversationsGetLast(_root, params: IListArgs, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder(params, { + _id: user._id, + starredConversationIds: user.starredConversationIds, + }); + + await qb.buildAllQueries(); + + return Conversations.findOne(qb.mainQuery()).sort({ updatedAt: -1 }); + }, + + /** + * Get all unread conversations for logged in user + */ + async conversationsTotalUnreadCount(_root, _args, { user }: IContext) { + // initiate query builder + const qb = new QueryBuilder({}, { _id: user._id }); + await qb.buildAllQueries(); + + // get all possible integration ids + const integrationsFilter = await qb.integrationsFilter(); + + return Conversations.find({ + ...integrationsFilter, + status: { $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN] }, + readUserIds: { $ne: user._id }, + + // exclude engage messages if customer did not reply + $or: [ + { + userId: { $exists: true }, + messageCount: { $gt: 1 }, + }, + { + userId: { $exists: false }, + }, + ], + }).countDocuments(); + }, +}; + +moduleRequireLogin(conversationQueries); + +checkPermission(conversationQueries, 'conversations', 'showConversations', []); + +export default conversationQueries; diff --git a/src/data/resolvers/queries/customers.ts b/src/data/resolvers/queries/customers.ts index 45beba738..e61079126 100644 --- a/src/data/resolvers/queries/customers.ts +++ b/src/data/resolvers/queries/customers.ts @@ -1,227 +1,127 @@ -import { Brands, Customers, Forms, Segments, Tags } from '../../../db/models'; -import { ACTIVITY_CONTENT_TYPES, TAG_TYPES } from '../../../db/models/definitions/constants'; -import { ISegment } from '../../../db/models/definitions/segments'; -import { COC_LEAD_STATUS_TYPES, COC_LIFECYCLE_STATE_TYPES, INTEGRATION_KIND_CHOICES } from '../../constants'; -import { Builder as BuildQuery, IListArgs, sortBuilder } from '../../modules/coc/customers'; -import QueryBuilder from '../../modules/segments/queryBuilder'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface ICountBy { - [index: string]: number; -} - -interface ICountParams extends IListArgs { - only: string; -} - -const count = (query, mainQuery) => { - const findQuery = { $and: [mainQuery, query] }; - - return Customers.find(findQuery).countDocuments(); -}; - -const countBySegment = async (qb: any, mainQuery: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by segments - const segments = await Segments.find({ - contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, - }); - - // Count customers by segment - for (const s of segments) { - try { - counts[s._id] = await count(await qb.segmentFilter(s._id), mainQuery); - } catch (e) { - // catch mongo error - if (e.name === 'CastError') { - counts[s._id] = 0; - } else { - throw new Error(e); - } - } - } - - return counts; -}; - -const countByBrand = async (qb: any, mainQuery: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by brand - const brands = await Brands.find({}); - - for (const brand of brands) { - counts[brand._id] = await count(await qb.brandFilter(brand._id), mainQuery); - } - - return counts; -}; - -const countByTag = async (qb: any, mainQuery: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by tag - const tags = await Tags.find({ type: TAG_TYPES.CUSTOMER }); - - for (const tag of tags) { - counts[tag._id] = await count(qb.tagFilter(tag._id), mainQuery); - } - - return counts; -}; - -const countByForm = async (qb: any, mainQuery: any, params: any): Promise => { - const counts: ICountBy = {}; - - // Count customers by submitted form - const forms = await Forms.find({}); - - for (const form of forms) { - counts[form._id] = await count(await qb.formFilter(form._id, params.startDate, params.endDate), mainQuery); - } - - return counts; -}; - -const customerQueries = { - /** - * Customers list - */ - async customers(_root, params: IListArgs) { - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - const sort = sortBuilder(params); - - return paginate(Customers.find(qb.mainQuery()).sort(sort), params); - }, - - /** - * Customers for only main list - */ - async customersMain(_root, params: IListArgs) { - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - const sort = sortBuilder(params); - - const list = await paginate(Customers.find(qb.mainQuery()).sort(sort), params); - const totalCount = await Customers.find(qb.mainQuery()).countDocuments(); - - return { list, totalCount }; - }, - - /** - * Group customer counts by brands, segments, integrations, tags - */ - async customerCounts(_root, params: ICountParams) { - const { only } = params; - - const counts = { - bySegment: {}, - byBrand: {}, - byIntegrationType: {}, - byTag: {}, - byFakeSegment: 0, - byForm: {}, - byLeadStatus: {}, - byLifecycleState: {}, - }; - - const qb = new BuildQuery(params); - - await qb.buildAllQueries(); - - let mainQuery = qb.mainQuery(); - - // if passed at least one filter other than perPage - // then find all filtered customers then add subsequent filter to it - if (Object.keys(params).length > 1) { - const customers = await Customers.find(qb.mainQuery(), { _id: 1 }); - const customerIds = customers.map(customer => customer._id); - - mainQuery = { _id: { $in: customerIds } }; - } - - switch (only) { - case 'bySegment': - counts.bySegment = await countBySegment(qb, mainQuery); - break; - - case 'byBrand': - counts.byBrand = await countByBrand(qb, mainQuery); - break; - - case 'byTag': - counts.byTag = await countByTag(qb, mainQuery); - break; - - case 'byForm': - counts.byForm = await countByForm(qb, mainQuery, params); - break; - case 'byLeadStatus': - { - for (const status of COC_LEAD_STATUS_TYPES) { - counts.byLeadStatus[status] = await count(qb.leadStatusFilter(status), mainQuery); - } - } - break; - - case 'byLifecycleState': - { - for (const state of COC_LIFECYCLE_STATE_TYPES) { - counts.byLifecycleState[state] = await count(qb.lifecycleStateFilter(state), mainQuery); - } - } - break; - - case 'byIntegrationType': - { - for (const kind of INTEGRATION_KIND_CHOICES.ALL) { - counts.byIntegrationType[kind] = await count(await qb.integrationTypeFilter(kind), mainQuery); - } - } - break; - } - - // Count customers by fake segment - if (params.byFakeSegment) { - counts.byFakeSegment = await count(await QueryBuilder.segments(params.byFakeSegment), mainQuery); - } - - return counts; - }, - - /** - * Publishes customers list for the preview - * when creating/editing a customer segment - */ - async customerListForSegmentPreview(_root, { segment, limit }: { segment: ISegment; limit: number }) { - const headSegment = await Segments.findOne({ _id: segment.subOf }); - - const query = await QueryBuilder.segments(segment, headSegment); - const sort = { 'messengerData.lastSeenAt': -1 }; - - return Customers.find(query) - .sort(sort) - .limit(limit); - }, - - /** - * Get one customer - */ - customerDetail(_root, { _id }: { _id: string }) { - return Customers.findOne({ _id }); - }, -}; - -moduleRequireLogin(customerQueries); - -checkPermission(customerQueries, 'customers', 'showCustomers', []); -checkPermission(customerQueries, 'customersMain', 'showCustomers', { list: [], totalCount: 0 }); - -export default customerQueries; +import { Customers, Forms } from '../../../db/models'; +import { KIND_CHOICES, TAG_TYPES } from '../../../db/models/definitions/constants'; +import { Builder as BuildQuery, IListArgs } from '../../modules/coc/customers'; +import { countByBrand, countByLeadStatus, countBySegment, countByTag, ICountBy } from '../../modules/coc/utils'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface ICountParams extends IListArgs { + only: string; +} + +const countByIntegrationType = async (qb): Promise => { + const counts: ICountBy = {}; + + for (const type of KIND_CHOICES.ALL) { + await qb.buildAllQueries(); + await qb.integrationTypeFilter(type); + + counts[type] = await qb.runQueries('count'); + } + + return counts; +}; + +const countByForm = async (qb: any, params: any): Promise => { + const counts: ICountBy = {}; + + // Count customers by submitted form + const forms = await Forms.find({}); + + for (const form of forms) { + await qb.buildAllQueries(); + await qb.formFilter(form._id, params); + + counts[form._id] = await qb.runQueries('count'); + } + + return counts; +}; + +const customerQueries = { + /** + * Customers list + */ + async customers(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + + await qb.buildAllQueries(); + + const { list } = await qb.runQueries(); + + return list; + }, + + /** + * Customers for only main list + */ + async customersMain(_root, params: IListArgs, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + + await qb.buildAllQueries(); + + const { list, totalCount } = await qb.runQueries(); + + return { list, totalCount }; + }, + + /** + * Group customer counts by brands, segments, integrations, tags + */ + async customerCounts(_root, params: ICountParams, { commonQuerySelector, commonQuerySelectorElk }: IContext) { + const { only, type } = params; + + const counts = { + bySegment: {}, + byBrand: {}, + byIntegrationType: {}, + byTag: {}, + byForm: {}, + byLeadStatus: {}, + }; + + const qb = new BuildQuery(params, { commonQuerySelector, commonQuerySelectorElk }); + + switch (only) { + case 'bySegment': + counts.bySegment = await countBySegment(type || 'customer', qb); + break; + + case 'byBrand': + counts.byBrand = await countByBrand(qb); + break; + + case 'byTag': + counts.byTag = await countByTag(TAG_TYPES.CUSTOMER, qb); + break; + + case 'byForm': + counts.byForm = await countByForm(qb, params); + break; + + case 'byLeadStatus': + counts.byLeadStatus = await countByLeadStatus(qb); + break; + + case 'byIntegrationType': + counts.byIntegrationType = await countByIntegrationType(qb); + break; + } + + return counts; + }, + + /** + * Get one customer + */ + customerDetail(_root, { _id }: { _id: string }) { + return Customers.findOne({ _id }); + }, +}; + +moduleRequireLogin(customerQueries); + +checkPermission(customerQueries, 'customers', 'showCustomers', []); +checkPermission(customerQueries, 'customersMain', 'showCustomers', { list: [], totalCount: 0 }); + +export default customerQueries; diff --git a/src/data/resolvers/queries/dealInsights.ts b/src/data/resolvers/queries/dealInsights.ts index fc69b1852..aafb9cbb0 100644 --- a/src/data/resolvers/queries/dealInsights.ts +++ b/src/data/resolvers/queries/dealInsights.ts @@ -1,5 +1,4 @@ import { Deals } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { INSIGHT_TYPES } from '../../constants'; import { getDateFieldAsStr } from '../../modules/insights/aggregationUtils'; import { IDealListArgs } from '../../modules/insights/types'; @@ -13,12 +12,13 @@ import { getTimezone, } from '../../modules/insights/utils'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const dealInsightQueries = { /** * Counts deals by each hours in each days. */ - async dealInsightsPunchCard(_root, args: IDealListArgs, { user }: { user: IUserDocument }) { + async dealInsightsPunchCard(_root, args: IDealListArgs, { user }: IContext) { const selector = await getDealSelector(args); return generatePunchData(Deals, selector, user); @@ -57,7 +57,7 @@ const dealInsightQueries = { /** * Calculates won or lost deals for each team members. */ - async dealInsightsByTeamMember(_root, args: IDealListArgs, { user }: { user: IUserDocument }) { + async dealInsightsByTeamMember(_root, args: IDealListArgs, { user }: IContext) { const dealMatch = await getDealSelector(args); const insightAggregateData = await Deals.aggregate([ diff --git a/src/data/resolvers/queries/deals.ts b/src/data/resolvers/queries/deals.ts index 33ddd07c8..d047098d6 100644 --- a/src/data/resolvers/queries/deals.ts +++ b/src/data/resolvers/queries/deals.ts @@ -1,71 +1,141 @@ -import { Deals } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateDealCommonFilters } from './boardUtils'; - -interface IDealListParams extends IListParams { - productIds?: [string]; -} - -const dealQueries = { - /** - * Deals list - */ - async deals(_root, args: IDealListParams) { - const filter = await generateDealCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Deals.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Deal total amounts - */ - async dealsTotalAmounts(_root, args: IDealListParams) { - const filter = await generateDealCommonFilters(args); - - const dealCount = await Deals.find(filter).countDocuments(); - const amountList = await Deals.aggregate([ - { - $match: filter, - }, - { - $unwind: '$productsData', - }, - { - $project: { - amount: '$productsData.amount', - currency: '$productsData.currency', - }, - }, - { - $group: { - _id: '$currency', - amount: { $sum: '$amount' }, - }, - }, - ]); - - const dealAmounts = amountList.map(deal => { - return { _id: Math.random(), currency: deal._id, amount: deal.amount }; - }); - - return { _id: Math.random(), dealCount, dealAmounts }; - }, - - /** - * Deal detail - */ - dealDetail(_root, { _id }: { _id: string }) { - return Deals.findOne({ _id }); - }, -}; - -moduleRequireLogin(dealQueries); - -checkPermission(dealQueries, 'deals', 'showDeals', []); - -export default dealQueries; +import { Deals } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { IListParams } from './boards'; +import { + archivedItems, + archivedItemsCount, + checkItemPermByUser, + generateDealCommonFilters, + generateSort, + IArchiveArgs, +} from './boardUtils'; + +interface IDealListParams extends IListParams { + productIds?: [string]; +} + +const dealQueries = { + /** + * Deals list + */ + async deals(_root, args: IDealListParams, { user, commonQuerySelector }: IContext) { + const filter = { ...commonQuerySelector, ...(await generateDealCommonFilters(user._id, args)) }; + const sort = generateSort(args); + + return Deals.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Archived list + */ + archivedDeals(_root, args: IArchiveArgs) { + return archivedItems(args, Deals); + }, + + archivedDealsCount(_root, args: IArchiveArgs) { + return archivedItemsCount(args, Deals); + }, + + /** + * Deal total amounts + */ + async dealsTotalAmounts(_root, args: IDealListParams, { user }: IContext) { + const filter = await generateDealCommonFilters(user._id, args); + + const dealCount = await Deals.find(filter).countDocuments(); + const amountList = await Deals.aggregate([ + { + $match: filter, + }, + { + $lookup: { + from: 'stages', + let: { letStageId: '$stageId' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$_id', '$$letStageId'], + }, + }, + }, + { + $project: { + probability: { + $cond: { + if: { + $or: [{ $eq: ['$probability', 'Won'] }, { $eq: ['$probability', 'Lost'] }], + }, + then: '$probability', + else: 'In progress', + }, + }, + }, + }, + ], + as: 'stageProbability', + }, + }, + { + $unwind: '$productsData', + }, + { + $unwind: '$stageProbability', + }, + { + $project: { + amount: '$productsData.amount', + currency: '$productsData.currency', + type: '$stageProbability.probability', + tickUsed: '$productsData.tickUsed', + }, + }, + { + $match: { tickUsed: true }, + }, + { + $group: { + _id: { currency: '$currency', type: '$type' }, + + amount: { $sum: '$amount' }, + }, + }, + { + $group: { + _id: '$_id.type', + currencies: { + $push: { amount: '$amount', name: '$_id.currency' }, + }, + }, + }, + { + $sort: { _id: -1 }, + }, + ]); + + const totalForType = amountList.map(type => { + return { _id: Math.random(), name: type._id, currencies: type.currencies }; + }); + + return { _id: Math.random(), dealCount, totalForType }; + }, + + /** + * Deal detail + */ + async dealDetail(_root, { _id }: { _id: string }, { user }: IContext) { + const deal = await Deals.getDeal(_id); + + return checkItemPermByUser(user._id, deal); + }, +}; + +moduleRequireLogin(dealQueries); + +checkPermission(dealQueries, 'deals', 'showDeals', []); + +export default dealQueries; diff --git a/src/data/resolvers/queries/emailDelivery.ts b/src/data/resolvers/queries/emailDelivery.ts new file mode 100644 index 000000000..b58688fa3 --- /dev/null +++ b/src/data/resolvers/queries/emailDelivery.ts @@ -0,0 +1,36 @@ +import { EmailDeliveries } from '../../../db/models'; +import { requireLogin } from '../../permissions/wrappers'; +import { paginate } from '../../utils'; + +const emailDeliveryQueries = { + emailDeliveryDetail(_root, { _id }: { _id: string }) { + return EmailDeliveries.findOne({ _id }); + }, + + async transactionEmailDeliveries( + _root, + { searchValue, ...params }: { searchValue: string; page: number; perPage: number }, + ) { + const selector: any = { kind: 'transaction' }; + + if (searchValue) { + selector.$or = [ + { from: { $regex: new RegExp(searchValue) } }, + { subject: { $regex: new RegExp(searchValue) } }, + { to: { $regex: new RegExp(searchValue) } }, + ]; + } + + const totalCount = await EmailDeliveries.countDocuments(selector); + + return { + list: paginate(EmailDeliveries.find(selector), params).sort({ createdAt: -1 }), + totalCount, + }; + }, +}; + +requireLogin(emailDeliveryQueries, 'emailDeliveryDetail'); +requireLogin(emailDeliveryQueries, 'transactionEmailDeliveries'); + +export default emailDeliveryQueries; diff --git a/src/data/resolvers/queries/emailTemplates.ts b/src/data/resolvers/queries/emailTemplates.ts index 4a4a5ae9c..fc91e71aa 100644 --- a/src/data/resolvers/queries/emailTemplates.ts +++ b/src/data/resolvers/queries/emailTemplates.ts @@ -1,24 +1,25 @@ -import { EmailTemplates } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const emailTemplateQueries = { - /** - * Email templates list - */ - emailTemplates(_root, args: { page: number; perPage: number }) { - return paginate(EmailTemplates.find({}), args); - }, - - /** - * Get all email templates count. We will use it in pager - */ - emailTemplatesTotalCount() { - return EmailTemplates.find({}).countDocuments(); - }, -}; - -requireLogin(emailTemplateQueries, 'emailTemplatesTotalCount'); -checkPermission(emailTemplateQueries, 'emailTemplates', 'showEmailTemplates', []); - -export default emailTemplateQueries; +import { EmailTemplates } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +const emailTemplateQueries = { + /** + * Email templates list + */ + emailTemplates(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + return paginate(EmailTemplates.find(commonQuerySelector), args); + }, + + /** + * Get all email templates count. We will use it in pager + */ + emailTemplatesTotalCount() { + return EmailTemplates.find({}).countDocuments(); + }, +}; + +requireLogin(emailTemplateQueries, 'emailTemplatesTotalCount'); +checkPermission(emailTemplateQueries, 'emailTemplates', 'showEmailTemplates', []); + +export default emailTemplateQueries; diff --git a/src/data/resolvers/queries/engages.ts b/src/data/resolvers/queries/engages.ts index d3b6430b9..b3d51e228 100644 --- a/src/data/resolvers/queries/engages.ts +++ b/src/data/resolvers/queries/engages.ts @@ -1,210 +1,234 @@ -import { EngageMessages, Tags } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -interface IListArgs { - kind?: string; - status?: string; - tag?: string; - ids?: string[]; - brandIds?: string[]; - segmentIds?: string[]; - tagIds?: string[]; - page?: number; - perPage?: number; -} - -interface IQuery { - kind?: string; -} - -interface IStatusQueryBuilder { - [index: string]: boolean | string; -} - -interface ICountsByStatus { - [index: string]: number; -} - -interface ICountsByTag { - [index: string]: number; -} - -// basic count helper -const count = async (selector: {}): Promise => { - const res = await EngageMessages.find(selector).countDocuments(); - return Number(res); -}; - -// Tag query builder -const tagQueryBuilder = (tagId: string) => ({ tagIds: tagId }); - -// status query builder -const statusQueryBuilder = (status: string, user?: IUserDocument): IStatusQueryBuilder => { - if (status === 'live') { - return { isLive: true }; - } - - if (status === 'draft') { - return { isDraft: true }; - } - - if (status === 'paused') { - return { isLive: false }; - } - - if (status === 'yours' && user) { - return { fromUserId: user._id }; - } - - return {}; -}; - -// count for each kind -const countsByKind = async () => ({ - all: await count({}), - auto: await count({ kind: 'auto' }), - visitorAuto: await count({ kind: 'visitorAuto' }), - manual: await count({ kind: 'manual' }), -}); - -// count for each status type -const countsByStatus = async ({ kind, user }: { kind: string; user: IUserDocument }): Promise => { - const query: IQuery = {}; - - if (kind) { - query.kind = kind; - } - - return { - live: await count({ ...query, ...statusQueryBuilder('live') }), - draft: await count({ ...query, ...statusQueryBuilder('draft') }), - paused: await count({ ...query, ...statusQueryBuilder('paused') }), - yours: await count({ ...query, ...statusQueryBuilder('yours', user) }), - }; -}; - -// cout for each tag -const countsByTag = async ({ - kind, - status, - user, -}: { - kind: string; - status: string; - user: IUserDocument; -}): Promise => { - let query: any = {}; - - if (kind) { - query.kind = kind; - } - - if (status) { - query = { ...query, ...statusQueryBuilder(status, user) }; - } - - const tags = await Tags.find({ type: 'engageMessage' }); - - // const response: {[name: string]: number} = {}; - const response: ICountsByTag[] = []; - - for (const tag of tags) { - response[tag._id] = await count({ ...query, ...tagQueryBuilder(tag._id) }); - } - - return response; -}; - -/* - * List filter - */ -const listQuery = ({ segmentIds, brandIds, tagIds, kind, status, tag, ids }: IListArgs, user: IUserDocument): any => { - if (ids) { - return EngageMessages.find({ _id: { $in: ids } }); - } - - if (segmentIds) { - return EngageMessages.find({ segmentIds: { $in: segmentIds } }); - } - - if (brandIds) { - return EngageMessages.find({ brandIds: { $in: brandIds } }); - } - - if (tagIds) { - return EngageMessages.find({ tagIds: { $in: tagIds } }); - } - - let query: any = {}; - - // filter by kind - if (kind) { - query.kind = kind; - } - - // filter by status - if (status) { - query = { ...query, ...statusQueryBuilder(status, user) }; - } - - // filter by tag - if (tag) { - query = { ...query, ...tagQueryBuilder(tag) }; - } - - return query; -}; - -const engageQueries = { - /** - * Group engage messages counts by kind, status, tag - */ - engageMessageCounts( - _root, - { name, kind, status }: { name: string; kind: string; status: string }, - { user }: { user: IUserDocument }, - ) { - if (name === 'kind') { - return countsByKind(); - } - - if (name === 'status') { - return countsByStatus({ kind, user }); - } - - if (name === 'tag') { - return countsByTag({ kind, status, user }); - } - }, - - /** - * Engage messages list - */ - engageMessages(_root, args: IListArgs, { user }: { user: IUserDocument }) { - return paginate(EngageMessages.find(listQuery(args, user)), args); - }, - - /** - * Get one message - */ - engageMessageDetail(_root, { _id }: { _id: string }) { - return EngageMessages.findOne({ _id }); - }, - - /** - * Get all messages count. We will use it in pager - */ - engageMessagesTotalCount(_root, args: IListArgs, { user }: { user: IUserDocument }) { - return EngageMessages.find(listQuery(args, user)).countDocuments(); - }, -}; - -requireLogin(engageQueries, 'engageMessagesTotalCount'); -requireLogin(engageQueries, 'engageMessageCounts'); -requireLogin(engageQueries, 'engageMessageDetail'); - -checkPermission(engageQueries, 'engageMessages', 'showEngagesMessages', []); - -export default engageQueries; +import { EngageMessages, Tags } from '../../../db/models'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +interface IListArgs { + kind?: string; + status?: string; + tag?: string; + ids?: string[]; + brandIds?: string[]; + segmentIds?: string[]; + tagIds?: string[]; + page?: number; + perPage?: number; +} + +interface IQuery { + kind?: string; +} + +interface IStatusQueryBuilder { + [index: string]: boolean | string; +} + +interface ICountsByStatus { + [index: string]: number; +} + +interface ICountsByTag { + [index: string]: number; +} + +// basic count helper +const count = async (selector: {}): Promise => { + const res = await EngageMessages.find(selector).countDocuments(); + return Number(res); +}; + +// Tag query builder +const tagQueryBuilder = (tagId: string) => ({ tagIds: tagId }); + +// status query builder +const statusQueryBuilder = (status: string, user?: IUserDocument): IStatusQueryBuilder | undefined => { + if (status === 'live') { + return { isLive: true }; + } + + if (status === 'draft') { + return { isDraft: true }; + } + + if (status === 'yours' && user) { + return { fromUserId: user._id }; + } + + // status is 'paused' + return { isLive: false }; +}; + +// count for each kind +const countsByKind = async commonSelector => ({ + all: await count(commonSelector), + auto: await count({ ...commonSelector, kind: 'auto' }), + visitorAuto: await count({ ...commonSelector, kind: 'visitorAuto' }), + manual: await count({ ...commonSelector, kind: 'manual' }), +}); + +// count for each status type +const countsByStatus = async ( + commonSelector, + { kind, user }: { kind: string; user: IUserDocument }, +): Promise => { + const query: IQuery = commonSelector; + + if (kind) { + query.kind = kind; + } + + return { + live: await count({ ...query, ...statusQueryBuilder('live') }), + draft: await count({ ...query, ...statusQueryBuilder('draft') }), + paused: await count({ ...query, ...statusQueryBuilder('paused') }), + yours: await count({ ...query, ...statusQueryBuilder('yours', user) }), + }; +}; + +// cout for each tag +const countsByTag = async ( + commonSelector, + { + kind, + status, + user, + }: { + kind: string; + status: string; + user: IUserDocument; + }, +): Promise => { + let query: any = commonSelector; + + if (kind) { + query.kind = kind; + } + + if (status) { + query = { ...query, ...statusQueryBuilder(status, user) }; + } + + const tags = await Tags.find({ type: 'engageMessage' }); + + // const response: {[name: string]: number} = {}; + const response: ICountsByTag[] = []; + + for (const tag of tags) { + response[tag._id] = await count({ ...query, ...tagQueryBuilder(tag._id) }); + } + + return response; +}; + +/* + * List filter + */ +const listQuery = ( + commonSelector, + { segmentIds, brandIds, tagIds, kind, status, tag, ids }: IListArgs, + user: IUserDocument, +): any => { + if (ids) { + return EngageMessages.find({ ...commonSelector, _id: { $in: ids } }); + } + + if (segmentIds) { + return EngageMessages.find({ ...commonSelector, segmentIds: { $in: segmentIds } }); + } + + if (brandIds) { + return EngageMessages.find({ ...commonSelector, brandIds: { $in: brandIds } }); + } + + if (tagIds) { + return EngageMessages.find({ ...commonSelector, tagIds: { $in: tagIds } }); + } + + let query = commonSelector; + + // filter by kind + if (kind) { + query.kind = kind; + } + + // filter by status + if (status) { + query = { ...query, ...statusQueryBuilder(status, user) }; + } + + // filter by tag + if (tag) { + query = { ...query, ...tagQueryBuilder(tag) }; + } + + return query; +}; + +const engageQueries = { + /** + * Group engage messages counts by kind, status, tag + */ + engageMessageCounts( + _root, + { name, kind, status }: { name: string; kind: string; status: string }, + { user, commonQuerySelector }: IContext, + ) { + if (name === 'kind') { + return countsByKind(commonQuerySelector); + } + + if (name === 'status') { + return countsByStatus(commonQuerySelector, { kind, user }); + } + + return countsByTag(commonQuerySelector, { kind, status, user }); + }, + + /** + * Engage messages list + */ + engageMessages(_root, args: IListArgs, { user, commonQuerySelector }: IContext) { + return paginate(EngageMessages.find(listQuery(commonQuerySelector, args, user)).sort({ createdAt: -1 }), args); + }, + + /** + * Get one message + */ + engageMessageDetail(_root, { _id }: { _id: string }) { + return EngageMessages.findOne({ _id }); + }, + + /** + * Config detail + */ + engagesConfigDetail(_root, _args, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesConfigDetail(); + }, + + engageReportsList(_root, params, { dataSources }: IContext) { + return dataSources.EngagesAPI.engageReportsList(params); + }, + + /** + * Get all messages count. We will use it in pager + */ + engageMessagesTotalCount(_root, args: IListArgs, { user, commonQuerySelector }: IContext) { + return EngageMessages.find(listQuery(commonQuerySelector, args, user)).countDocuments(); + }, + + /** + * Get all verified emails + */ + engageVerifiedEmails(_root, _args, { dataSources }: IContext) { + return dataSources.EngagesAPI.engagesGetVerifiedEmails(); + }, +}; + +requireLogin(engageQueries, 'engageMessagesTotalCount'); +requireLogin(engageQueries, 'engageMessageCounts'); +requireLogin(engageQueries, 'engageMessageDetail'); + +checkPermission(engageQueries, 'engageMessages', 'showEngagesMessages', []); + +export default engageQueries; diff --git a/src/data/resolvers/queries/fields.ts b/src/data/resolvers/queries/fields.ts index b0f8b59df..5f4f6ed40 100644 --- a/src/data/resolvers/queries/fields.ts +++ b/src/data/resolvers/queries/fields.ts @@ -1,183 +1,94 @@ -import { FIELD_CONTENT_TYPES, FIELDS_GROUPS_CONTENT_TYPES, INTEGRATION_KIND_CHOICES } from '../../../data/constants'; -import { Brands, Companies, Customers, Fields, FieldsGroups, Integrations } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; - -interface IFieldsQuery { - contentType: string; - contentTypeId?: string; -} - -interface IfieldsDefaultColmns { - [index: number]: { name: string; label: string; order: number } | {}; -} - -const fieldQueries = { - /** - * Fields list - */ - fields(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { - const query: IFieldsQuery = { contentType }; - - if (contentTypeId) { - query.contentTypeId = contentTypeId; - } - - return Fields.find(query).sort({ order: 1 }); - }, - - /** - * Generates all field choices base on given kind. - * For example if kind is customer - * then it will generate customer related fields - * [{ name: 'messengerData.isActive', text: 'Messenger: is Active' }] - */ - async fieldsCombinedByContentType(_root, { contentType }: { contentType: string }) { - /* - * Generates fields using given schema - */ - const generateFieldsFromSchema = (queSchema: any, namePrefix: string) => { - const queFields: any = []; - - // field definations - const paths = queSchema.paths; - - queSchema.eachPath(name => { - const label = paths[name].options.label; - - // add to fields list - if (label) { - queFields.push({ - _id: Math.random(), - name: `${namePrefix}${name}`, - label, - }); - } - }); - - return queFields; - }; - - let schema: any = Companies.schema; - let fields: Array<{ _id: number; name: string; label?: string; brandName?: string; brandId?: string }> = []; - - if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { - const messengerIntegrations = await Integrations.find({ kind: INTEGRATION_KIND_CHOICES.MESSENGER }); - - // generate messengerData.customData fields - for (const integration of messengerIntegrations) { - const brand = await Brands.findOne({ _id: integration.brandId }); - - const lastCustomers = await Customers.find({ - integrationId: integration._id, - $and: [ - { 'messengerData.customData': { $exists: true } }, - { 'messengerData.customData': { $ne: null } }, - { 'messengerData.customData': { $ne: {} } }, - ], - }) - .sort({ createdAt: -1 }) - .limit(1); - - if (brand && integration && lastCustomers.length > 0) { - const [lastCustomer] = lastCustomers; - - if (lastCustomer.messengerData) { - const customDataFields = Object.keys(lastCustomer.messengerData.customData || {}); - - for (const customDataField of customDataFields) { - fields.push({ - _id: Math.random(), - name: `messengerData.customData.${customDataField}`, - label: customDataField, - brandName: brand.name, - brandId: brand._id, - }); - } - } - } - } - - schema = Customers.schema; - } - - // generate list using customer or company schema - fields = [...fields, ...generateFieldsFromSchema(schema, '')]; - - schema.eachPath(name => { - const path = schema.paths[name]; - - // extend fields list using sub schema fields - if (path.schema) { - fields = [...fields, ...generateFieldsFromSchema(path.schema, `${name}.`)]; - } - }); - - const customFields = await Fields.find({ contentType }); - - // extend fields list using custom fields - for (const customField of customFields) { - const group = await FieldsGroups.findOne({ _id: customField.groupId }); - - if (group && group.isVisible && customField.isVisible) { - fields.push({ - _id: Math.random(), - name: `customFieldsData.${customField._id}`, - label: customField.text, - }); - } - } - - return fields; - }, - - /** - * Default list columns config - */ - fieldsDefaultColumnsConfig(_root, { contentType }: { contentType: string }): IfieldsDefaultColmns { - if (contentType === FIELD_CONTENT_TYPES.CUSTOMER) { - return [ - { name: 'firstName', label: 'First name', order: 1 }, - { name: 'lastName', label: 'Last name', order: 1 }, - { name: 'primaryEmail', label: 'Primary email', order: 2 }, - { name: 'primaryPhone', label: 'Primary phone', order: 3 }, - ]; - } - - if (contentType === FIELD_CONTENT_TYPES.COMPANY) { - return [ - { name: 'primaryName', label: 'Primary Name', order: 1 }, - { name: 'size', label: 'Size', order: 2 }, - { name: 'links.website', label: 'Website', order: 3 }, - { name: 'industry', label: 'Industry', order: 4 }, - { name: 'plan', label: 'Plan', order: 5 }, - { name: 'lastSeenAt', label: 'Last seen at', order: 6 }, - { name: 'sessionCount', label: 'Session count', order: 7 }, - ]; - } - - return []; - }, -}; - -requireLogin(fieldQueries, 'fieldsCombinedByContentType'); -requireLogin(fieldQueries, 'fieldsDefaultColumnsConfig'); - -checkPermission(fieldQueries, 'fields', 'showFields', []); - -const fieldsGroupQueries = { - /** - * Fields group list - */ - fieldsGroups(_root, { contentType }: { contentType: string }) { - const query: any = {}; - - // querying by content type - query.contentType = contentType || FIELDS_GROUPS_CONTENT_TYPES.CUSTOMER; - - return FieldsGroups.find(query).sort({ order: 1 }); - }, -}; - -checkPermission(fieldsGroupQueries, 'fieldsGroups', 'showFieldsGroups', []); - -export { fieldQueries, fieldsGroupQueries }; +import { FIELD_CONTENT_TYPES, FIELDS_GROUPS_CONTENT_TYPES } from '../../../data/constants'; +import { Fields, FieldsGroups } from '../../../db/models'; +import { fieldsCombinedByContentType } from '../../modules/fields/utils'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +interface IFieldsDefaultColmns { + [index: number]: { name: string; label: string; order: number } | {}; +} + +export interface IFieldsQuery { + contentType: string; + contentTypeId?: string; +} + +const fieldQueries = { + /** + * Fields list + */ + fields(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { + const query: IFieldsQuery = { contentType }; + + if (contentTypeId) { + query.contentTypeId = contentTypeId; + } + + return Fields.find(query).sort({ order: 1 }); + }, + + /** + * Generates all field choices base on given kind. + */ + async fieldsCombinedByContentType(_root, args) { + return fieldsCombinedByContentType(args); + }, + + /** + * Default list columns config + */ + fieldsDefaultColumnsConfig(_root, { contentType }: { contentType: string }): IFieldsDefaultColmns { + if (contentType === FIELD_CONTENT_TYPES.COMPANY) { + return [ + { name: 'primaryName', label: 'Primary Name', order: 1 }, + { name: 'size', label: 'Size', order: 2 }, + { name: 'links.website', label: 'Website', order: 3 }, + { name: 'industry', label: 'Industry', order: 4 }, + { name: 'plan', label: 'Plan', order: 5 }, + { name: 'lastSeenAt', label: 'Last seen at', order: 6 }, + { name: 'sessionCount', label: 'Session count', order: 7 }, + ]; + } + + if (contentType === FIELD_CONTENT_TYPES.PRODUCT) { + return [ + { name: 'categoryCode', label: 'Category Code', order: 0 }, + { name: 'code', label: 'Code', order: 1 }, + { name: 'name', label: 'Name', order: 1 }, + ]; + } + + return [ + { name: 'location.country', label: 'Country', order: 0 }, + { name: 'firstName', label: 'First name', order: 1 }, + { name: 'lastName', label: 'Last name', order: 2 }, + { name: 'primaryEmail', label: 'Primary email', order: 3 }, + { name: 'lastSeenAt', label: 'Last seen at', order: 4 }, + { name: 'sessionCount', label: 'Session count', order: 5 }, + { name: 'profileScore', label: 'Profile score', order: 6 }, + ]; + }, +}; + +requireLogin(fieldQueries, 'fieldsCombinedByContentType'); +requireLogin(fieldQueries, 'fieldsDefaultColumnsConfig'); + +checkPermission(fieldQueries, 'fields', 'showForms', []); + +const fieldsGroupQueries = { + /** + * Fields group list + */ + fieldsGroups(_root, { contentType }: { contentType: string }, { commonQuerySelector }: IContext) { + const query: any = commonQuerySelector; + + // querying by content type + query.contentType = contentType || FIELDS_GROUPS_CONTENT_TYPES.CUSTOMER; + + return FieldsGroups.find(query).sort({ order: 1 }); + }, +}; + +checkPermission(fieldsGroupQueries, 'fieldsGroups', 'showForms', []); + +export { fieldQueries, fieldsGroupQueries }; diff --git a/src/data/resolvers/queries/forms.ts b/src/data/resolvers/queries/forms.ts index 434bbd477..1ad365497 100644 --- a/src/data/resolvers/queries/forms.ts +++ b/src/data/resolvers/queries/forms.ts @@ -1,23 +1,23 @@ -import { Forms } from '../../../db/models'; -import { checkPermission, requireLogin } from '../../permissions/wrappers'; - -const formQueries = { - /** - * Forms list - */ - forms() { - return Forms.find({}).sort({ title: 1 }); - }, - - /** - * Get one form - */ - formDetail(_root, { _id }: { _id: string }) { - return Forms.findOne({ _id }); - }, -}; - -requireLogin(formQueries, 'formDetail'); -checkPermission(formQueries, 'forms', 'showForms', []); - -export default formQueries; +import { Forms } from '../../../db/models'; +import { checkPermission } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +const formQueries = { + /** + * Forms list + */ + forms(_root, _args, { commonQuerySelector }: IContext) { + return Forms.find(commonQuerySelector).sort({ title: 1 }); + }, + + /** + * Get one form + */ + formDetail(_root, { _id }: { _id: string }) { + return Forms.findOne({ _id }); + }, +}; + +checkPermission(formQueries, 'forms', 'showForms', []); + +export default formQueries; diff --git a/src/data/resolvers/queries/growthHacks.ts b/src/data/resolvers/queries/growthHacks.ts new file mode 100644 index 000000000..4d0b8731a --- /dev/null +++ b/src/data/resolvers/queries/growthHacks.ts @@ -0,0 +1,107 @@ +import { GrowthHacks } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { IListParams } from './boards'; +import { + archivedItems, + archivedItemsCount, + checkItemPermByUser, + generateGrowthHackCommonFilters, + IArchiveArgs, +} from './boardUtils'; + +interface IGrowthHackListParams extends IListParams { + hackStage?: string; + limit?: number; +} + +const growthHackQueries = { + /** + * Growth hack list + */ + async growthHacks(_root, args: IGrowthHackListParams, { user, commonQuerySelector }: IContext) { + const filter = { ...commonQuerySelector, ...(await generateGrowthHackCommonFilters(user._id, args)) }; + const { sortField, sortDirection, skip = 0, limit = 10 } = args; + + const sort: { [key: string]: any } = {}; + + if (sortField) { + sort[sortField] = sortDirection; + } + + sort.order = 1; + sort.createdAt = -1; + + return GrowthHacks.find(filter) + .sort(sort) + .skip(skip) + .limit(limit); + }, + + /** + * Archived list + */ + archivedGrowthHacks(_root, args: IArchiveArgs) { + return archivedItems(args, GrowthHacks); + }, + + archivedGrowthHacksCount(_root, args: IArchiveArgs) { + return archivedItemsCount(args, GrowthHacks); + }, + + /** + * Get all growth hacks count. We will use it in pager + */ + async growthHacksTotalCount(_root, args: IGrowthHackListParams, { user }: IContext) { + const filter = await generateGrowthHackCommonFilters(user._id, args); + + return GrowthHacks.find(filter).countDocuments(); + }, + + async growthHacksPriorityMatrix(_root, args: IListParams, { user }: IContext) { + const filter = await generateGrowthHackCommonFilters(user._id, args); + + filter.ease = { $exists: true, $gt: 0 }; + filter.impact = { $exists: true, $gt: 0 }; + + return GrowthHacks.aggregate([ + { + $match: filter, + }, + { + $group: { + _id: { + impact: '$impact', + ease: '$ease', + }, + names: { $push: '$name' }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + x: '$_id.ease', + y: '$_id.impact', + names: 1, + count: 1, + }, + }, + ]); + }, + + /** + * Growth hack detail + */ + async growthHackDetail(_root, { _id }: { _id: string }, { user }: IContext) { + const growthHack = await GrowthHacks.getGrowthHack(_id); + + return checkItemPermByUser(user._id, growthHack); + }, +}; + +moduleRequireLogin(growthHackQueries); + +checkPermission(growthHackQueries, 'growthHacks', 'showGrowthHacks', []); + +export default growthHackQueries; diff --git a/src/data/resolvers/queries/importHistory.ts b/src/data/resolvers/queries/importHistory.ts index b5b526338..56a5c4369 100644 --- a/src/data/resolvers/queries/importHistory.ts +++ b/src/data/resolvers/queries/importHistory.ts @@ -1,31 +1,27 @@ -import { ImportHistory } from '../../../db/models'; -import { checkPermission } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -const importHistoryQueries = { - /** - * Import history list - */ - importHistories(_root, { type, ...args }: { page: number; perPage: number; type: string }) { - const list = paginate(ImportHistory.find({ contentType: type }), args).sort({ date: -1 }); - const count = ImportHistory.find({ contentType: type }).countDocuments(); - - return { list, count }; - }, - - async importHistoryDetail(_root, { _id }: { _id: string }) { - const importHistory = await ImportHistory.findOne({ _id }); - - if (!importHistory) { - throw new Error('Import history not found'); - } - - importHistory.errorMsgs = (importHistory.errorMsgs || []).slice(0, 100); - - return importHistory; - }, -}; - -checkPermission(importHistoryQueries, 'importHistories', 'importHistories', []); - -export default importHistoryQueries; +import { ImportHistory } from '../../../db/models'; +import { checkPermission } from '../../permissions/wrappers'; +import { paginate } from '../../utils'; + +const importHistoryQueries = { + /** + * Import history list + */ + importHistories(_root, { type, ...args }: { page: number; perPage: number; type: string }) { + const list = paginate(ImportHistory.find({ contentType: type }), args).sort({ date: -1 }); + const count = ImportHistory.find({ contentType: type }).countDocuments(); + + return { list, count }; + }, + + async importHistoryDetail(_root, { _id }: { _id: string }) { + const importHistory = await ImportHistory.getImportHistory(_id); + + importHistory.errorMsgs = (importHistory.errorMsgs || []).slice(0, 100); + + return importHistory; + }, +}; + +checkPermission(importHistoryQueries, 'importHistories', 'importHistories', []); + +export default importHistoryQueries; diff --git a/src/data/resolvers/queries/index.ts b/src/data/resolvers/queries/index.ts index a9e4fa4ce..ef73d0e47 100644 --- a/src/data/resolvers/queries/index.ts +++ b/src/data/resolvers/queries/index.ts @@ -2,33 +2,40 @@ import activityLogs from './activityLogs'; import boards from './boards'; import brands from './brands'; import channels from './channels'; +import checklists from './checklists'; import companies from './companies'; import configs from './configs'; import conversations from './conversations'; import customers from './customers'; import dealInsights from './dealInsights'; import deals from './deals'; +import emailDeliveries from './emailDelivery'; import emailTemplates from './emailTemplates'; import engages from './engages'; import { fieldQueries as fields, fieldsGroupQueries as fieldsgroups } from './fields'; import forms from './forms'; +import growthHack from './growthHacks'; import importHistory from './importHistory'; import insights from './insights'; import integrations from './integrations'; import internalNotes from './internalNotes'; import knowledgeBase from './knowledgeBase'; import logs from './logs'; -import messengerApps from './messengerApps'; import notifications from './notifications'; import { permissionQueries as permissions, usersGroupQueries as usersGroups } from './permissions'; +import pipelineLabels from './pipelineLabels'; +import pipelineTemplates from './pipelineTemplates'; import products from './products'; import responseTemplates from './responseTemplates'; +import robot from './robot'; import scripts from './scripts'; import segments from './segments'; import tags from './tags'; import tasks from './tasks'; import tickets from './tickets'; import users from './users'; +import webhooks from './webhooks'; +import widgets from './widgets'; export default { ...users, @@ -39,6 +46,7 @@ export default { ...responseTemplates, ...scripts, ...emailTemplates, + ...emailDeliveries, ...engages, ...forms, ...tags, @@ -58,10 +66,16 @@ export default { ...configs, ...fieldsgroups, ...importHistory, - ...messengerApps, ...permissions, ...usersGroups, ...tickets, ...tasks, ...logs, + ...growthHack, + ...pipelineTemplates, + ...checklists, + ...robot, + ...pipelineLabels, + ...widgets, + ...webhooks, }; diff --git a/src/data/resolvers/queries/insights.ts b/src/data/resolvers/queries/insights.ts index c1b531c4f..3146735a7 100644 --- a/src/data/resolvers/queries/insights.ts +++ b/src/data/resolvers/queries/insights.ts @@ -1,702 +1,726 @@ -import { ConversationMessages, Conversations, Integrations, Tags } from '../../../db/models'; -import { TAG_TYPES } from '../../../db/models/definitions/constants'; -import { IUserDocument } from '../../../db/models/definitions/users'; -import { INTEGRATION_KIND_CHOICES } from '../../constants'; -import { getDateFieldAsStr, getDurationField } from '../../modules/insights/aggregationUtils'; -import { IListArgs, IPieChartData } from '../../modules/insights/types'; -import { - fixChartData, - fixDates, - generateChartDataByCollection, - generateChartDataBySelector, - generatePunchData, - generateResponseData, - getConversationReportLookup, - getConversationSelector, - getConversationSelectorByMsg, - getConversationSelectorToMsg, - getFilterSelector, - getMessageSelector, - getSummaryData, - getSummaryDates, - getTimezone, - noConversationSelector, -} from '../../modules/insights/utils'; -import { moduleCheckPermission, moduleRequireLogin } from '../../permissions/wrappers'; - -const insightQueries = { - /** - * Builds insights charting data contains - * count of conversations in various integrations kinds. - */ - async insightsIntegrations(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const conversationSelector = await getConversationSelector(filterSelector); - - const integrations: IPieChartData[] = []; - - // count conversations by each integration kind - for (const kind of INTEGRATION_KIND_CHOICES.ALL) { - const integrationIds = await Integrations.find({ - ...filterSelector.integration, - kind, - }).select('_id'); - - // find conversation counts of given integrations - const value = await Conversations.countDocuments({ - ...conversationSelector, - integrationId: { $in: integrationIds }, - }); - - if (value > 0) { - integrations.push({ id: kind, label: kind, value }); - } - } - - return integrations; - }, - - /** - * Builds insights charting data contains - * count of conversations in various integrations tags. - */ - async insightsTags(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const conversationSelector = { - createdAt: filterSelector.createdAt, - ...noConversationSelector, - }; - - const tagDatas: IPieChartData[] = []; - - const tags = await Tags.find({ type: TAG_TYPES.CONVERSATION }).select('name'); - - const integrationIdsByTag = await Integrations.find(filterSelector.integration).select('_id'); - - const rawIntegrationIdsByTag = integrationIdsByTag.map(row => row._id); - - const tagData = await Conversations.aggregate([ - { - $match: { - ...conversationSelector, - integrationId: { $in: rawIntegrationIdsByTag }, - }, - }, - { - $unwind: '$tagIds', - }, - { - $group: { - _id: '$tagIds', - count: { $sum: 1 }, - }, - }, - ]); - - const tagDictionaryData = {}; - - tagData.forEach(row => { - tagDictionaryData[row._id] = row.count; - }); - - // count conversations by each tag - for (const tag of tags) { - // find conversation counts of given tag - const value = tagDictionaryData[tag._id]; - if (tag._id in tagDictionaryData) { - tagDatas.push({ id: tag.name, label: tag.name, value }); - } - } - - return tagDatas; - }, - - /** - * Counts conversations by each hours in each days. - */ - async insightsPunchCard(_root, args: IListArgs, { user }: { user: IUserDocument }) { - const messageSelector = await getMessageSelector({ args }); - - return generatePunchData(ConversationMessages, messageSelector, user); - }, - - /** - * Sends combined charting data for trends. - */ - async insightsTrend(_root, args: IListArgs) { - const messageSelector = await getMessageSelector({ args }); - - return generateChartDataBySelector({ selector: messageSelector }); - }, - - /** - * Sends summary datas. - */ - async insightsSummaryData(_root, args: IListArgs) { - const selector = await getMessageSelector({ - args, - createdAt: getSummaryDates(args.endDate), - }); - - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - return getSummaryData({ - start, - end, - collection: ConversationMessages, - selector, - }); - }, - - /** - * Sends combined charting data for trends and summaries. - */ - async insightsConversation(_root, args: IListArgs) { - const filterSelector = getFilterSelector(args); - - const selector = await getConversationSelector(filterSelector); - - const conversations = await Conversations.find(selector); - - const insightData: any = { - summary: [], - trend: await generateChartDataByCollection(conversations), - }; - - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - insightData.summary = await getSummaryData({ - start, - end, - collection: Conversations, - selector, - }); - - return insightData; - }, - - /** - * Calculates average first response time for each team members. - */ - async insightsFirstResponse(_root, args: IListArgs) { - const { startDate, endDate } = args; - const filterSelector = getFilterSelector(args); - const { start, end } = fixDates(startDate, endDate); - - const conversationSelector = { - firstRespondedUserId: { $exists: true }, - firstRespondedDate: { $exists: true }, - messageCount: { $gt: 1 }, - createdAt: { $gte: start, $lte: end }, - }; - - const insightData = { teamMembers: [], trend: [] }; - - // Variable that holds all responded conversation messages - const firstResponseData: any = []; - - // Variables holds every user's response time. - const responseUserData: any = {}; - - let allResponseTime = 0; - - const selector = await getConversationSelector(filterSelector, conversationSelector); - - const conversations = await Conversations.find(selector); - - if (conversations.length < 1) { - return insightData; - } - - const summaries = [0, 0, 0, 0]; - - // Processes total first response time for each users. - for (const conversation of conversations) { - const { firstRespondedUserId, firstRespondedDate, createdAt } = conversation; - - let responseTime = 0; - - // checking wheter or not this is actual conversation - if (firstRespondedDate && firstRespondedUserId) { - responseTime = createdAt.getTime() - firstRespondedDate.getTime(); - responseTime = Math.abs(responseTime / 1000); - - const userId = firstRespondedUserId; - - // collecting each user's respond information - firstResponseData.push({ - createdAt: firstRespondedDate, - userId, - responseTime, - }); - - allResponseTime += responseTime; - - // Builds every users's response time and conversation message count. - if (responseUserData[userId]) { - responseUserData[userId].responseTime = responseTime + responseUserData[userId].responseTime; - responseUserData[userId].count = responseUserData[userId].count + 1; - } else { - responseUserData[userId] = { - responseTime, - count: 1, - summaries: [0, 0, 0, 0], - }; - } - - const minute = Math.floor(responseTime / 60); - const index = minute < 3 ? minute : 3; - - summaries[index] = summaries[index] + 1; - responseUserData[userId].summaries[index] = responseUserData[userId].summaries[index] + 1; - } - } - - const doc = await generateResponseData(firstResponseData, responseUserData, allResponseTime); - - return { ...doc, summaries }; - }, - - /** - * Calculates average response close time for each team members. - */ - async insightsResponseClose(_root, args: IListArgs, { user }: { user: IUserDocument }) { - const { startDate, endDate } = args; - const { start, end } = fixDates(startDate, endDate); - - const conversationSelector = { - createdAt: { $gte: start, $lte: end }, - closedAt: { $exists: true }, - closedUserId: { $exists: true }, - }; - - const conversationMatch = await getConversationSelector(getFilterSelector(args), { ...conversationSelector }); - - const insightAggregateData = await Conversations.aggregate([ - { - $match: conversationMatch, - }, - { - $project: { - responseTime: getDurationField({ startField: '$closedAt', endField: '$createdAt' }), - date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), - closedUserId: 1, - }, - }, - { - $group: { - _id: { - closedUserId: '$closedUserId', - date: '$date', - }, - totalResponseTime: { $sum: '$responseTime' }, - avgResponseTime: { $avg: '$responseTime' }, - count: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - closedUserId: '$_id.closedUserId', - date: '$_id.date', - totalResponseTime: 1, - avgResponseTime: 1, - count: 1, - }, - }, - { - $lookup: { - from: 'users', - localField: 'closedUserId', - foreignField: '_id', - as: 'userDoc', - }, - }, - { - $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, - }, - { - $group: { - _id: '$closedUserId', - responseTime: { $sum: '$totalResponseTime' }, - avgResponseTime: { $avg: '$avgResponseTime' }, - count: { $sum: '$count' }, - fullName: { $first: '$fullName' }, - avatar: { $first: '$avatar' }, - chartDatas: { - $push: { - date: '$date', - count: '$count', - }, - }, - }, - }, - ]); - - // Variables holds every user's response time. - const teamMembers: any = []; - const responseUserData: any = {}; - - let allResponseTime = 0; - let totalCount = 0; - const aggregatedTrend = {}; - - for (const userData of insightAggregateData) { - // responseUserData - responseUserData[userData._id] = { - responseTime: userData.responseTime, - count: userData.count, - avgResponseTime: userData.avgResponseTime, - fullName: userData.fullName, - avatar: userData.avatar, - }; - // team members gather - const fixedChartData = await fixChartData(userData.chartDatas, 'date', 'count'); - userData.chartDatas.forEach(row => { - if (row.date in aggregatedTrend) { - aggregatedTrend[row.date] += row.count; - } else { - aggregatedTrend[row.date] = row.count; - } - }); - - teamMembers.push({ - data: { - fullName: userData.fullName, - avatar: userData.avatar, - graph: fixedChartData, - }, - }); - // calculate allResponseTime to render average responseTime - allResponseTime += userData.responseTime; - totalCount += userData.count; - } - - if (insightAggregateData.length < 1) { - return { teamMembers: [], trend: [] }; - } - - const trend = await fixChartData( - Object.keys(aggregatedTrend) - .sort() - .map(key => { - return { date: key, count: aggregatedTrend[key] }; - }), - 'date', - 'count', - ); - const time = Math.floor(allResponseTime / totalCount); - - return { trend, teamMembers, time }; - }, - - /** - * Calculates average ConversationMessages frequency time for second - */ - async insightsConversationSummary(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateData = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { internal: false }, { userId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.customerId': { $exists: true }, - }, - }, - lookupHelper.diffSecondCalc, - { - $lookup: { - from: 'users', - localField: 'userId', - foreignField: '_id', - as: 'userDoc', - }, - }, - { - $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, - }, - { - $group: { - _id: { - date: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } }, - user: '$userId', - }, - userId: { $first: '$userId' }, - fullName: { $first: '$fullName' }, - avatar: { $first: '$avatar' }, - date: { $first: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } } }, - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - if (insightAggregateData.length === 0) { - return { - avg: [{ title: 'Average all operator response time', count: 0 }], - trend: [], - teamMembers: [], - }; - } - - const averageTotal = - insightAggregateData.reduce((preVal, currVal) => ({ - avgSecond: preVal.avgSecond + currVal.avgSecond, - })).avgSecond / insightAggregateData.length; - - const summaryChart = await fixChartData(insightAggregateData, 'date', 'avgSecond'); - - const userIds = await insightAggregateData.map(item => item.userId).filter((v, i, a) => a.indexOf(v) === i); - - const perUserChart: object[] = []; - - for (const userId of userIds) { - const perData = insightAggregateData.filter(item => item.userId === userId); - const perChart = await fixChartData(perData, 'date', 'avgSecond'); - - perUserChart.push({ - avatar: perData[0].avatar, - fullName: perData[0].fullName, - graph: perChart, - }); - } - - return { - avg: [{ title: 'Average all operator response time', count: averageTotal }], - trend: summaryChart, - teamMembers: perUserChart, - }; - }, - - /** - * Calculates average ConversationMessages spec CustomerAvg - */ - async insightsConversationCustomerAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateCustomer = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { internal: false }, { customerId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.userId': { $exists: true }, - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Average all customer response time', - count: insightAggregateCustomer.length ? insightAggregateCustomer[0].avgSecond : 0, - }, - ]; - }, - - /** - * Calculates average ConversationMessages spec InternalMsgsAvg - */ - async insightsConversationInternalAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - lookupHelper.lookupPrevMsg.$lookup.pipeline[1].$project.sizeMentionedIds = { $size: '$mentionedUserIds' }; - - const insightAggregateInternal = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector, { userId: { $exists: true } }], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - 'prevMsg.sizeMentionedIds': { $gt: 0 }, - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Average internal response time', - count: insightAggregateInternal.length ? insightAggregateInternal[0].avgSecond : 0, - }, - ]; - }, - - /** - * Calculates average ConversationMessages spec Overall - */ - async insightsConversationOverallAvg(_root, args: IListArgs) { - const { startDate, endDate, integrationIds, brandIds } = args; - const { start, end } = fixDates(startDate, endDate); - - const messageSelector = { - createdAt: { $gte: start, $lte: end }, - }; - - const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); - - const lookupHelper = await getConversationReportLookup(); - - const insightAggregateAllAvg = await ConversationMessages.aggregate([ - { - $match: { - $and: [conversationSelector, messageSelector], - }, - }, - lookupHelper.lookupPrevMsg, - lookupHelper.prevMsgSlice, - lookupHelper.firstProject, - { $unwind: '$prevMsg' }, - { - $match: { - $or: [ - { $and: [{ userId: { $exists: true } }, { 'prevMsg.customerId': { $exists: true } }] }, - { $and: [{ customerId: { $exists: true } }, { 'prevMsg.userId': { $exists: true } }] }, - ], - }, - }, - lookupHelper.diffSecondCalc, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - // all duration average - const conversationMatch = await getConversationSelectorToMsg(integrationIds, brandIds); - - const insightAggregateDurationAvg = await Conversations.aggregate([ - { - $match: { - $and: [conversationMatch, messageSelector], - }, - }, - { - $lookup: { - from: 'conversation_messages', - let: { checkConversation: '$_id' }, - pipeline: [ - { - $match: { - $expr: { $eq: ['$conversationId', '$$checkConversation'] }, - }, - }, - { - $project: { createdAt: 1 }, - }, - ], - as: 'allMsgs', - }, - }, - { - $addFields: { - firstMsg: { $slice: ['$allMsgs', 1] }, - lastMsg: { $slice: ['$allMsgs', -1] }, - }, - }, - { - $project: { - _id: 1, - createdAt: 1, - firstMsg: 1, - lastMsg: 1, - }, - }, - { $unwind: '$firstMsg' }, - { $unwind: '$lastMsg' }, - { - $addFields: { - diffSec: { - $divide: [{ $subtract: ['$lastMsg.createdAt', '$firstMsg.createdAt'] }, 1000], - }, - }, - }, - { - $group: { - _id: '', - avgSecond: { $avg: '$diffSec' }, - }, - }, - ]); - - return [ - { - title: 'Overall average', - count: insightAggregateAllAvg.length ? insightAggregateAllAvg[0].avgSecond : 0, - }, - { - title: 'Summary duration average', - count: insightAggregateDurationAvg.length ? insightAggregateDurationAvg[0].avgSecond : 0, - }, - ]; - }, -}; - -moduleRequireLogin(insightQueries); - -moduleCheckPermission(insightQueries, 'showInsights'); - -export default insightQueries; +import { ConversationMessages, Conversations, Integrations, Tags } from '../../../db/models'; +import { KIND_CHOICES, TAG_TYPES } from '../../../db/models/definitions/constants'; +import { getDateFieldAsStr, getDurationField } from '../../modules/insights/aggregationUtils'; +import { IListArgs, IPieChartData } from '../../modules/insights/types'; +import { + fixChartData, + fixDates, + generateChartDataByCollection, + generateChartDataBySelector, + generatePunchData, + generateResponseData, + getConversationReportLookup, + getConversationSelector, + getConversationSelectorByMsg, + getConversationSelectorToMsg, + getFilterSelector, + getMessageSelector, + getSummaryData, + getSummaryDates, + getTimezone, + noConversationSelector, +} from '../../modules/insights/utils'; +import { moduleCheckPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { registerOnboardHistory } from '../../utils'; + +const insightQueries = { + /** + * Builds insights charting data contains + * count of conversations in various integrations kinds. + */ + async insightsIntegrations(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const filterSelector = getFilterSelector(args); + + const conversationSelector = await getConversationSelector(filterSelector); + + const integrations: IPieChartData[] = []; + + // count conversations by each integration kind + for (const kind of KIND_CHOICES.ALL) { + const integrationIds = await Integrations.findIntegrations({ + ...filterSelector.integration, + kind, + }).select('_id'); + + // find conversation counts of given integrations + const value = await Conversations.countDocuments({ + ...conversationSelector, + integrationId: { $in: integrationIds }, + }); + + if (value > 0) { + integrations.push({ id: kind, label: kind, value }); + } + } + + return integrations; + }, + + /** + * Builds insights charting data contains + * count of conversations in various integrations tags. + */ + async insightsTags(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const filterSelector = getFilterSelector(args); + + const conversationSelector = { + createdAt: filterSelector.createdAt, + ...noConversationSelector, + }; + + const tagDatas: IPieChartData[] = []; + + const tags = await Tags.find({ type: TAG_TYPES.CONVERSATION }).select('name'); + + const integrationIdsByTag = await Integrations.findIntegrations(filterSelector.integration).select('_id'); + + const rawIntegrationIdsByTag = integrationIdsByTag.map(row => row._id); + + const tagData = await Conversations.aggregate([ + { + $match: { + ...conversationSelector, + integrationId: { $in: rawIntegrationIdsByTag }, + }, + }, + { + $unwind: '$tagIds', + }, + { + $group: { + _id: '$tagIds', + count: { $sum: 1 }, + }, + }, + ]); + + const tagDictionaryData = {}; + + tagData.forEach(row => { + tagDictionaryData[row._id] = row.count; + }); + + // count conversations by each tag + for (const tag of tags) { + // find conversation counts of given tag + const value = tagDictionaryData[tag._id]; + if (tag._id in tagDictionaryData) { + tagDatas.push({ id: tag.name, label: tag.name, value }); + } + } + + return tagDatas; + }, + + /** + * Counts conversations by each hours in each days. + */ + async insightsPunchCard(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const messageSelector = await getMessageSelector({ args }); + + return generatePunchData(ConversationMessages, messageSelector, user); + }, + + /** + * Sends combined charting data for trends. + */ + async insightsTrend(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const messageSelector = await getMessageSelector({ args }); + + return generateChartDataBySelector({ selector: messageSelector }); + }, + + /** + * Sends summary datas. + */ + async insightsSummaryData(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const selector = await getMessageSelector({ + args, + createdAt: getSummaryDates(args.endDate), + }); + + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + return getSummaryData({ + start, + end, + collection: ConversationMessages, + selector, + }); + }, + + /** + * Sends combined charting data for trends and summaries. + */ + async insightsConversation(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const filterSelector = getFilterSelector(args); + + const selector = await getConversationSelector(filterSelector); + + const conversations = await Conversations.find(selector); + + const insightData: any = { + summary: [], + trend: await generateChartDataByCollection(conversations), + }; + + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + insightData.summary = await getSummaryData({ + start, + end, + collection: Conversations, + selector, + }); + + return insightData; + }, + + /** + * Calculates average first response time for each team members. + */ + async insightsFirstResponse(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate } = args; + const filterSelector = getFilterSelector(args); + const { start, end } = fixDates(startDate, endDate); + + const conversationSelector = { + firstRespondedUserId: { $exists: true }, + firstRespondedDate: { $exists: true }, + messageCount: { $gt: 1 }, + createdAt: { $gte: start, $lte: end }, + }; + + const insightData = { teamMembers: [], trend: [] }; + + // Variable that holds all responded conversation messages + const firstResponseData: any = []; + + // Variables holds every user's response time. + const responseUserData: any = {}; + + let allResponseTime = 0; + + const selector = await getConversationSelector(filterSelector, conversationSelector); + + const conversations = await Conversations.find(selector); + + if (conversations.length < 1) { + return insightData; + } + + const summaries = [0, 0, 0, 0]; + + // Processes total first response time for each users. + for (const conversation of conversations) { + const { firstRespondedUserId, firstRespondedDate, createdAt } = conversation; + + let responseTime = 0; + + // checking wheter or not this is actual conversation + if (firstRespondedDate && firstRespondedUserId) { + responseTime = createdAt.getTime() - firstRespondedDate.getTime(); + responseTime = Math.abs(responseTime / 1000); + + const userId = firstRespondedUserId; + + // collecting each user's respond information + firstResponseData.push({ + createdAt: firstRespondedDate, + userId, + responseTime, + }); + + allResponseTime += responseTime; + + // Builds every users's response time and conversation message count. + if (responseUserData[userId]) { + responseUserData[userId].responseTime = responseTime + responseUserData[userId].responseTime; + responseUserData[userId].count = responseUserData[userId].count + 1; + } else { + responseUserData[userId] = { + responseTime, + count: 1, + summaries: [0, 0, 0, 0], + }; + } + + const minute = Math.floor(responseTime / 60); + const index = minute < 3 ? minute : 3; + + summaries[index] = summaries[index] + 1; + responseUserData[userId].summaries[index] = responseUserData[userId].summaries[index] + 1; + } + } + + const doc = await generateResponseData(firstResponseData, responseUserData, allResponseTime); + + return { ...doc, summaries }; + }, + + /** + * Calculates average response close time for each team members. + */ + async insightsResponseClose(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate } = args; + const { start, end } = fixDates(startDate, endDate); + + const conversationSelector = { + createdAt: { $gte: start, $lte: end }, + closedAt: { $exists: true }, + closedUserId: { $exists: true }, + }; + + const conversationMatch = await getConversationSelector(getFilterSelector(args), { ...conversationSelector }); + + const insightAggregateData = await Conversations.aggregate([ + { + $match: conversationMatch, + }, + { + $project: { + responseTime: getDurationField({ startField: '$closedAt', endField: '$createdAt' }), + date: await getDateFieldAsStr({ timeZone: getTimezone(user) }), + closedUserId: 1, + }, + }, + { + $group: { + _id: { + closedUserId: '$closedUserId', + date: '$date', + }, + totalResponseTime: { $sum: '$responseTime' }, + avgResponseTime: { $avg: '$responseTime' }, + count: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + closedUserId: '$_id.closedUserId', + date: '$_id.date', + totalResponseTime: 1, + avgResponseTime: 1, + count: 1, + }, + }, + { + $lookup: { + from: 'users', + localField: 'closedUserId', + foreignField: '_id', + as: 'userDoc', + }, + }, + { + $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, + }, + { + $group: { + _id: '$closedUserId', + responseTime: { $sum: '$totalResponseTime' }, + avgResponseTime: { $avg: '$avgResponseTime' }, + count: { $sum: '$count' }, + fullName: { $first: '$fullName' }, + avatar: { $first: '$avatar' }, + chartDatas: { + $push: { + date: '$date', + count: '$count', + }, + }, + }, + }, + ]); + + // Variables holds every user's response time. + const teamMembers: any = []; + const responseUserData: any = {}; + + let allResponseTime = 0; + let totalCount = 0; + const aggregatedTrend = {}; + + for (const userData of insightAggregateData) { + // responseUserData + responseUserData[userData._id] = { + responseTime: userData.responseTime, + count: userData.count, + avgResponseTime: userData.avgResponseTime, + fullName: userData.fullName, + avatar: userData.avatar, + }; + // team members gather + const fixedChartData = await fixChartData(userData.chartDatas, 'date', 'count'); + userData.chartDatas.forEach(row => { + if (row.date in aggregatedTrend) { + aggregatedTrend[row.date] += row.count; + } else { + aggregatedTrend[row.date] = row.count; + } + }); + + teamMembers.push({ + data: { + fullName: userData.fullName, + avatar: userData.avatar, + graph: fixedChartData, + }, + }); + // calculate allResponseTime to render average responseTime + allResponseTime += userData.responseTime; + totalCount += userData.count; + } + + if (insightAggregateData.length < 1) { + return { teamMembers: [], trend: [] }; + } + + const trend = await fixChartData( + Object.keys(aggregatedTrend) + .sort() + .map(key => { + return { date: key, count: aggregatedTrend[key] }; + }), + 'date', + 'count', + ); + const time = Math.floor(allResponseTime / totalCount); + + return { trend, teamMembers, time }; + }, + + /** + * Calculates average ConversationMessages frequency time for second + */ + async insightsConversationSummary(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateData = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { internal: false }, { userId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.customerId': { $exists: true }, + }, + }, + lookupHelper.diffSecondCalc, + { + $lookup: { + from: 'users', + localField: 'userId', + foreignField: '_id', + as: 'userDoc', + }, + }, + { + $replaceRoot: { newRoot: { $mergeObjects: [{ $arrayElemAt: ['$userDoc.details', 0] }, '$$ROOT'] } }, + }, + { + $group: { + _id: { + date: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } }, + user: '$userId', + }, + userId: { $first: '$userId' }, + fullName: { $first: '$fullName' }, + avatar: { $first: '$avatar' }, + date: { $first: { $dateToString: { date: '$createdAt', format: '%Y-%m-%d' } } }, + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + if (insightAggregateData.length === 0) { + return { + avg: [{ title: 'Response time of the operator', count: 0 }], + trend: [], + teamMembers: [], + }; + } + + const averageTotal = + insightAggregateData.reduce((preVal, currVal) => ({ + avgSecond: preVal.avgSecond + currVal.avgSecond, + })).avgSecond / insightAggregateData.length; + + const summaryChart = await fixChartData(insightAggregateData, 'date', 'avgSecond'); + + const userIds = await insightAggregateData.map(item => item.userId).filter((v, i, a) => a.indexOf(v) === i); + + const perUserChart: object[] = []; + + for (const userId of userIds) { + const perData = insightAggregateData.filter(item => item.userId === userId); + const perChart = await fixChartData(perData, 'date', 'avgSecond'); + + perUserChart.push({ + avatar: perData[0].avatar, + fullName: perData[0].fullName, + graph: perChart, + }); + } + + return { + avg: [{ title: 'Response time of the operator', count: averageTotal }], + trend: summaryChart, + teamMembers: perUserChart, + }; + }, + + /** + * Calculates average ConversationMessages spec CustomerAvg + */ + async insightsConversationCustomerAvg(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateCustomer = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { internal: false }, { customerId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.userId': { $exists: true }, + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Response time of the customer', + count: insightAggregateCustomer.length ? insightAggregateCustomer[0].avgSecond : 0, + }, + ]; + }, + + /** + * Calculates average ConversationMessages spec InternalMsgsAvg + */ + async insightsConversationInternalAvg(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + lookupHelper.lookupPrevMsg.$lookup.pipeline[1].$project.sizeMentionedIds = { $size: '$mentionedUserIds' }; + + const insightAggregateInternal = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector, { userId: { $exists: true } }], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + 'prevMsg.sizeMentionedIds': { $gt: 0 }, + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Response time of the internal message', + count: insightAggregateInternal.length ? insightAggregateInternal[0].avgSecond : 0, + }, + ]; + }, + + /** + * Calculates average ConversationMessages spec Overall + */ + async insightsConversationOverallAvg(_root, args: IListArgs, { user }: IContext) { + registerOnboardHistory({ type: 'showInsights', user }); + + const { startDate, endDate, integrationIds, brandIds } = args; + const { start, end } = fixDates(startDate, endDate); + + const messageSelector = { + createdAt: { $gte: start, $lte: end }, + }; + + const conversationSelector = await getConversationSelectorByMsg(integrationIds, brandIds); + + const lookupHelper = await getConversationReportLookup(); + + const insightAggregateAllAvg = await ConversationMessages.aggregate([ + { + $match: { + $and: [conversationSelector, messageSelector], + }, + }, + lookupHelper.lookupPrevMsg, + lookupHelper.prevMsgSlice, + lookupHelper.firstProject, + { $unwind: '$prevMsg' }, + { + $match: { + $or: [ + { $and: [{ userId: { $exists: true } }, { 'prevMsg.customerId': { $exists: true } }] }, + { $and: [{ customerId: { $exists: true } }, { 'prevMsg.userId': { $exists: true } }] }, + ], + }, + }, + lookupHelper.diffSecondCalc, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + // all duration average + const conversationMatch = await getConversationSelectorToMsg(integrationIds, brandIds); + + const insightAggregateDurationAvg = await Conversations.aggregate([ + { + $match: { + $and: [conversationMatch, messageSelector], + }, + }, + { + $lookup: { + from: 'conversation_messages', + let: { checkConversation: '$_id' }, + pipeline: [ + { + $match: { + $expr: { $eq: ['$conversationId', '$$checkConversation'] }, + }, + }, + { + $project: { createdAt: 1 }, + }, + ], + as: 'allMsgs', + }, + }, + { + $addFields: { + firstMsg: { $slice: ['$allMsgs', 1] }, + lastMsg: { $slice: ['$allMsgs', -1] }, + }, + }, + { + $project: { + _id: 1, + createdAt: 1, + firstMsg: 1, + lastMsg: 1, + }, + }, + { $unwind: '$firstMsg' }, + { $unwind: '$lastMsg' }, + { + $addFields: { + diffSec: { + $divide: [{ $subtract: ['$lastMsg.createdAt', '$firstMsg.createdAt'] }, 1000], + }, + }, + }, + { + $group: { + _id: '', + avgSecond: { $avg: '$diffSec' }, + }, + }, + ]); + + return [ + { + title: 'Wait time between messages', + count: insightAggregateAllAvg.length ? insightAggregateAllAvg[0].avgSecond : 0, + }, + { + title: 'Duration time up until the conversation is resolved', + count: insightAggregateDurationAvg.length ? insightAggregateDurationAvg[0].avgSecond : 0, + }, + ]; + }, +}; + +moduleRequireLogin(insightQueries); + +moduleCheckPermission(insightQueries, 'showInsights'); + +export default insightQueries; diff --git a/src/data/resolvers/queries/integrations.ts b/src/data/resolvers/queries/integrations.ts index fa747122d..330f70da8 100644 --- a/src/data/resolvers/queries/integrations.ts +++ b/src/data/resolvers/queries/integrations.ts @@ -1,8 +1,11 @@ import { Brands, Channels, Integrations, Tags } from '../../../db/models'; -import { KIND_CHOICES, TAG_TYPES } from '../../../db/models/definitions/constants'; +import { INTEGRATION_NAMES_MAP, KIND_CHOICES, TAG_TYPES } from '../../../db/models/definitions/constants'; import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { fetchIntegrationApi, paginate } from '../../utils'; +import messageBroker from '../../../messageBroker'; +import { RABBITMQ_QUEUES } from '../../constants'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; /** * Common helper for integrations & integrationsTotalCount */ @@ -13,10 +16,16 @@ const generateFilterQuery = async ({ kind, channelId, brandId, searchValue, tag query.kind = kind; } + if (kind === 'mail') { + query.kind = { + $in: ['gmail', 'nylas-gmail', 'nylas-imap', 'nylas-office365', 'nylas-outlook', 'nylas-yahoo', 'nylas-exchange'], + }; + } + // filter integrations by channel if (channelId) { - const channel = await Channels.findOne({ _id: channelId }); - query._id = { $in: channel ? channel.integrationIds : [] }; + const channel = await Channels.getChannel(channelId); + query._id = { $in: channel.integrationIds || [] }; } // filter integrations by brand @@ -46,18 +55,35 @@ const integrationQueries = { page: number; perPage: number; kind: string; + searchValue: string; channelId: string; brandId: string; tag: string; }, + { singleBrandIdSelector }: IContext, ) { - const query = await generateFilterQuery(args); - const integrations = paginate(Integrations.find(query), args); + const query = { ...singleBrandIdSelector, ...(await generateFilterQuery(args)) }; + const integrations = paginate(Integrations.findAllIntegrations(query), args); return integrations.sort({ name: 1 }); }, + /** + * Get used integration types + */ + async integrationsGetUsedTypes(_root, {}) { + const usedTypes: Array<{ _id: string; name: string }> = []; + + for (const kind of KIND_CHOICES.ALL) { + if ((await Integrations.findIntegrations({ kind }).countDocuments()) > 0) { + usedTypes.push({ _id: kind, name: INTEGRATION_NAMES_MAP[kind] }); + } + } + + return usedTypes; + }, + /** * Get one integration */ @@ -78,7 +104,7 @@ const integrationQueries = { }; const count = query => { - return Integrations.find(query).countDocuments(); + return Integrations.findAllIntegrations(query).countDocuments(); }; // Counting integrations by tag @@ -118,8 +144,19 @@ const integrationQueries = { /** * Fetch integrations api */ - integrationsFetchApi(_root, { path, params }: { path: string; params: { [key: string]: string } }) { - return fetchIntegrationApi({ path, method: 'GET', params }); + integrationsFetchApi( + _root, + { path, params }: { path: string; params: { [key: string]: string } }, + { dataSources }: IContext, + ) { + return dataSources.IntegrationsAPI.fetchApi(path, params); + }, + + async integrationGetLineWebhookUrl(_root, { _id }: { _id: string }) { + return messageBroker().sendRPCMessage(RABBITMQ_QUEUES.RPC_API_TO_INTEGRATIONS, { + action: 'line-webhook', + data: { _id }, + }); }, }; diff --git a/src/data/resolvers/queries/internalNotes.ts b/src/data/resolvers/queries/internalNotes.ts index 9b33d15b8..65a62acb1 100644 --- a/src/data/resolvers/queries/internalNotes.ts +++ b/src/data/resolvers/queries/internalNotes.ts @@ -1,17 +1,20 @@ -import { InternalNotes } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -const internalNoteQueries = { - /** - * InternalNotes list - */ - internalNotes(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { - return InternalNotes.find({ contentType, contentTypeId }).sort({ - createdDate: 1, - }); - }, -}; - -moduleRequireLogin(internalNoteQueries); - -export default internalNoteQueries; +import { InternalNotes } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; + +const internalNoteQueries = { + async internalNoteDetail(_root, { _id }: { _id: string }) { + return InternalNotes.findOne({ _id }); + }, + /** + * InternalNotes list + */ + internalNotes(_root, { contentType, contentTypeId }: { contentType: string; contentTypeId: string }) { + return InternalNotes.find({ contentType, contentTypeId }).sort({ + createdDate: 1, + }); + }, +}; + +moduleRequireLogin(internalNoteQueries); + +export default internalNoteQueries; diff --git a/src/data/resolvers/queries/knowledgeBase.ts b/src/data/resolvers/queries/knowledgeBase.ts index d5c5a24fe..9a5016fa0 100644 --- a/src/data/resolvers/queries/knowledgeBase.ts +++ b/src/data/resolvers/queries/knowledgeBase.ts @@ -1,160 +1,159 @@ -import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; - -import { checkPermission, requireLogin } from '../../permissions/wrappers'; -import { paginate } from '../../utils'; - -/* Articles list & total count helper */ -const articlesQuery = async ({ categoryIds }: { categoryIds: string[] }) => { - const query: any = {}; - - // filter articles by category i - if (categoryIds) { - const categories = await KnowledgeBaseCategories.find({ - _id: { - $in: categoryIds, - }, - }); - - let articleIds: any = []; - - for (const category of categories) { - articleIds = articleIds.concat(category.articleIds || []); - } - - query._id = { - $in: articleIds, - }; - } - - return query; -}; - -/* Categories list & total count helper */ -const categoriesQuery = async ({ topicIds }: { topicIds: string[] }) => { - const query: any = {}; - - // filter categories by topic id - if (topicIds) { - let categoryIds: any = []; - - const topics = await KnowledgeBaseTopics.find({ - _id: { - $in: topicIds, - }, - }); - - for (const topic of topics) { - categoryIds = categoryIds.concat(topic.categoryIds || []); - } - - query._id = { - $in: categoryIds, - }; - } - - return query; -}; - -const knowledgeBaseQueries = { - /** - * Article list - */ - async knowledgeBaseArticles(_root, args: { page: number; perPage: number; categoryIds: string[] }) { - const query = await articlesQuery(args); - const articles = KnowledgeBaseArticles.find(query).sort({ - createdData: -1, - }); - - return paginate(articles, args); - }, - - /** - * Article detail - */ - knowledgeBaseArticleDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseArticles.findOne({ _id }); - }, - - /** - * Total article count - */ - async knowledgeBaseArticlesTotalCount(_root, args: { categoryIds: string[] }) { - const query = await articlesQuery(args); - - return KnowledgeBaseArticles.find(query).countDocuments(); - }, - - /** - * Category list - */ - async knowledgeBaseCategories(_root, args: { page: number; perPage: number; topicIds: string[] }) { - const query = await categoriesQuery(args); - - const categories = KnowledgeBaseCategories.find(query).sort({ - modifiedDate: -1, - }); - - return paginate(categories, args); - }, - - /** - * Category detail - */ - knowledgeBaseCategoryDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseCategories.findOne({ _id }).then(category => { - return category; - }); - }, - - /** - * Category total count - */ - async knowledgeBaseCategoriesTotalCount(_root, args: { topicIds: string[] }) { - const query = await categoriesQuery(args); - - return KnowledgeBaseCategories.find(query).countDocuments(); - }, - - /** - * Get last category - */ - knowledgeBaseCategoriesGetLast() { - return KnowledgeBaseCategories.findOne({}).sort({ createdDate: -1 }); - }, - - /** - * Topic list - */ - knowledgeBaseTopics(_root, args: { page: number; perPage: number }) { - const topics = paginate(KnowledgeBaseTopics.find({}), args); - return topics.sort({ modifiedDate: -1 }); - }, - - /** - * Topic detail - */ - knowledgeBaseTopicDetail(_root, { _id }: { _id: string }) { - return KnowledgeBaseTopics.findOne({ _id }); - }, - - /** - * Total topic count - */ - knowledgeBaseTopicsTotalCount() { - return KnowledgeBaseTopics.find({}).countDocuments(); - }, -}; - -requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticleDetail'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticlesTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicsTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicDetail'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesGetLast'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesTotalCount'); -requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoryDetail'); - -checkPermission(knowledgeBaseQueries, 'knowledgeBaseArticles', 'showKnowledgeBase', []); -checkPermission(knowledgeBaseQueries, 'knowledgeBaseTopics', 'showKnowledgeBase', []); -checkPermission(knowledgeBaseQueries, 'knowledgeBaseCategories', 'showKnowledgeBase', []); - -export default knowledgeBaseQueries; +import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } from '../../../db/models'; + +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { paginate } from '../../utils'; + +/* Articles list & total count helper */ +const articlesQuery = async ({ categoryIds }: { categoryIds: string[] }) => { + const query: any = {}; + + // filter articles by category i + if (categoryIds) { + const categories = await KnowledgeBaseCategories.find({ + _id: { + $in: categoryIds, + }, + }); + + let articleIds: any = []; + + for (const category of categories) { + articleIds = articleIds.concat(category.articleIds || []); + } + + query._id = { + $in: articleIds, + }; + } + + return query; +}; + +/* Categories list & total count helper */ +const categoriesQuery = async ({ topicIds }: { topicIds: string[] }) => { + const query: any = {}; + + // filter categories by topic id + if (topicIds) { + let categoryIds: any = []; + + const topics = await KnowledgeBaseTopics.find({ + _id: { + $in: topicIds, + }, + }); + + for (const topic of topics) { + categoryIds = categoryIds.concat(topic.categoryIds || []); + } + + query._id = { + $in: categoryIds, + }; + } + + return query; +}; + +const knowledgeBaseQueries = { + /** + * Article list + */ + async knowledgeBaseArticles(_root, args: { page: number; perPage: number; categoryIds: string[] }) { + const query = await articlesQuery(args); + + const articles = KnowledgeBaseArticles.find(query).sort({ + createdData: -1, + }); + + return paginate(articles, args); + }, + + /** + * Article detail + */ + knowledgeBaseArticleDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseArticles.findOne({ _id }); + }, + + /** + * Total article count + */ + async knowledgeBaseArticlesTotalCount(_root, args: { categoryIds: string[] }) { + const query = await articlesQuery(args); + + return KnowledgeBaseArticles.find(query).countDocuments(); + }, + + /** + * Category list + */ + async knowledgeBaseCategories(_root, args: { page: number; perPage: number; topicIds: string[] }) { + const query = await categoriesQuery(args); + + const categories = KnowledgeBaseCategories.find(query).sort({ + modifiedDate: -1, + }); + + return paginate(categories, args); + }, + + /** + * Category detail + */ + knowledgeBaseCategoryDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseCategories.findOne({ _id }).then(category => { + return category; + }); + }, + + /** + * Category total count + */ + async knowledgeBaseCategoriesTotalCount(_root, args: { topicIds: string[] }) { + const query = await categoriesQuery(args); + + return KnowledgeBaseCategories.find(query).countDocuments(); + }, + + /** + * Get last category + */ + knowledgeBaseCategoriesGetLast(_root, _args, { commonQuerySelector }: IContext) { + return KnowledgeBaseCategories.findOne(commonQuerySelector).sort({ createdDate: -1 }); + }, + + /** + * Topic list + */ + knowledgeBaseTopics(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + const topics = paginate(KnowledgeBaseTopics.find(commonQuerySelector), args); + return topics.sort({ modifiedDate: -1 }); + }, + + /** + * Topic detail + */ + knowledgeBaseTopicDetail(_root, { _id }: { _id: string }) { + return KnowledgeBaseTopics.findOne({ _id }); + }, + + /** + * Total topic count + */ + knowledgeBaseTopicsTotalCount(_root, _args, { commonQuerySelector }: IContext) { + return KnowledgeBaseTopics.find(commonQuerySelector).countDocuments(); + }, +}; + +requireLogin(knowledgeBaseQueries, 'knowledgeBaseArticlesTotalCount'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseTopicsTotalCount'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesGetLast'); +requireLogin(knowledgeBaseQueries, 'knowledgeBaseCategoriesTotalCount'); + +checkPermission(knowledgeBaseQueries, 'knowledgeBaseArticles', 'showKnowledgeBase', []); +checkPermission(knowledgeBaseQueries, 'knowledgeBaseTopics', 'showKnowledgeBase', []); +checkPermission(knowledgeBaseQueries, 'knowledgeBaseCategories', 'showKnowledgeBase', []); + +export default knowledgeBaseQueries; diff --git a/src/data/resolvers/queries/logs.ts b/src/data/resolvers/queries/logs.ts index d42b774df..ba56c0dc6 100644 --- a/src/data/resolvers/queries/logs.ts +++ b/src/data/resolvers/queries/logs.ts @@ -1,27 +1,251 @@ -import { fetchLogs, ILogQueryParams } from '../../utils'; +import { attachmentSchema, boardSchema, pipelineSchema } from '../../../db/models/definitions/boards'; +import { brandEmailConfigSchema, brandSchema } from '../../../db/models/definitions/brands'; +import { channelSchema } from '../../../db/models/definitions/channels'; +import { checklistItemSchema, checklistSchema } from '../../../db/models/definitions/checklists'; +import { ruleSchema } from '../../../db/models/definitions/common'; +import { companySchema } from '../../../db/models/definitions/companies'; +import { customerSchema, locationSchema, visitorContactSchema } from '../../../db/models/definitions/customers'; +import { + dealSchema, + productCategorySchema, + productDataSchema, + productSchema, +} from '../../../db/models/definitions/deals'; +import { emailTemplateSchema } from '../../../db/models/definitions/emailTemplates'; +import { + emailSchema, + engageMessageSchema, + messengerSchema, + scheduleDateSchema, +} from '../../../db/models/definitions/engages'; +import { growthHackSchema } from '../../../db/models/definitions/growthHacks'; +import { importHistorySchema } from '../../../db/models/definitions/importHistory'; +import { calloutSchema, integrationSchema, leadDataSchema } from '../../../db/models/definitions/integrations'; +import { internalNoteSchema } from '../../../db/models/definitions/internalNotes'; +import { articleSchema, categorySchema, topicSchema } from '../../../db/models/definitions/knowledgebase'; +import { permissionSchema, userGroupSchema } from '../../../db/models/definitions/permissions'; +import { pipelineLabelSchema } from '../../../db/models/definitions/pipelineLabels'; +import { pipelineTemplateSchema, stageSchema } from '../../../db/models/definitions/pipelineTemplates'; +import { responseTemplateSchema } from '../../../db/models/definitions/responseTemplates'; +import { scriptSchema } from '../../../db/models/definitions/scripts'; +import { conditionSchema, segmentSchema } from '../../../db/models/definitions/segments'; +import { tagSchema } from '../../../db/models/definitions/tags'; +import { taskSchema } from '../../../db/models/definitions/tasks'; +import { ticketSchema } from '../../../db/models/definitions/tickets'; +import { userSchema } from '../../../db/models/definitions/users'; +import { MODULE_NAMES } from '../../constants'; +import { fetchLogs, ILogQueryParams } from '../../logUtils'; +import { checkPermission } from '../../permissions/wrappers'; + +interface INameLabel { + name: string; + label: string; +} + +interface ISchemaMap { + name: string; + schemas: any[]; +} + +const LOG_MAPPINGS: ISchemaMap[] = [ + { + name: MODULE_NAMES.BOARD_DEAL, + schemas: [attachmentSchema, boardSchema], + }, + { + name: MODULE_NAMES.BOARD_TASK, + schemas: [attachmentSchema, boardSchema], + }, + { + name: MODULE_NAMES.BOARD_TICKET, + schemas: [attachmentSchema, boardSchema], + }, + { + name: MODULE_NAMES.PIPELINE_DEAL, + schemas: [pipelineSchema], + }, + { + name: MODULE_NAMES.PIPELINE_TASK, + schemas: [pipelineSchema], + }, + { + name: MODULE_NAMES.PIPELINE_TICKET, + schemas: [pipelineSchema], + }, + { + name: MODULE_NAMES.BRAND, + schemas: [brandEmailConfigSchema, brandSchema], + }, + { + name: MODULE_NAMES.CHANNEL, + schemas: [channelSchema], + }, + { + name: MODULE_NAMES.CHECKLIST, + schemas: [checklistSchema], + }, + { + name: MODULE_NAMES.CHECKLIST_ITEM, + schemas: [checklistItemSchema], + }, + { + name: MODULE_NAMES.COMPANY, + schemas: [companySchema], + }, + { + name: MODULE_NAMES.CUSTOMER, + schemas: [customerSchema, locationSchema, visitorContactSchema], + }, + { + name: MODULE_NAMES.DEAL, + schemas: [dealSchema, productDataSchema], + }, + { + name: MODULE_NAMES.EMAIL_TEMPLATE, + schemas: [emailTemplateSchema], + }, + { + name: MODULE_NAMES.IMPORT_HISTORY, + schemas: [importHistorySchema], + }, + { + name: MODULE_NAMES.TAG, + schemas: [tagSchema], + }, + { + name: MODULE_NAMES.RESPONSE_TEMPLATE, + schemas: [responseTemplateSchema], + }, + { + name: MODULE_NAMES.PRODUCT, + schemas: [productSchema, tagSchema], + }, + { + name: MODULE_NAMES.PRODUCT_CATEGORY, + schemas: [productCategorySchema], + }, + { + name: MODULE_NAMES.KB_TOPIC, + schemas: [topicSchema], + }, + { + name: MODULE_NAMES.KB_CATEGORY, + schemas: [categorySchema], + }, + { + name: MODULE_NAMES.KB_ARTICLE, + schemas: [articleSchema], + }, + { + name: MODULE_NAMES.PERMISSION, + schemas: [permissionSchema], + }, + { + name: MODULE_NAMES.USER_GROUP, + schemas: [userGroupSchema], + }, + { + name: MODULE_NAMES.INTERNAL_NOTE, + schemas: [internalNoteSchema], + }, + { + name: MODULE_NAMES.PIPELINE_LABEL, + schemas: [pipelineLabelSchema], + }, + { + name: MODULE_NAMES.PIPELINE_TEMPLATE, + schemas: [pipelineTemplateSchema, stageSchema], + }, + { + name: MODULE_NAMES.TASK, + schemas: [taskSchema, attachmentSchema], + }, + { + name: MODULE_NAMES.GROWTH_HACK, + schemas: [growthHackSchema, attachmentSchema], + }, + { + name: MODULE_NAMES.INTEGRATION, + schemas: [calloutSchema, integrationSchema, leadDataSchema, ruleSchema], + }, + { + name: MODULE_NAMES.TICKET, + schemas: [ticketSchema, attachmentSchema], + }, + { + name: MODULE_NAMES.SEGMENT, + schemas: [conditionSchema, segmentSchema], + }, + { + name: MODULE_NAMES.ENGAGE, + schemas: [engageMessageSchema, emailSchema, messengerSchema, scheduleDateSchema], + }, + { + name: MODULE_NAMES.SCRIPT, + schemas: [scriptSchema], + }, + { + name: MODULE_NAMES.USER, + schemas: [userSchema], + }, +]; + +/** + * Creates field name-label mapping list from given object + */ +const buildLabelList = (obj = {}): INameLabel[] => { + const list: INameLabel[] = []; + const fieldNames: string[] = Object.getOwnPropertyNames(obj); + + for (const name of fieldNames) { + const field: any = obj[name]; + const label: string = field && field.label ? field.label : ''; + + list.push({ name, label }); + } + + return list; +}; const logQueries = { /** * Fetches logs from logs api server - * @param {string} params.start Start date - * @param {string} params.end End date - * @param {string} params.userId User - * @param {string} params.action Action (one of create|update|delete) - * @param {string} params.page - * @param {string} params.perPage */ logs(_root, params: ILogQueryParams) { - const { start, end, userId, action, page, perPage } = params; + return fetchLogs(params); + }, + + async getDbSchemaLabels(_root, params: { type: string }) { + let fieldNames: INameLabel[] = []; + + const found: ISchemaMap | undefined = LOG_MAPPINGS.find(m => m.name === params.type); - return fetchLogs({ - start, - end, - userId, - action, - page, - perPage, - }); + if (found) { + const schemas: any = found.schemas || []; + + for (const schema of schemas) { + // schema comes as either mongoose schema or plain object + const names: string[] = Object.getOwnPropertyNames(schema.obj || schema); + + for (const name of names) { + const field: any = schema.obj ? schema.obj[name] : schema[name]; + + if (field && field.label) { + fieldNames.push({ name, label: field.label }); + } + + // nested object field names + if (typeof field === 'object' && field.type && field.type.obj) { + fieldNames = fieldNames.concat(buildLabelList(field.type.obj)); + } + } + } // end schema for loop + } // end schema name mapping + + return fieldNames; }, }; +checkPermission(logQueries, 'logs', 'viewLogs'); + export default logQueries; diff --git a/src/data/resolvers/queries/messengerApps.ts b/src/data/resolvers/queries/messengerApps.ts deleted file mode 100644 index 979690bd9..000000000 --- a/src/data/resolvers/queries/messengerApps.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { MessengerApps } from '../../../db/models'; -import { moduleRequireLogin } from '../../permissions/wrappers'; - -const messengerAppQueries = { - /* - * MessengerApps list - */ - messengerApps(_root, { kind }: { kind: string }) { - const query: any = {}; - - if (kind) { - query.kind = kind; - } - - return MessengerApps.find(query); - }, - - /* - * MessengerApps count - */ - messengerAppsCount(_root, { kind }: { kind: string }) { - const query: any = {}; - - if (kind) { - query.kind = kind; - } - - return MessengerApps.find(query).countDocuments(); - }, -}; - -moduleRequireLogin(messengerAppQueries); - -export default messengerAppQueries; diff --git a/src/data/resolvers/queries/notifications.ts b/src/data/resolvers/queries/notifications.ts index f9c2b4096..6667572f0 100644 --- a/src/data/resolvers/queries/notifications.ts +++ b/src/data/resolvers/queries/notifications.ts @@ -1,7 +1,7 @@ import { NotificationConfigurations, Notifications } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { NOTIFICATION_MODULES } from '../../constants'; import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const notificationQueries = { @@ -22,7 +22,7 @@ const notificationQueries = { page: number; perPage: number; }, - { user }: { user: IUserDocument }, + { user }: IContext, ) { const sort = { date: -1 }; const selector: any = { receiver: user._id }; @@ -47,7 +47,7 @@ const notificationQueries = { /** * Notification counts */ - notificationCounts(_root, { requireRead }: { requireRead: boolean }, { user }: { user: IUserDocument }) { + notificationCounts(_root, { requireRead }: { requireRead: boolean }, { user }: IContext) { const selector: any = { receiver: user._id }; if (requireRead) { @@ -67,7 +67,7 @@ const notificationQueries = { /** * Get per user configuration */ - notificationsGetConfigurations(_root, _args, { user }: { user: IUserDocument }) { + notificationsGetConfigurations(_root, _args, { user }: IContext) { return NotificationConfigurations.find({ user: user._id }); }, }; diff --git a/src/data/resolvers/queries/permissions.ts b/src/data/resolvers/queries/permissions.ts index 3f3c1a81e..6475c19a7 100644 --- a/src/data/resolvers/queries/permissions.ts +++ b/src/data/resolvers/queries/permissions.ts @@ -1,6 +1,6 @@ import * as _ from 'underscore'; -import { Permissions, UsersGroups } from '../../../db/models'; -import { actionsMap, IActionsMap, IModulesMap, modulesMap } from '../../permissions/utils'; +import { Permissions, Users, UsersGroups } from '../../../db/models'; +import { actionsMap, IActionsMap, IModuleMap, modulesMap } from '../../permissions/utils'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; import { paginate } from '../../utils'; @@ -10,7 +10,7 @@ interface IListArgs { searchValue?: string; } -const generateSelector = ({ module, action, userId, groupId }) => { +const generateSelector = async ({ module, action, userId, groupId, allowed }) => { const filter: any = {}; if (module) { @@ -21,8 +21,22 @@ const generateSelector = ({ module, action, userId, groupId }) => { filter.action = action; } + filter.allowed = typeof allowed === 'undefined' ? true : allowed; + if (userId) { - filter.userId = userId; + const user = await Users.findOne({ _id: userId }); + + let permissionIds: string[] = []; + + if (user) { + const groups = await UsersGroups.find({ _id: { $in: user.groupIds } }, { _id: 1 }); + const groupIds = groups.map(group => group._id); + const permissions = await Permissions.find({ groupId: { $in: groupIds } }); + + permissionIds = permissions.map(permission => permission._id); + } + + filter.$or = [{ userId }, { _id: { $in: permissionIds } }]; } if (groupId) { @@ -43,13 +57,14 @@ const permissionQueries = { * @param {Int} args.perPage * @return {Promise} filtered permissions list by given parameter */ - permissions(_root, { module, action, userId, groupId, ...args }) { - const filter = generateSelector({ module, action, userId, groupId }); + async permissions(_root, { module, action, userId, groupId, allowed, ...args }) { + const filter = await generateSelector({ module, action, userId, groupId, allowed }); + return paginate(Permissions.find(filter), args); }, permissionModules() { - const modules: IModulesMap[] = []; + const modules: IModuleMap[] = []; for (const m of _.pairs(modulesMap)) { modules.push({ name: m[0], description: m[1] }); @@ -79,8 +94,8 @@ const permissionQueries = { * @param {String} args.userId * @return {Promise} total count */ - permissionsTotalCount(_root, args) { - const filter = generateSelector(args); + async permissionsTotalCount(_root, args) { + const filter = await generateSelector(args); return Permissions.find(filter).countDocuments(); }, }; diff --git a/src/data/resolvers/queries/pipelineLabels.ts b/src/data/resolvers/queries/pipelineLabels.ts new file mode 100644 index 000000000..c86811cc5 --- /dev/null +++ b/src/data/resolvers/queries/pipelineLabels.ts @@ -0,0 +1,22 @@ +import { PipelineLabels } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; + +const pipelineLabelQueries = { + /** + * Pipeline label list + */ + pipelineLabels(_root, { pipelineId }: { pipelineId: string }) { + return PipelineLabels.find({ pipelineId }); + }, + + /** + * Pipeline label detail + */ + pipelineLabelDetail(_root, { _id }: { _id: string }) { + return PipelineLabels.findOne({ _id }); + }, +}; + +moduleRequireLogin(pipelineLabelQueries); + +export default pipelineLabelQueries; diff --git a/src/data/resolvers/queries/pipelineTemplates.ts b/src/data/resolvers/queries/pipelineTemplates.ts new file mode 100644 index 000000000..5bcba7894 --- /dev/null +++ b/src/data/resolvers/queries/pipelineTemplates.ts @@ -0,0 +1,37 @@ +import { PipelineTemplates } from '../../../db/models'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { checkPermission } from '../boardUtils'; + +const pipelineTemplateQueries = { + /** + * Pipeline template list + */ + async pipelineTemplates(_root, { type }: { type: string }, { user }: IContext) { + await checkPermission(type, user, 'showTemplates'); + + return PipelineTemplates.find({ type }); + }, + + /** + * Pipeline template detail + */ + async pipelineTemplateDetail(_root, { _id }: { _id: string }, { user }: IContext) { + const pipelineTemplate = await PipelineTemplates.getPipelineTemplate(_id); + + await checkPermission(pipelineTemplate.type, user, 'showTemplates'); + + return PipelineTemplates.findOne({ _id }); + }, + + /** + * Pipeline template total count + */ + pipelineTemplatesTotalCount() { + return PipelineTemplates.find().countDocuments(); + }, +}; + +moduleRequireLogin(pipelineTemplateQueries); + +export default pipelineTemplateQueries; diff --git a/src/data/resolvers/queries/products.ts b/src/data/resolvers/queries/products.ts index 029d3d7fa..2a70c2ea5 100644 --- a/src/data/resolvers/queries/products.ts +++ b/src/data/resolvers/queries/products.ts @@ -1,5 +1,7 @@ -import { Products } from '../../../db/models'; +import { ProductCategories, Products, Tags } from '../../../db/models'; +import { TAG_TYPES } from '../../../db/models/definitions/constants'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const productQueries = { @@ -10,24 +12,48 @@ const productQueries = { _root, { type, + categoryId, searchValue, + tag, ids, ...pagintationArgs - }: { ids: string[]; type: string; searchValue: string; page: number; perPage: number }, + }: { + ids: string[]; + type: string; + categoryId: string; + searchValue: string; + tag: string; + page: number; + perPage: number; + }, + { commonQuerySelector }: IContext, ) { - const filter: any = {}; + const filter: any = commonQuerySelector; if (type) { filter.type = type; } + if (categoryId) { + filter.categoryId = categoryId; + } + if (ids) { filter._id = { $in: ids }; } + if (tag) { + filter.tagIds = { $in: [tag] }; + } + // search ========= if (searchValue) { - filter.name = new RegExp(`.*${searchValue}.*`, 'i'); + const fields = [ + { name: { $in: [new RegExp(`.*${searchValue}.*`, 'i')] } }, + { code: { $in: [new RegExp(`.*${searchValue}.*`, 'i')] } }, + ]; + + filter.$or = fields; } return paginate(Products.find(filter), pagintationArgs); @@ -36,8 +62,8 @@ const productQueries = { /** * Get all products count. We will use it in pager */ - productsTotalCount(_root, { type }: { type: string }) { - const filter: any = {}; + productsTotalCount(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + const filter: any = commonQuerySelector; if (type) { filter.type = type; @@ -45,9 +71,54 @@ const productQueries = { return Products.find(filter).countDocuments(); }, + + productCategories( + _root, + { parentId, searchValue }: { parentId: string; searchValue: string }, + { commonQuerySelector }: IContext, + ) { + const filter: any = commonQuerySelector; + + if (parentId) { + filter.parentId = parentId; + } + + if (searchValue) { + filter.name = new RegExp(`.*${searchValue}.*`, 'i'); + } + + return ProductCategories.find(filter).sort({ order: 1 }); + }, + + productCategoriesTotalCount(_root) { + return ProductCategories.find().countDocuments(); + }, + + productDetail(_root, { _id }: { _id: string }) { + return Products.findOne({ _id }); + }, + + productCategoryDetail(_root, { _id }: { _id: string }) { + return ProductCategories.findOne({ _id }); + }, + + async productCountByTags() { + const counts = {}; + + // Count products by tag ========= + const tags = await Tags.find({ type: TAG_TYPES.PRODUCT }); + + for (const tag of tags) { + counts[tag._id] = await Products.find({ tagIds: tag._id }).countDocuments(); + } + + return counts; + }, }; requireLogin(productQueries, 'productsTotalCount'); checkPermission(productQueries, 'products', 'showProducts', []); +checkPermission(productQueries, 'productCategories', 'showProducts', []); +checkPermission(productQueries, 'productCountByTags', 'showProducts', []); export default productQueries; diff --git a/src/data/resolvers/queries/responseTemplates.ts b/src/data/resolvers/queries/responseTemplates.ts index ebf92f18d..1d50cd48b 100644 --- a/src/data/resolvers/queries/responseTemplates.ts +++ b/src/data/resolvers/queries/responseTemplates.ts @@ -1,5 +1,6 @@ import { ResponseTemplates } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; interface IListParams { @@ -9,20 +10,17 @@ interface IListParams { searchValue: string; } -const generateFilter = (args: IListParams) => { +const generateFilter = (commonSelector, args: IListParams) => { const { brandId, searchValue } = args; - const filter: any = {}; + const filter: any = commonSelector; if (brandId) { filter.brandId = brandId; } if (searchValue) { - filter.$or = [ - { name: new RegExp(`.*${searchValue || ''}.*`, 'i') }, - { content: new RegExp(`.*${searchValue || ''}.*`, 'i') }, - ]; + filter.$or = [{ name: new RegExp(`.*${searchValue}.*`, 'i') }, { content: new RegExp(`.*${searchValue}.*`, 'i') }]; } return filter; @@ -32,8 +30,8 @@ const responseTemplateQueries = { /** * Response templates list */ - responseTemplates(_root, args: IListParams) { - const filter = generateFilter(args); + responseTemplates(_root, args: IListParams, { commonQuerySelector }: IContext) { + const filter = generateFilter(commonQuerySelector, args); return paginate(ResponseTemplates.find(filter), args); }, @@ -41,8 +39,8 @@ const responseTemplateQueries = { /** * Get all response templates count. We will use it in pager */ - responseTemplatesTotalCount(_root, args: IListParams) { - const filter = generateFilter(args); + responseTemplatesTotalCount(_root, args: IListParams, { commonQuerySelector }: IContext) { + const filter = generateFilter(commonQuerySelector, args); return ResponseTemplates.find(filter).countDocuments(); }, diff --git a/src/data/resolvers/queries/robot.ts b/src/data/resolvers/queries/robot.ts new file mode 100644 index 000000000..3773b2a69 --- /dev/null +++ b/src/data/resolvers/queries/robot.ts @@ -0,0 +1,182 @@ +import { RobotEntries } from '../../../db/models'; +import { IUserDocument } from '../../../db/models/definitions/users'; +import { OnboardingHistories } from '../../../db/models/Robot'; +import { moduleObjects } from '../../permissions/actions/permission'; +import { getUserAllowedActions, IModuleMap } from '../../permissions/utils'; +import { moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +const features: { [key: string]: { settings: string[]; settingsPermissions: string[] } } = { + growthHacks: { + settings: [ + 'growthHackBoardsCreate', + 'growthHackPipelinesCreate', + 'growthHackTemplatesDuplicate', + 'growthHackCreate', + ], + settingsPermissions: [ + 'growthHackBoardsAdd', + 'growthHackPipelinesAdd', + 'growthHackStagesAdd', + 'growthHackTemplatesDuplicate', + ], + }, + deals: { + settings: ['dealBoardsCreate', 'dealPipelinesCreate', 'productCreate', 'dealCreate'], + settingsPermissions: ['dealBoardsAdd', 'dealPipelinesAdd', 'dealStagesAdd', 'manageProducts'], + }, + inbox: { + settings: [ + 'brandCreate', + 'channelCreate', + 'messengerIntegrationCreate', + 'connectIntegrationsToChannel', + 'responseTemplateCreate', + ], + settingsPermissions: [ + 'manageBrands', + 'manageChannels', + 'integrationsCreateMessengerIntegration', + 'manageResponseTemplate', + ], + }, + engages: { + settings: ['engageVerifyEmail', 'engageSendTestEmail', 'emailTemplateCreate', 'segmentCreate', 'engageCreate'], + settingsPermissions: ['manageEmailTemplate', 'manageSegments', 'engageMessageAdd', 'engageMessageRemove'], + }, + contacts: { + settings: ['leadCreate', 'customerCreate', 'companyCreate', 'productCreate', 'fieldCreate', 'tagCreate'], + settingsPermissions: ['customersAdd', 'companiesAdd', 'manageProducts', 'manageTags', 'manageForms'], + }, + integrations: { + settings: [ + 'brandCreate', + 'messengerIntegrationCreate', + 'connectIntegrationsToChannel', + 'messengerIntegrationInstalled', + ], + settingsPermissions: ['integrationsCreateMessengerIntegration', 'manageChannels', 'manageBrands'], + }, + leads: { + settings: ['brandCreate', 'leadIntegrationCreate', 'leadIntegrationInstalled'], + settingsPermissions: ['integrationsCreateLeadIntegration', 'manageBrands'], + }, + knowledgeBase: { + settings: [ + 'brandCreate', + 'knowledgeBaseTopicCreate', + 'knowledgeBaseCategoryCreate', + 'knowledgeBaseArticleCreate', + 'knowledgeBaseInstalled', + ], + settingsPermissions: ['manageKnowledgeBase', 'manageBrands'], + }, + tasks: { + settings: ['taskBoardsCreate', 'taskPipelinesCreate', 'taskCreate', 'taskAssignUser'], + settingsPermissions: ['taskBoardsAdd', 'taskPipelinesAdd', 'taskStagesAdd', 'taskAdd', 'taskEdit'], + }, +}; + +const checkShowModule = ( + user: IUserDocument, + actionsMap, + moduleName: string, +): { showModule: boolean; showSettings: boolean } => { + if (user.isOwner) { + return { + showModule: true, + showSettings: true, + }; + } + + const module: IModuleMap = moduleObjects[moduleName]; + + interface IAction { + name: string; + description?: string; + } + + let actions: IAction[] = []; + let showModule = false; + let showSettings = true; + + if (!module) { + if (moduleName === 'leads') { + actions = [{ name: 'integrationsCreateLeadIntegration' }]; + } + + if (moduleName === 'properties') { + actions = [{ name: 'manageForms' }]; + } + } else { + actions = module.actions as IAction[]; + } + + for (const action of actions) { + if (actionsMap.includes(action.name || '')) { + showModule = true; + break; + } + } + + for (const permission of features[moduleName].settingsPermissions) { + if (!actionsMap.includes(permission)) { + showSettings = false; + break; + } + } + + return { + showModule, + showSettings, + }; +}; + +const robotQueries = { + robotEntries(_root, { isNotified, action, parentId }: { isNotified: boolean; action: string; parentId: string }) { + const selector: any = { parentId, action }; + + if (typeof isNotified !== 'undefined') { + selector.isNotified = isNotified; + } + + return RobotEntries.find(selector); + }, + + onboardingStepsCompleteness(_root, { steps }: { steps: string[] }, { user }: IContext) { + return OnboardingHistories.stepsCompletness(steps, user); + }, + + async onboardingGetAvailableFeatures(_root, _args, { user }: IContext) { + const results: Array<{ name: string; isComplete: boolean; settings?: string[]; showSettings?: boolean }> = []; + const actionsMap = await getUserAllowedActions(user); + + for (const feature of Object.keys(features)) { + const { settings } = features[feature]; + const { showModule, showSettings } = checkShowModule(user, actionsMap, feature); + + if (showModule) { + let steps: string[] = []; + + if (showSettings) { + steps = [...steps, ...settings]; + } + + const selector = { userId: user._id, completedSteps: { $all: steps } }; + + results.push({ + name: feature, + settings, + showSettings, + isComplete: (await OnboardingHistories.find(selector).countDocuments()) > 0, + }); + } + } + + return results; + }, +}; + +moduleRequireLogin(robotQueries); + +export default robotQueries; diff --git a/src/data/resolvers/queries/scripts.ts b/src/data/resolvers/queries/scripts.ts index 289dff864..dfefae368 100644 --- a/src/data/resolvers/queries/scripts.ts +++ b/src/data/resolvers/queries/scripts.ts @@ -1,20 +1,21 @@ import { Scripts } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; const scriptQueries = { /** * Scripts list */ - scripts(_root, args: { page: number; perPage: number }) { - return paginate(Scripts.find({}), args); + scripts(_root, args: { page: number; perPage: number }, { commonQuerySelector }: IContext) { + return paginate(Scripts.find(commonQuerySelector), args); }, /** * Get all scripts count. We will use it in pager */ - scriptsTotalCount() { - return Scripts.find({}).countDocuments(); + scriptsTotalCount(_root, _args, { commonQuerySelector }: IContext) { + return Scripts.find(commonQuerySelector).countDocuments(); }, }; diff --git a/src/data/resolvers/queries/segments.ts b/src/data/resolvers/queries/segments.ts index 2237756c6..234243dca 100644 --- a/src/data/resolvers/queries/segments.ts +++ b/src/data/resolvers/queries/segments.ts @@ -1,19 +1,22 @@ import { Segments } from '../../../db/models'; +import { fetchElk } from '../../../elasticsearch'; +import { fetchBySegments } from '../../modules/segments/queryBuilder'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const segmentQueries = { /** * Segments list */ - segments(_root, { contentType }: { contentType: string }) { - return Segments.find({ contentType }).sort({ name: 1 }); + segments(_root, { contentTypes }: { contentTypes: string[] }, { commonQuerySelector }: IContext) { + return Segments.find({ ...commonQuerySelector, contentType: { $in: contentTypes } }).sort({ name: 1 }); }, /** * Only segment that has no sub segments */ - async segmentsGetHeads() { - return Segments.find({ $or: [{ subOf: { $exists: false } }, { subOf: '' }] }); + async segmentsGetHeads(_root, _args, { commonQuerySelector }: IContext) { + return Segments.find({ ...commonQuerySelector, $or: [{ subOf: { $exists: false } }, { subOf: '' }] }); }, /** @@ -22,10 +25,85 @@ const segmentQueries = { segmentDetail(_root, { _id }: { _id: string }) { return Segments.findOne({ _id }); }, + + /** + * Return event names with attribute names + */ + async segmentsEvents(_root, { contentType }: { contentType: string }) { + const aggs = { + names: { + terms: { + field: 'name', + }, + aggs: { + hits: { + top_hits: { + _source: ['attributes'], + size: 1, + }, + }, + }, + }, + }; + + const query = { + exists: { + field: contentType === 'customer' ? 'customerId' : 'companyId', + }, + }; + + const aggreEvents = await fetchElk('search', 'events', { + aggs, + query, + }); + + const buckets = aggreEvents.aggregations.names.buckets || []; + + const events = buckets.map(bucket => { + const [hit] = bucket.hits.hits.hits; + + return { + name: bucket.key, + attributeNames: hit._source.attributes.map(attr => attr.field), + }; + }); + + return events; + }, + + /** + * Preview count + */ + async segmentsPreviewCount( + _root, + { contentType, conditions, subOf }: { contentType: string; conditions; subOf?: string }, + ) { + const { positiveList, negativeList } = await fetchBySegments( + { name: 'preview', color: '#fff', subOf: subOf || '', contentType, conditions }, + 'count', + ); + + try { + const response = await fetchElk('count', contentType === 'company' ? 'companies' : 'customers', { + query: { + bool: { + must: positiveList, + must_not: negativeList, + }, + }, + }); + + return response.count; + } catch (e) { + return 0; + } + }, }; requireLogin(segmentQueries, 'segmentsGetHeads'); requireLogin(segmentQueries, 'segmentDetail'); +requireLogin(segmentQueries, 'segmentsPreviewCount'); +requireLogin(segmentQueries, 'segmentsEvents'); checkPermission(segmentQueries, 'segments', 'showSegments', []); diff --git a/src/data/resolvers/queries/tags.ts b/src/data/resolvers/queries/tags.ts index 1bb263fde..1bc366f0e 100644 --- a/src/data/resolvers/queries/tags.ts +++ b/src/data/resolvers/queries/tags.ts @@ -1,12 +1,13 @@ import { Tags } from '../../../db/models'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; const tagQueries = { /** * Tags list */ - tags(_root, { type }: { type: string }) { - return Tags.find({ type }).sort({ name: 1 }); + tags(_root, { type }: { type: string }, { commonQuerySelector }: IContext) { + return Tags.find({ ...commonQuerySelector, type }).sort({ name: 1 }); }, /** diff --git a/src/data/resolvers/queries/tasks.ts b/src/data/resolvers/queries/tasks.ts index 0d1e46703..82af9e48b 100644 --- a/src/data/resolvers/queries/tasks.ts +++ b/src/data/resolvers/queries/tasks.ts @@ -1,32 +1,53 @@ -import { Tasks } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateTaskCommonFilters } from './boardUtils'; - -const taskQueries = { - /** - * Tasks list - */ - async tasks(_root, args: IListParams) { - const filter = await generateTaskCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Tasks.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Tasks detail - */ - taskDetail(_root, { _id }: { _id: string }) { - return Tasks.findOne({ _id }); - }, -}; - -moduleRequireLogin(taskQueries); - -checkPermission(taskQueries, 'tasks', 'showTasks', []); - -export default taskQueries; +import { Tasks } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { IListParams } from './boards'; +import { + archivedItems, + archivedItemsCount, + checkItemPermByUser, + generateSort, + generateTaskCommonFilters, + IArchiveArgs, +} from './boardUtils'; + +const taskQueries = { + /** + * Tasks list + */ + async tasks(_root, args: IListParams, { user, commonQuerySelector }: IContext) { + const filter = { ...commonQuerySelector, ...(await generateTaskCommonFilters(user._id, args)) }; + const sort = generateSort(args); + + return Tasks.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Archived list + */ + archivedTasks(_root, args: IArchiveArgs) { + return archivedItems(args, Tasks); + }, + + archivedTasksCount(_root, args: IArchiveArgs) { + return archivedItemsCount(args, Tasks); + }, + + /** + * Tasks detail + */ + async taskDetail(_root, { _id }: { _id: string }, { user }: IContext) { + const task = await Tasks.getTask(_id); + + return checkItemPermByUser(user._id, task); + }, +}; + +moduleRequireLogin(taskQueries); + +checkPermission(taskQueries, 'tasks', 'showTasks', []); + +export default taskQueries; diff --git a/src/data/resolvers/queries/tickets.ts b/src/data/resolvers/queries/tickets.ts index 516585ed6..5cc227f4d 100644 --- a/src/data/resolvers/queries/tickets.ts +++ b/src/data/resolvers/queries/tickets.ts @@ -1,32 +1,53 @@ -import { Tickets } from '../../../db/models'; -import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; -import { IListParams } from './boards'; -import { generateTicketCommonFilters } from './boardUtils'; - -const ticketQueries = { - /** - * Tickets list - */ - async tickets(_root, args: IListParams) { - const filter = await generateTicketCommonFilters(args); - const sort = { order: 1, createdAt: -1 }; - - return Tickets.find(filter) - .sort(sort) - .skip(args.skip || 0) - .limit(10); - }, - - /** - * Tickets detail - */ - ticketDetail(_root, { _id }: { _id: string }) { - return Tickets.findOne({ _id }); - }, -}; - -moduleRequireLogin(ticketQueries); - -checkPermission(ticketQueries, 'tickets', 'showTickets', []); - -export default ticketQueries; +import { Tickets } from '../../../db/models'; +import { checkPermission, moduleRequireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; +import { IListParams } from './boards'; +import { + archivedItems, + archivedItemsCount, + checkItemPermByUser, + generateSort, + generateTicketCommonFilters, + IArchiveArgs, +} from './boardUtils'; + +const ticketQueries = { + /** + * Tickets list + */ + async tickets(_root, args: IListParams, { user, commonQuerySelector }: IContext) { + const filter = { ...commonQuerySelector, ...(await generateTicketCommonFilters(user._id, args)) }; + const sort = generateSort(args); + + return Tickets.find(filter) + .sort(sort) + .skip(args.skip || 0) + .limit(10); + }, + + /** + * Archived list + */ + archivedTickets(_root, args: IArchiveArgs) { + return archivedItems(args, Tickets); + }, + + archivedTicketsCount(_root, args: IArchiveArgs) { + return archivedItemsCount(args, Tickets); + }, + + /** + * Tickets detail + */ + async ticketDetail(_root, { _id }: { _id: string }, { user }: IContext) { + const ticket = await Tickets.getTicket(_id); + + return checkItemPermByUser(user._id, ticket); + }, +}; + +moduleRequireLogin(ticketQueries); + +checkPermission(ticketQueries, 'tickets', 'showTickets', []); + +export default ticketQueries; diff --git a/src/data/resolvers/queries/types.ts b/src/data/resolvers/queries/types.ts new file mode 100644 index 000000000..f6674bb6f --- /dev/null +++ b/src/data/resolvers/queries/types.ts @@ -0,0 +1,6 @@ +export interface IConformityQueryParams { + conformityMainType?: string; + conformityMainTypeId?: string; + conformityIsRelated?: boolean; + conformityIsSaved?: boolean; +} diff --git a/src/data/resolvers/queries/users.ts b/src/data/resolvers/queries/users.ts index 0ec15977e..fe4d03341 100644 --- a/src/data/resolvers/queries/users.ts +++ b/src/data/resolvers/queries/users.ts @@ -1,6 +1,6 @@ import { Conversations, Users } from '../../../db/models'; -import { IUserDocument } from '../../../db/models/definitions/users'; import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; import { paginate } from '../../utils'; interface IListArgs { @@ -8,12 +8,15 @@ interface IListArgs { perPage?: number; searchValue?: string; isActive?: boolean; + requireUsername: boolean; ids?: string[]; + email?: string; status?: string; + brandIds?: string[]; } const queryBuilder = async (params: IListArgs) => { - const { searchValue, isActive, ids, status } = params; + const { searchValue, isActive, requireUsername, ids, status, brandIds } = params; const selector: any = { isActive, @@ -21,6 +24,7 @@ const queryBuilder = async (params: IListArgs) => { if (searchValue) { const fields = [ + { email: new RegExp(`.*${params.searchValue}.*`, 'i') }, { 'details.fullName': new RegExp(`.*${params.searchValue}.*`, 'i') }, { 'details.position': new RegExp(`.*${params.searchValue}.*`, 'i') }, ]; @@ -28,16 +32,24 @@ const queryBuilder = async (params: IListArgs) => { selector.$or = fields; } + if (requireUsername) { + selector.username = { $ne: null }; + } + if (isActive === undefined || isActive === null) { selector.isActive = true; } - if (ids) { - selector._id = { $in: ids }; + if (ids && ids.length > 0) { + return { _id: { $in: ids } }; } if (status) { - selector.registrationToken = { $exists: false, $eq: null }; + selector.registrationToken = { $eq: null }; + } + + if (brandIds && brandIds.length > 0) { + selector.brandIds = { $in: brandIds }; } return selector; @@ -47,13 +59,26 @@ const userQueries = { /** * Users list */ - async users(_root, args: IListArgs) { - const selector = await queryBuilder(args); + async users(_root, args: IListArgs, { userBrandIdsSelector }: IContext) { + const selector = { ...userBrandIdsSelector, ...(await queryBuilder(args)) }; const sort = { username: 1 }; return paginate(Users.find(selector).sort(sort), args); }, + /** + * All users + */ + allUsers(_root, { isActive }: { isActive: boolean }, { userBrandIdsSelector }: IContext) { + const selector: { isActive?: boolean } = userBrandIdsSelector; + + if (isActive) { + selector.isActive = true; + } + + return Users.find(selector).sort({ username: 1 }); + }, + /** * Get one user */ @@ -64,8 +89,8 @@ const userQueries = { /** * Get all users count. We will use it in pager */ - async usersTotalCount(_root, args: IListArgs) { - const selector = await queryBuilder(args); + async usersTotalCount(_root, args: IListArgs, { userBrandIdsSelector }: IContext) { + const selector = { ...userBrandIdsSelector, ...(await queryBuilder(args)) }; return Users.find(selector).countDocuments(); }, @@ -73,12 +98,8 @@ const userQueries = { /** * Current user */ - currentUser(_root, _args, { user }: { user: IUserDocument }) { - if (user) { - return Users.findOne({ _id: user._id, isActive: { $ne: false } }); - } - - return null; + currentUser(_root, _args, { user }: IContext) { + return user ? Users.findOne({ _id: user._id, isActive: { $ne: false } }) : null; }, /** diff --git a/src/data/resolvers/queries/webhooks.ts b/src/data/resolvers/queries/webhooks.ts new file mode 100644 index 000000000..4c363da3f --- /dev/null +++ b/src/data/resolvers/queries/webhooks.ts @@ -0,0 +1,28 @@ +import { Webhooks } from '../../../db/models'; +import { checkPermission, requireLogin } from '../../permissions/wrappers'; +import { IContext } from '../../types'; + +const webhookQueries = { + /** + * Webhooks list + */ + webhooks(_root) { + return Webhooks.find({}); + }, + + /** + * Get one Webhook + */ + webhookDetail(_root, { _id }: { _id: string }) { + return Webhooks.findOne({ _id }); + }, + + async webhooksTotalCount(_root, { commonQuerySelector }: IContext) { + return Webhooks.find({ ...commonQuerySelector }).countDocuments(); + }, +}; + +requireLogin(webhookQueries, 'webhookDetail'); +checkPermission(webhookQueries, 'webhooks', 'showWebhooks', []); + +export default webhookQueries; diff --git a/src/data/resolvers/queries/widgets.ts b/src/data/resolvers/queries/widgets.ts new file mode 100644 index 000000000..93fa719b2 --- /dev/null +++ b/src/data/resolvers/queries/widgets.ts @@ -0,0 +1,188 @@ +import * as momentTz from 'moment-timezone'; +import { + ConversationMessages, + Conversations, + Integrations, + KnowledgeBaseArticles as KnowledgeBaseArticlesModel, + KnowledgeBaseCategories as KnowledgeBaseCategoriesModel, + KnowledgeBaseTopics, + KnowledgeBaseTopics as KnowledgeBaseTopicsModel, + Users, +} from '../../../db/models'; +import Messages from '../../../db/models/ConversationMessages'; +import { IIntegrationDocument } from '../../../db/models/definitions/integrations'; +import { registerOnboardHistory } from '../../utils'; + +export const isMessengerOnline = async (integration: IIntegrationDocument) => { + if (!integration.messengerData) { + return false; + } + + const { availabilityMethod, isOnline, onlineHours, timezone } = integration.messengerData; + + const modifiedIntegration = { + ...integration.toJSON(), + messengerData: { + availabilityMethod, + isOnline, + onlineHours, + timezone, + }, + }; + + return Integrations.isOnline(modifiedIntegration); +}; + +const messengerSupporters = async (integration: IIntegrationDocument) => { + const messengerData = integration.messengerData || { supporterIds: [] }; + + return Users.find({ _id: { $in: messengerData.supporterIds } }); +}; + +const getWidgetMessages = (conversationId: string) => { + return ConversationMessages.find({ + conversationId, + internal: false, + fromBot: { $exists: false }, + }).sort({ + createdAt: 1, + }); +}; + +export default { + /* + * Search published articles that contain searchString (case insensitive) + * in a topic found by topicId + * @return {Promise} searched articles + */ + async widgetsKnowledgeBaseArticles(_root: any, args: { topicId: string; searchString: string }) { + const { topicId, searchString = '' } = args; + + let articleIds: string[] = []; + + const topic = await KnowledgeBaseTopicsModel.findOne({ _id: topicId }); + + if (!topic) { + return []; + } + + const categories = await KnowledgeBaseCategoriesModel.find({ + _id: topic.categoryIds, + }); + + categories.forEach(category => { + articleIds = [...articleIds, ...(category.articleIds || [])]; + }); + + return KnowledgeBaseArticlesModel.find({ + _id: { $in: articleIds }, + content: { $regex: `.*${searchString.trim()}.*`, $options: 'i' }, + status: 'publish', + }); + }, + + widgetsGetMessengerIntegration(_root, args: { brandCode: string }) { + return Integrations.getWidgetIntegration(args.brandCode, 'messenger'); + }, + + widgetsConversations(_root, args: { integrationId: string; customerId: string }) { + const { integrationId, customerId } = args; + + return Conversations.find({ + integrationId, + customerId, + }).sort({ createdAt: -1 }); + }, + + async widgetsConversationDetail(_root, args: { _id: string; integrationId: string }) { + const { _id, integrationId } = args; + + const conversation = await Conversations.findOne({ _id, integrationId }); + const integration = await Integrations.findOne({ _id: integrationId }); + + // When no one writes a message + if (!conversation && integration) { + return { + messages: [], + isOnline: await isMessengerOnline(integration), + }; + } + + if (!conversation || !integration) { + return null; + } + + return { + _id, + messages: await getWidgetMessages(conversation._id), + isOnline: await isMessengerOnline(integration), + operatorStatus: conversation.operatorStatus, + participatedUsers: await Users.find({ + _id: { $in: conversation.participatedUserIds }, + }), + supporters: await messengerSupporters(integration), + }; + }, + + widgetsMessages(_root, args: { conversationId: string }) { + const { conversationId } = args; + + return getWidgetMessages(conversationId); + }, + + widgetsUnreadCount(_root, args: { conversationId: string }) { + const { conversationId } = args; + + return Messages.widgetsGetUnreadMessagesCount(conversationId); + }, + + async widgetsTotalUnreadCount(_root, args: { integrationId: string; customerId: string }) { + const { integrationId, customerId } = args; + + // find conversations + const convs = await Conversations.find({ integrationId, customerId }); + + // find read messages count + return Messages.countDocuments(Conversations.widgetsUnreadMessagesQuery(convs)); + }, + + async widgetsMessengerSupporters(_root, { integrationId }: { integrationId: string }) { + const integration = await Integrations.findOne({ _id: integrationId }); + let timezone = ''; + + if (!integration) { + return { + supporters: [], + isOnline: false, + serverTime: momentTz().tz(), + }; + } + + const messengerData = integration.messengerData || { supporterIds: [] }; + + if (integration.messengerData && integration.messengerData.timezone) { + timezone = integration.messengerData.timezone; + } + + return { + supporters: await Users.find({ _id: { $in: messengerData.supporterIds || [] } }), + isOnline: await isMessengerOnline(integration), + serverTime: momentTz().tz(timezone), + }; + }, + + /** + * Topic detail + */ + async widgetsKnowledgeBaseTopicDetail(_root, { _id }: { _id: string }) { + const topic = await KnowledgeBaseTopics.findOne({ _id }); + + if (topic && topic.createdBy) { + const user = await Users.getUser(topic.createdBy); + + registerOnboardHistory({ type: 'knowledgeBaseInstalled', user }); + } + + return topic; + }, +}; diff --git a/src/data/resolvers/script.ts b/src/data/resolvers/script.ts index 0b660dfb6..27118b3d0 100644 --- a/src/data/resolvers/script.ts +++ b/src/data/resolvers/script.ts @@ -11,6 +11,6 @@ export default { }, leads(script: IScriptDocument) { - return Integrations.find({ _id: { $in: script.leadIds || [] } }); + return Integrations.findIntegrations({ _id: { $in: script.leadIds || [] } }); }, }; diff --git a/src/data/resolvers/stages.ts b/src/data/resolvers/stages.ts index 6a3e033ed..4d2c4ac8d 100644 --- a/src/data/resolvers/stages.ts +++ b/src/data/resolvers/stages.ts @@ -1,18 +1,24 @@ -import { Deals, Stages, Tasks, Tickets } from '../../db/models'; +import { Deals, GrowthHacks, Stages, Tasks, Tickets } from '../../db/models'; import { IStageDocument } from '../../db/models/definitions/boards'; -import { BOARD_TYPES } from '../../db/models/definitions/constants'; +import { BOARD_STATUSES, BOARD_TYPES } from '../../db/models/definitions/constants'; +import { IContext } from '../types'; import { generateDealCommonFilters, + generateGrowthHackCommonFilters, generateTaskCommonFilters, generateTicketCommonFilters, } from './queries/boardUtils'; export default { - async amount(stage: IStageDocument, _args, _context, { variableValues: args }) { + async amount(stage: IStageDocument, _args, { user }: IContext, { variableValues: args }) { const amountsMap = {}; if (stage.type === BOARD_TYPES.DEAL) { - const filter = await generateDealCommonFilters({ ...args, stageId: stage._id }, args.extraParams); + const filter = await generateDealCommonFilters( + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); const amountList = await Deals.aggregate([ { @@ -25,8 +31,12 @@ export default { $project: { amount: '$productsData.amount', currency: '$productsData.currency', + tickUsed: '$productsData.tickUsed', }, }, + { + $match: { tickUsed: true }, + }, { $group: { _id: '$currency', @@ -45,31 +55,52 @@ export default { return amountsMap; }, - async itemsTotalCount(stage: IStageDocument, _args, _context, { variableValues: args }) { + async itemsTotalCount(stage: IStageDocument, _args, { user }: IContext, { variableValues: args }) { switch (stage.type) { case BOARD_TYPES.DEAL: { - const filter = await generateDealCommonFilters({ ...args, stageId: stage._id }, args.extraParams); + const filter = await generateDealCommonFilters( + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); return Deals.find(filter).countDocuments(); } case BOARD_TYPES.TICKET: { - const filter = await generateTicketCommonFilters({ ...args, stageId: stage._id }, args.extraParams); + const filter = await generateTicketCommonFilters( + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); return Tickets.find(filter).countDocuments(); } case BOARD_TYPES.TASK: { - const filter = await generateTaskCommonFilters({ ...args, stageId: stage._id }, args.extraParams); + const filter = await generateTaskCommonFilters(user._id, { + ...args, + stageId: stage._id, + pipelineId: stage.pipelineId, + }); return Tasks.find(filter).countDocuments(); } + case BOARD_TYPES.GROWTH_HACK: { + const filter = await generateGrowthHackCommonFilters( + user._id, + { ...args, stageId: stage._id, pipelineId: stage.pipelineId }, + args.extraParams, + ); + + return GrowthHacks.find(filter).countDocuments(); + } } }, /* * Total count of deals that are created on this stage initially */ - async initialDealsTotalCount(stage: IStageDocument, _args, _context, { variableValues: args }) { - const filter = await generateDealCommonFilters({ ...args, initialStageId: stage._id }, args.extraParams); + async initialDealsTotalCount(stage: IStageDocument, _args, { user }: IContext, { variableValues: args }) { + const filter = await generateDealCommonFilters(user._id, { ...args, initialStageId: stage._id }, args.extraParams); return Deals.find(filter).countDocuments(); }, @@ -79,14 +110,12 @@ export default { * 1. created on this stage initially * 2. moved to other stage which has probability other than Lost */ - async inProcessDealsTotalCount(stage: IStageDocument, _args, _context, { variableValues: args }) { - const filter = await generateDealCommonFilters( - { - ...args, - $and: [{ pipelineId: stage.pipelineId }, { probability: { $ne: 'Lost' } }, { _id: { $ne: stage._id } }], - }, - args.extraParams, - ); + async inProcessDealsTotalCount(stage: IStageDocument) { + const filter = { + pipelineId: stage.pipelineId, + probability: { $ne: 'Lost' }, + id: { $ne: stage._id }, + }; const deals = await Stages.aggregate([ { @@ -95,8 +124,16 @@ export default { { $lookup: { from: 'deals', - localField: '_id', - foreignField: 'stageId', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$stageId', '$$stageId'] }, { $ne: ['$status', BOARD_STATUSES.ARCHIVED] }], + }, + }, + }, + ], as: 'deals', }, }, @@ -119,9 +156,10 @@ export default { return deals.length; }, - async stayedDealsTotalCount(stage: IStageDocument, _args, _context, { variableValues: args }) { + async stayedDealsTotalCount(stage: IStageDocument, _args, { user }: IContext, { variableValues: args }) { const filter = await generateDealCommonFilters( - { ...args, initialStageId: stage._id, stageId: stage._id }, + user._id, + { ...args, initialStageId: stage._id, stageId: stage._id, pipelineId: stage.pipelineId }, args.extraParams, ); @@ -132,21 +170,16 @@ export default { * Compare current stage with next stage * by initial and current deals count */ - async compareNextStage(stage: IStageDocument, _args, _context, { variableValues: args }) { + async compareNextStage(stage: IStageDocument) { const result: { count?: number; percent?: number } = {}; const { order = 1 } = stage; - const filter = await generateDealCommonFilters( - { - ...args, - order: { $in: [order, order + 1] }, - probability: { $ne: 'Lost' }, - }, - args.extraParams, - ); - - filter.pipelineId = stage.pipelineId; + const filter = { + order: { $in: [order, order + 1] }, + probability: { $ne: 'Lost' }, + pipelineId: stage.pipelineId, + }; const stages = await Stages.aggregate([ { @@ -155,16 +188,32 @@ export default { { $lookup: { from: 'deals', - localField: '_id', - foreignField: 'stageId', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$stageId', '$$stageId'] }, { $ne: ['$status', BOARD_STATUSES.ARCHIVED] }], + }, + }, + }, + ], as: 'currentDeals', }, }, { $lookup: { from: 'deals', - localField: '_id', - foreignField: 'initialStageId', + let: { stageId: '$_id' }, + pipeline: [ + { + $match: { + $expr: { + $and: [{ $eq: ['$initialStageId', '$$stageId'] }, { $ne: ['$status', BOARD_STATUSES.ARCHIVED] }], + }, + }, + }, + ], as: 'initialDeals', }, }, diff --git a/src/data/resolvers/subscriptions/checklists.ts b/src/data/resolvers/subscriptions/checklists.ts new file mode 100644 index 000000000..64f4600d9 --- /dev/null +++ b/src/data/resolvers/subscriptions/checklists.ts @@ -0,0 +1,27 @@ +import { withFilter } from 'apollo-server-express'; +import { graphqlPubsub } from '../../../pubsub'; + +export default { + /* + * Listen for checklist updates + */ + checklistsChanged: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('checklistsChanged'), + (payload, variables) => { + const { contentType, contentTypeId } = payload.checklistsChanged; + + return contentType === variables.contentType && contentTypeId === variables.contentTypeId; + }, + ), + }, + + checklistDetailChanged: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('checklistDetailChanged'), + (payload, variables) => { + return payload.checklistDetailChanged._id === variables._id; + }, + ), + }, +}; diff --git a/src/data/resolvers/subscriptions/conversations.ts b/src/data/resolvers/subscriptions/conversations.ts index 5494cc0d8..a16466c38 100644 --- a/src/data/resolvers/subscriptions/conversations.ts +++ b/src/data/resolvers/subscriptions/conversations.ts @@ -29,6 +29,18 @@ export default { ), }, + /* + * Show typing while waiting Bot response + */ + conversationBotTypingStatus: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('conversationBotTypingStatus'), + async (payload, variables) => { + return payload.conversationBotTypingStatus.conversationId === variables._id; + }, + ), + }, + /* * Admin is listening for this subscription to show typing notification */ @@ -62,7 +74,7 @@ export default { return false; } - const availableChannelsCount = await Channels.count({ + const availableChannelsCount = await Channels.countDocuments({ integrationIds: { $in: [integration._id] }, memberIds: { $in: [variables.userId] }, }); @@ -84,4 +96,11 @@ export default { }, ), }, + + /* + * Integrations api is listener + */ + conversationExternalIntegrationMessageInserted: { + subscribe: () => graphqlPubsub.asyncIterator('conversationExternalIntegrationMessageInserted'), + }, }; diff --git a/src/data/resolvers/subscriptions/index.ts b/src/data/resolvers/subscriptions/index.ts index eb2ee0cef..c2bcbd412 100644 --- a/src/data/resolvers/subscriptions/index.ts +++ b/src/data/resolvers/subscriptions/index.ts @@ -1,13 +1,21 @@ import activityLogs from './activityLogs'; +import checklists from './checklists'; import conversations from './conversations'; import customers from './customers'; import importHistory from './importHistory'; +import notifications from './notifications'; +import pipelines from './pipelines'; +import robot from './robot'; let subscriptions: any = { ...conversations, ...customers, ...activityLogs, ...importHistory, + ...notifications, + ...robot, + ...checklists, + ...pipelines, }; const { NODE_ENV } = process.env; diff --git a/src/data/resolvers/subscriptions/notifications.ts b/src/data/resolvers/subscriptions/notifications.ts new file mode 100644 index 000000000..86addc7bb --- /dev/null +++ b/src/data/resolvers/subscriptions/notifications.ts @@ -0,0 +1,16 @@ +import { withFilter } from 'apollo-server-express'; +import { graphqlPubsub } from '../../../pubsub'; + +export default { + /* + * Listen for notification + */ + notificationInserted: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('notificationInserted'), + (payload, variables) => { + return payload.notificationInserted.userId === variables.userId; + }, + ), + }, +}; diff --git a/src/data/resolvers/subscriptions/pipelines.ts b/src/data/resolvers/subscriptions/pipelines.ts new file mode 100644 index 000000000..5a53c1e48 --- /dev/null +++ b/src/data/resolvers/subscriptions/pipelines.ts @@ -0,0 +1,17 @@ +import { withFilter } from 'apollo-server-express'; +import { graphqlPubsub } from '../../../pubsub'; + +export default { + /* + * Listen for pipeline updates + */ + pipelinesChanged: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('pipelinesChanged'), + // filter by _id + (payload, variables) => { + return payload.pipelinesChanged._id === variables._id; + }, + ), + }, +}; diff --git a/src/data/resolvers/subscriptions/robot.ts b/src/data/resolvers/subscriptions/robot.ts new file mode 100644 index 000000000..a3e32d7ac --- /dev/null +++ b/src/data/resolvers/subscriptions/robot.ts @@ -0,0 +1,17 @@ +import { withFilter } from 'apollo-server-express'; +import { graphqlPubsub } from '../../../pubsub'; + +export default { + onboardingChanged: { + subscribe: withFilter( + () => graphqlPubsub.asyncIterator('onboardingChanged'), + (payload, variables) => { + if (!payload) { + return false; + } + + return payload.onboardingChanged.userId === variables.userId; + }, + ), + }, +}; diff --git a/src/data/resolvers/tasks.ts b/src/data/resolvers/tasks.ts index dbe778202..5fb6d51c3 100644 --- a/src/data/resolvers/tasks.ts +++ b/src/data/resolvers/tasks.ts @@ -1,27 +1,48 @@ -import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; +import { + Companies, + Conformities, + Customers, + Notifications, + PipelineLabels, + Pipelines, + Stages, + Users, +} from '../../db/models'; import { ITaskDocument } from '../../db/models/definitions/tasks'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { - companies(task: ITaskDocument) { - return Companies.find({ _id: { $in: task.companyIds || [] } }); + async companies(task: ITaskDocument) { + const companyIds = await Conformities.savedConformity({ + mainType: 'task', + mainTypeId: task._id, + relTypes: ['company'], + }); + + return Companies.find({ _id: { $in: companyIds || [] } }); }, - customers(task: ITaskDocument) { - return Customers.find({ _id: { $in: task.customerIds || [] } }); + async createdUser(task: ITaskDocument) { + return Users.findOne({ _id: task.userId }); + }, + + async customers(task: ITaskDocument) { + const customerIds = await Conformities.savedConformity({ + mainType: 'task', + mainTypeId: task._id, + relTypes: ['customer'], + }); + + return Customers.find({ _id: { $in: customerIds || [] } }); }, assignedUsers(task: ITaskDocument) { - return Users.find({ _id: { $in: task.assignedUserIds } }); + return Users.find({ _id: { $in: task.assignedUserIds || [] } }); }, async pipeline(task: ITaskDocument) { - const stage = await Stages.findOne({ _id: task.stageId }); - - if (!stage) { - return null; - } + const stage = await Stages.getStage(task.stageId); return Pipelines.findOne({ _id: stage.pipelineId }); }, @@ -31,10 +52,10 @@ export default { }, stage(task: ITaskDocument) { - return Stages.findOne({ _id: task.stageId }); + return Stages.getStage(task.stageId); }, - isWatched(task: ITaskDocument, _args, { user }: { user: IUserDocument }) { + isWatched(task: ITaskDocument, _args, { user }: IContext) { const watchedUserIds = task.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { @@ -43,4 +64,12 @@ export default { return false; }, + + hasNotified(deal: ITaskDocument, _args, { user }: IContext) { + return Notifications.checkIfRead(user._id, deal._id); + }, + + labels(task: ITaskDocument) { + return PipelineLabels.find({ _id: { $in: task.labelIds || [] } }); + }, }; diff --git a/src/data/resolvers/tickets.ts b/src/data/resolvers/tickets.ts index 4114eb662..8989d02d7 100644 --- a/src/data/resolvers/tickets.ts +++ b/src/data/resolvers/tickets.ts @@ -1,27 +1,44 @@ -import { Companies, Customers, Pipelines, Stages, Users } from '../../db/models'; +import { + Companies, + Conformities, + Customers, + Notifications, + PipelineLabels, + Pipelines, + Stages, + Users, +} from '../../db/models'; import { ITicketDocument } from '../../db/models/definitions/tickets'; -import { IUserDocument } from '../../db/models/definitions/users'; +import { IContext } from '../types'; import { boardId } from './boardUtils'; export default { - companies(ticket: ITicketDocument) { - return Companies.find({ _id: { $in: ticket.companyIds || [] } }); + async companies(ticket: ITicketDocument) { + const companyIds = await Conformities.savedConformity({ + mainType: 'ticket', + mainTypeId: ticket._id, + relTypes: ['company'], + }); + + return Companies.find({ _id: { $in: companyIds || [] } }); }, - customers(ticket: ITicketDocument) { - return Customers.find({ _id: { $in: ticket.customerIds || [] } }); + async customers(ticket: ITicketDocument) { + const customerIds = await Conformities.savedConformity({ + mainType: 'ticket', + mainTypeId: ticket._id, + relTypes: ['customer'], + }); + + return Customers.find({ _id: { $in: customerIds || [] } }); }, assignedUsers(ticket: ITicketDocument) { - return Users.find({ _id: { $in: ticket.assignedUserIds } }); + return Users.find({ _id: { $in: ticket.assignedUserIds || [] } }); }, async pipeline(ticket: ITicketDocument) { - const stage = await Stages.findOne({ _id: ticket.stageId }); - - if (!stage) { - return null; - } + const stage = await Stages.getStage(ticket.stageId); return Pipelines.findOne({ _id: stage.pipelineId }); }, @@ -31,10 +48,10 @@ export default { }, stage(ticket: ITicketDocument) { - return Stages.findOne({ _id: ticket.stageId }); + return Stages.getStage(ticket.stageId); }, - isWatched(ticket: ITicketDocument, _args, { user }: { user: IUserDocument }) { + isWatched(ticket: ITicketDocument, _args, { user }: IContext) { const watchedUserIds = ticket.watchedUserIds || []; if (watchedUserIds.includes(user._id)) { @@ -43,4 +60,16 @@ export default { return false; }, + + hasNotified(ticket: ITicketDocument, _args, { user }: IContext) { + return Notifications.checkIfRead(user._id, ticket._id); + }, + + labels(ticket: ITicketDocument) { + return PipelineLabels.find({ _id: { $in: ticket.labelIds || [] } }); + }, + + createdUser(ticket: ITicketDocument) { + return Users.findOne({ _id: ticket.userId }); + }, }; diff --git a/src/data/resolvers/user.ts b/src/data/resolvers/user.ts index 755e23096..0f4852f8a 100644 --- a/src/data/resolvers/user.ts +++ b/src/data/resolvers/user.ts @@ -1,16 +1,70 @@ +import { Brands, Configs, OnboardingHistories } from '../../db/models'; +import { DEFAULT_CONSTANT_VALUES } from '../../db/models/definitions/constants'; import { IUserDocument } from '../../db/models/definitions/users'; -import { getUserAllowedActions } from '../permissions/utils'; +import { getUserActionsMap } from '../permissions/utils'; +import { getConfigs } from '../utils'; export default { status(user: IUserDocument) { if (user.registrationToken) { - return 'Pending Invitation'; + return 'Not verified'; } return 'Verified'; }, + brands(user: IUserDocument) { + if (user.isOwner) { + return Brands.find({}); + } + + return Brands.find({ _id: { $in: user.brandIds } }); + }, + async permissionActions(user: IUserDocument) { - return getUserAllowedActions(user); + return getUserActionsMap(user); + }, + + async configs() { + return getConfigs(); + }, + + async configsConstants() { + const results: any[] = []; + const configs = await getConfigs(); + const constants = Configs.constants(); + + for (const key of Object.keys(constants)) { + const configValues = configs[key] || []; + const constant = constants[key]; + + let values = constant.filter(c => configValues.includes(c.value)); + + if (!values || values.length === 0) { + values = DEFAULT_CONSTANT_VALUES[key]; + } + + results.push({ + key, + values, + }); + } + + return results; + }, + + async onboardingHistory(user: IUserDocument) { + const entries = await OnboardingHistories.find({ userId: user._id }); + const completed = entries.find(item => item.isCompleted); + + /** + * When multiple entries are recorded, using findOne() gave wrong result. + * Therefore return the first completed one if exists + */ + if (completed) { + return completed; + } + + return entries[0]; }, }; diff --git a/src/data/resolvers/usersGroup.ts b/src/data/resolvers/usersGroup.ts index 553388c9a..f863e519e 100644 --- a/src/data/resolvers/usersGroup.ts +++ b/src/data/resolvers/usersGroup.ts @@ -1,10 +1,18 @@ import { Users } from '../../db/models'; import { IUserGroupDocument } from '../../db/models/definitions/permissions'; +const getUsers = async (id: string) => { + return Users.find({ groupIds: { $in: [id] } }, { _id: 1, email: 1, 'details.avatar': 1, 'details.fullName': 1 }); +}; + export default { async memberIds(group: IUserGroupDocument) { - const users = await Users.find({ groupIds: { $in: [group._id] } }, { _id: 1 }).lean(); + const users = await getUsers(group._id); + + return users.map(u => u._id); + }, - return users.map(user => user._id); + async members(group: IUserGroupDocument) { + return getUsers(group._id); }, }; diff --git a/src/data/schema/activityLog.ts b/src/data/schema/activityLog.ts index 0ae656895..49698933f 100644 --- a/src/data/schema/activityLog.ts +++ b/src/data/schema/activityLog.ts @@ -1,23 +1,16 @@ export const types = ` - type ActivityLogPerformerDetails { - avatar: String - fullName: String - position: String - } - - type ActivityLogActionPerformer { + type ActivityLog { _id: String - type: String! - details: ActivityLogPerformerDetails - } + action: String + contentId: String + contentType: String + content: JSON + createdAt: Date + createdBy: String - type ActivityLog { - _id: String! - action: String! - id: String - createdAt: Date! - content: String - by: ActivityLogActionPerformer + createdByDetail: JSON + contentDetail: JSON + contentTypeDetail: JSON } `; diff --git a/src/data/schema/board.ts b/src/data/schema/board.ts index 6dbc60ba9..3f714b7e1 100644 --- a/src/data/schema/board.ts +++ b/src/data/schema/board.ts @@ -21,50 +21,88 @@ export const types = ` members: [User] bgColor: String isWatched: Boolean + itemsTotalCount: Int + + startDate: Date + endDate: Date + metric: String + hackScoringType: String + templateId: String + state: String + isCheckUser: Boolean + excludeCheckUserIds: [String] ${commonTypes} } type Stage { _id: String! name: String! - probability: String pipelineId: String! + probability: String + status: String amount: JSON itemsTotalCount: Int compareNextStage: JSON stayedDealsTotalCount: Int initialDealsTotalCount: Int inProcessDealsTotalCount: Int + formId: String ${commonTypes} } + type PipelineChangeResponse { + _id: String + proccessId: String + action: String + data: JSON + } + + type ConvertTo { + ticketUrl: String, + dealUrl: String, + taskUrl: String, + } + + type BoardCount { + _id: String + name: String + count: Int + } + input ItemDate { month: Int year: Int } `; +const stageParams = ` + search: String, + companyIds: [String] + customerIds: [String] + assignedUserIds: [String] + labelIds: [String] + extraParams: JSON, + closeDateType: String +`; + export const queries = ` boards(type: String!): [Board] + boardCounts(type: String!): [BoardCount] boardGetLast(type: String!): Board boardDetail(_id: String!): Board - pipelines(boardId: String!): [Pipeline] + pipelines(boardId: String, type: String, page: Int, perPage: Int): [Pipeline] pipelineDetail(_id: String!): Pipeline stages( isNotLost: Boolean, + isAll: Boolean, pipelineId: String!, - search: String, - companyIds: [String], - customerIds: [String], - assignedUserIds: [String], - extraParams: JSON, - nextDay: String, - nextWeek: String, - nextMonth: String, - noCloseDate: String, - overdue: String, + ${stageParams} ): [Stage] - stageDetail(_id: String!): Stage + stageDetail(_id: String!, ${stageParams}): Stage + convertToInfo(conversationId: String!): ConvertTo + pipelineStateCount(boardId: String, type: String): JSON + archivedStages(pipelineId: String!, search: String, page: Int, perPage: Int): [Stage] + archivedStagesCount(pipelineId: String!, search: String): Int `; const commonParams = ` @@ -79,7 +117,14 @@ const pipelineParams = ` stages: JSON, visibility: String!, memberIds: [String], - bgColor: String + bgColor: String, + startDate: Date, + endDate: Date, + metric: String, + hackScoringType: String, + templateId: String, + isCheckUser: Boolean + excludeCheckUserIds: [String], `; export const mutations = ` @@ -95,4 +140,5 @@ export const mutations = ` stagesUpdateOrder(orders: [OrderItem]): [Stage] stagesRemove(_id: String!): JSON + stagesEdit(_id: String!, type: String, name: String, status: String): Stage `; diff --git a/src/data/schema/brand.ts b/src/data/schema/brand.ts index e87b0fb78..0712d3b39 100644 --- a/src/data/schema/brand.ts +++ b/src/data/schema/brand.ts @@ -13,10 +13,11 @@ export const types = ` `; export const queries = ` - brands(page: Int, perPage: Int): [Brand] + brands(page: Int, perPage: Int, searchValue: String): [Brand] brandDetail(_id: String!): Brand brandsTotalCount: Int brandsGetLast: Brand + brandsGetDefaultEmailConfig: String `; export const mutations = ` diff --git a/src/data/schema/channel.ts b/src/data/schema/channel.ts index 435ae5c30..ac6417f0e 100644 --- a/src/data/schema/channel.ts +++ b/src/data/schema/channel.ts @@ -11,6 +11,7 @@ export const types = ` openConversationCount: Int integrations: [Integration] + members: [User] } `; @@ -21,18 +22,15 @@ export const queries = ` channelsGetLast: Channel `; -export const mutations = ` - channelsAdd( - name: String!, - description: String, - memberIds: [String], - integrationIds: [String]): Channel +const commonMutationParams = ` + name: String!, + description: String, + memberIds: [String], + integrationIds: [String] +`; - channelsEdit( - _id: String!, - name: String!, - description: String, - memberIds: [String], - integrationIds: [String]): Channel - channelsRemove(_id: String!): JSON +export const mutations = ` + channelsAdd(${commonMutationParams}): Channel + channelsEdit(_id: String!, ${commonMutationParams}): Channel + channelsRemove(_id: String!): JSON `; diff --git a/src/data/schema/checklist.ts b/src/data/schema/checklist.ts new file mode 100644 index 000000000..e3e797b8b --- /dev/null +++ b/src/data/schema/checklist.ts @@ -0,0 +1,37 @@ +export const types = ` + type ChecklistItem { + _id: String! + checklistId: String + isChecked: Boolean + content: String + order: Int + } + + type Checklist { + _id: String! + contentType: String + contentTypeId: String + title: String + createdUserId: String + createdDate: Date + items: [ChecklistItem] + percent: Float + } + +`; + +export const queries = ` + checklists(contentType: String, contentTypeId: String): [Checklist] + checklistDetail(_id: String!): Checklist +`; + +export const mutations = ` + checklistsAdd(contentType: String, contentTypeId: String, title: String): Checklist + checklistsEdit(_id: String!, title: String, contentType: String, contentTypeId: String,): Checklist + checklistsRemove(_id: String!): Checklist + checklistItemsOrder(_id: String!, destinationIndex: Int): ChecklistItem + + checklistItemsAdd(checklistId: String, content: String, isChecked: Boolean): ChecklistItem + checklistItemsEdit(_id: String!, checklistId: String, content: String, isChecked: Boolean): ChecklistItem + checklistItemsRemove(_id: String!): ChecklistItem +`; diff --git a/src/data/schema/common.ts b/src/data/schema/common.ts index a8a09a315..7570a638a 100644 --- a/src/data/schema/common.ts +++ b/src/data/schema/common.ts @@ -5,6 +5,7 @@ const ruleFields = ` condition: String!, value: String, `; + export const types = ` type Rule { ${ruleFields} @@ -14,3 +15,64 @@ export const types = ` ${ruleFields} } `; + +export const conformityQueryFields = ` + conformityMainType: String + conformityMainTypeId: String + conformityIsRelated: Boolean + conformityIsSaved: Boolean +`; + +export const commonTypes = ` + name: String! + order: Float + createdAt: Date + hasNotified: Boolean + assignedUserIds: [String] + labelIds: [String] + closeDate: Date + description: String + modifiedAt: Date + modifiedBy: String + reminderMinute: Int, + isComplete: Boolean, + isWatched: Boolean, + stageId: String + boardId: String + priority: String + status: String + attachments: [Attachment] + userId: String + + assignedUsers: [User] + stage: Stage + labels: [PipelineLabel] + pipeline: Pipeline + createdUser: User +`; + +export const commonMutationParams = ` + proccessId: String, + aboveItemId: String, + stageId: String, + assignedUserIds: [String], + attachments: [AttachmentInput], + closeDate: Date, + description: String, + order: Int, + reminderMinute: Int, + isComplete: Boolean, + priority: String, + status: String, + sourceConversationId: String, +`; + +export const commonDragParams = ` + itemId: String!, + aboveItemId: String, + destinationStageId: String!, + sourceStageId: String, + proccessId: String +`; + +export const copyParams = `companyIds: [String], customerIds: [String], labelIds: [String]`; diff --git a/src/data/schema/company.ts b/src/data/schema/company.ts index f2cc9638a..bdebd7c47 100644 --- a/src/data/schema/company.ts +++ b/src/data/schema/company.ts @@ -1,3 +1,5 @@ +import { conformityQueryFields } from './common'; + export const types = ` type Company { _id: String! @@ -5,31 +7,26 @@ export const types = ` createdAt: Date modifiedAt: Date avatar: String - + size: Int website: String industry: String plan: String parentCompanyId: String ownerId: String + mergedIds: [String] names: [String] primaryName: String - emails: [String] primaryEmail: String - - phones: [String] primaryPhone: String - - leadStatus: String - lifecycleState: String businessType: String description: String doNotDisturb: String - links: CompanyLinks + links: JSON owner: User parentCompany: Company @@ -38,23 +35,14 @@ export const types = ` customFieldsData: JSON customers: [Customer] - deals: [Deal] getTags: [Tag] + code: String } type CompaniesListResponse { list: [Company], totalCount: Float, } - - type CompanyLinks { - linkedIn: String - twitter: String - facebook: String - github: String - youtube: String - website: String - } `; const queryParams = ` @@ -64,17 +52,18 @@ const queryParams = ` tag: String ids: [String] searchValue: String - lifecycleState: String - leadStatus: String + autoCompletion: Boolean + autoCompletionType: String sortField: String sortDirection: Int brand: String + ${conformityQueryFields} `; export const queries = ` companiesMain(${queryParams}): CompaniesListResponse companies(${queryParams}): [Company] - companyCounts(${queryParams}, byFakeSegment: JSON, only: String): JSON + companyCounts(${queryParams}, only: String): JSON companyDetail(_id: String!): Company `; @@ -97,8 +86,6 @@ const commonFields = ` parentCompanyId: String, email: String, ownerId: String, - leadStatus: String, - lifecycleState: String, businessType: String, description: String, doNotDisturb: String, @@ -106,12 +93,12 @@ const commonFields = ` tagIds: [String] customFieldsData: JSON + code: String `; export const mutations = ` companiesAdd(${commonFields}): Company companiesEdit(_id: String!, ${commonFields}): Company - companiesEditCustomers(_id: String!, customerIds: [String]): Company companiesRemove(companyIds: [String]): [String] companiesMerge(companyIds: [String], companyFields: JSON) : Company `; diff --git a/src/data/schema/config.ts b/src/data/schema/config.ts index c9d0a2cb4..7a49bf08b 100644 --- a/src/data/schema/config.ts +++ b/src/data/schema/config.ts @@ -2,29 +2,61 @@ export const types = ` type Config { _id: String! code: String! - value: [String]! + value: JSON } - type GitInfos { + type GeneralInfo { packageVersion: String - branch: String - sha: String - abbreviatedSha: String } - type ProjectInfos { - erxesVersion: GitInfos - apiVersion: GitInfos - widgetVersion: GitInfos - widgetApiVersion: GitInfos + type OSInfo { + type: String + platform: String + arch: String + release: String + uptime: Int + loadavg: [Float] + totalmem: Float + freemem: Float + cpuCount: Int + } + + type ProcessInfo { + nodeVersion: String + pid: String + uptime: String + } + + type MongoInfo { + version: String + storageEngine: String + } + + type Statistic { + packageVersion: String + os: OSInfo + process: ProcessInfo + mongo: MongoInfo + } + + type ProjectStatistics { + erxesApi: Statistic + erxesIntegration: Statistic + erxes: GeneralInfo + } + + type ENV { + USE_BRAND_RESTRICTIONS: String } `; export const queries = ` - configsDetail(code: String!): Config - configsVersions: ProjectInfos + configs: [Config] + configsStatus: ProjectStatistics + configsGetEnv: ENV + configsConstants: JSON `; export const mutations = ` - configsInsert(code: String!, value: [String]!): Config + configsUpdate(configsMap: JSON!): JSON `; diff --git a/src/data/schema/conformity.ts b/src/data/schema/conformity.ts new file mode 100644 index 000000000..90909abd7 --- /dev/null +++ b/src/data/schema/conformity.ts @@ -0,0 +1,32 @@ +export const types = ` + type Conformity { + _id: String! + mainType: String + mainTypeId: String + relType: String + relTypeId: String + } + + type SuccessResult { + success: Boolean, + } +`; + +const commonParams = ` + mainType: String + mainTypeId: String + relType: String + relTypeId: String +`; + +const commonParamsCreate = ` + mainType: String + mainTypeId: String + relType: String + relTypeIds: [String] +`; + +export const mutations = ` + conformityAdd(${commonParams}): Conformity + conformityEdit(${commonParamsCreate}): SuccessResult +`; diff --git a/src/data/schema/conversation.ts b/src/data/schema/conversation.ts index 9957d2366..e77833d4d 100644 --- a/src/data/schema/conversation.ts +++ b/src/data/schema/conversation.ts @@ -24,8 +24,11 @@ export const types = ` messageCount: Int number: Int tagIds: [String] + operatorStatus: String messages: [ConversationMessage] + facebookPost: FacebookPost + callProAudio: String tags: [Tag] customer: Customer integration: Integration @@ -33,6 +36,8 @@ export const types = ` assignedUser: User participatedUsers: [User] participatorCount: Int + videoCallData: VideoCallData + productBoardLink: String } type EngageData { @@ -60,6 +65,7 @@ export const types = ` conversationId: String internal: Boolean fromBot: Boolean + botData: JSON customerId: String userId: String createdAt: Date @@ -69,6 +75,70 @@ export const types = ` messengerAppData: JSON user: User customer: Customer + mailData: MailData + videoCallData: VideoCallData + contentType: String + } + + type FacebookPost { + postId: String + recipientId: String + senderId: String + content:String + erxesApiId: String + attachments: [String] + timestamp: Date + permalink_url: String + } + + type FacebookComment { + conversationId: String + commentId: String + postId: String + parentId: String + recipientId:String + senderId: String + permalink_url: String + attachments: [String] + content: String + erxesApiId: String + timestamp: Date + customer: Customer + commentCount: Int + isResolved: Boolean + } + + type Email { + email: String + } + + type MailData { + messageId: String, + threadId: String, + replyTo: [String], + inReplyTo: String, + subject: String, + body: String, + integrationEmail: String, + to: [Email], + from: [Email], + cc: [Email], + bcc: [Email], + accountId: String, + replyToMessageId: [String], + references: [String], + headerId: String, + attachments: [MailAttachment] + } + + type MailAttachment { + id: String, + content_type: String, + filename: String, + mimeType: String, + size: Int, + attachmentId: String, + data: String, } type ConversationChangedResponse { @@ -81,6 +151,17 @@ export const types = ` text: String } + type ConversationAdminMessageInsertedResponse { + customerId: String! + unreadCount: Int + } + + type VideoCallData { + url: String + name: String + status: String + } + input ConversationMessageParams { content: String, mentionedUserIds: [String], @@ -95,13 +176,12 @@ export const types = ` input AttachmentInput { url: String! name: String! - type: String! + type: String size: Float } `; -const filterParams = ` - limit: Int, +const mutationFilterParams = ` channelId: String status: String unassigned: String @@ -109,12 +189,18 @@ const filterParams = ` tag: String integrationType: String participating: String + awaitingResponse: String starred: String - ids: [String] startDate: String endDate: String `; +const filterParams = ` + limit: Int, + ids: [String] + ${mutationFilterParams} +`; + export const queries = ` conversations(${filterParams}): [Conversation] @@ -122,8 +208,23 @@ export const queries = ` conversationId: String! skip: Int limit: Int + getFirst: Boolean ): [ConversationMessage] + converstationFacebookComments( + postId: String! + isResolved: Boolean + commentId: String + senderId: String + skip: Int + limit: Int + ): [FacebookComment] + + converstationFacebookCommentsCount( + postId: String! + isResolved: Boolean + ): JSON + conversationMessagesTotalCount(conversationId: String!): Int conversationCounts(${filterParams}, only: String): JSON conversationsTotalCount(${filterParams}): Int @@ -139,10 +240,17 @@ export const mutations = ` mentionedUserIds: [String], internal: Boolean, attachments: [AttachmentInput], + contentType: String ): ConversationMessage - + conversationsReplyFacebookComment(conversationId: String, commentId: String, content: String): FacebookComment + conversationsChangeStatusFacebookComment(commentId: String): FacebookComment conversationsAssign(conversationIds: [String]!, assignedUserId: String): [Conversation] conversationsUnassign(_ids: [String]!): [Conversation] conversationsChangeStatus(_ids: [String]!, status: String!): [Conversation] conversationMarkAsRead(_id: String): Conversation + conversationDeleteVideoChatRoom(name: String!): Boolean + conversationCreateVideoChatRoom(_id: String!): VideoCallData + conversationCreateProductBoardNote(_id: String!): String + changeConversationOperator(_id: String! operatorStatus: String!): JSON + conversationResolveAll(${mutationFilterParams}): Int `; diff --git a/src/data/schema/customer.ts b/src/data/schema/customer.ts index 8ba692ddf..82af69c9f 100644 --- a/src/data/schema/customer.ts +++ b/src/data/schema/customer.ts @@ -1,3 +1,5 @@ +import { conformityQueryFields } from './common'; + // TODO: remove customer's email and phone field after customCommand export const types = ` @@ -6,23 +8,17 @@ export const types = ` status: String! } - type CustomerLinks { - linkedIn: String - twitter: String - facebook: String - youtube: String - github: String - website: String - } - type Customer { _id: String! + state: String createdAt: Date modifiedAt: Date avatar: String integrationId: String firstName: String lastName: String + birthDate: Date + sex: Int email: String primaryEmail: String @@ -31,30 +27,33 @@ export const types = ` phones: [String] phone: String - isUser: Boolean tagIds: [String] remoteAddress: String internalNotes: JSON location: JSON visitorContactInfo: JSON customFieldsData: JSON - messengerData: JSON + trackedData: JSON ownerId: String position: String department: String leadStatus: String - lifecycleState: String hasAuthority: String description: String doNotDisturb: String + code: String + emailValidationStatus: String + phoneValidationStatus: String + + isOnline: Boolean + lastSeenAt: Date + sessionCount: Int + urlVisits: [JSON] integration: Integration - links: CustomerLinks + links: JSON companies: [Company] conversations: [Conversation] - deals: [Deal] - getIntegrationData: JSON - getMessengerCustomData: JSON getTags: [Tag] owner: User } @@ -69,27 +68,30 @@ const queryParams = ` page: Int perPage: Int segment: String - type: String + type: String tag: String ids: [String] searchValue: String + autoCompletion: Boolean + autoCompletionType: String brand: String integration: String form: String startDate: String endDate: String - lifecycleState: String leadStatus: String sortField: String sortDirection: Int + sex:Int + birthDate: Date + ${conformityQueryFields} `; export const queries = ` customersMain(${queryParams}): CustomersListResponse customers(${queryParams}): [Customer] - customerCounts(${queryParams}, byFakeSegment: JSON, only: String): JSON + customerCounts(${queryParams}, only: String): JSON customerDetail(_id: String!): Customer - customerListForSegmentPreview(segment: JSON, limit: Int): [Customer] `; const fields = ` @@ -104,18 +106,24 @@ const fields = ` position: String department: String leadStatus: String - lifecycleState: String hasAuthority: String description: String doNotDisturb: String links: JSON customFieldsData: JSON + code: String + sex: Int + birthDate: Date + emailValidationStatus: String + phoneValidationStatus: String `; export const mutations = ` - customersAdd(${fields}): Customer + customersAdd(state: String, ${fields}): Customer customersEdit(_id: String!, ${fields}): Customer - customersEditCompanies(_id: String!, companyIds: [String]): Customer customersMerge(customerIds: [String], customerFields: JSON): Customer customersRemove(customerIds: [String]): [String] + customersChangeState(_id: String!, value: String!): Customer + customersVerify(verificationType:String!): String + customersChangeVerificationStatus(customerIds: [String], type: String!, status: String!): [Customer] `; diff --git a/src/data/schema/deal.ts b/src/data/schema/deal.ts index 027c9347d..f9f2315d9 100644 --- a/src/data/schema/deal.ts +++ b/src/data/schema/deal.ts @@ -1,97 +1,79 @@ -const commonTypes = ` - order: Int - createdAt: Date -`; +import { commonDragParams, commonMutationParams, commonTypes, conformityQueryFields, copyParams } from './common'; export const types = ` type Deal { _id: String! - name: String! - stageId: String - pipeline: Pipeline - boardId: String - companyIds: [String] - customerIds: [String] - assignedUserIds: [String] amount: JSON - closeDate: Date - description: String companies: [Company] customers: [Customer] products: JSON productsData: JSON - assignedUsers: [User] - modifiedAt: Date - modifiedBy: String - stage: Stage - isWatched: Boolean + paymentsData: JSON ${commonTypes} } - type DealTotalAmount { - _id: String - currency: String + type DealTotalCurrency { amount: Float + name: String + } + + type TotalForType { + _id: String + name: String + currencies: [DealTotalCurrency] } type DealTotalAmounts { _id: String dealCount: Int - dealAmounts: [DealTotalAmount] + totalForType: [TotalForType] } `; +const dealMutationParams = ` + paymentsData: JSON, + productsData: JSON, +`; + +const commonQueryParams = ` + date: ItemDate + pipelineId: String + customerIds: [String] + companyIds: [String] + assignedUserIds: [String] + productIds: [String] + closeDateType: String + labelIds: [String] + search: String + priority: [String] + sortField: String + sortDirection: Int + userIds: [String] + `; + export const queries = ` dealDetail(_id: String!): Deal deals( initialStageId: String - pipelineId: String stageId: String - customerIds: [String] - companyIds: [String] - date: ItemDate skip: Int - search: String - assignedUserIds: [String] - productIds: [String] - nextDay: String - nextWeek: String - nextMonth: String - noCloseDate: String - overdue: String - ): [Deal] + ${commonQueryParams} + ${conformityQueryFields} + ): [Deal] + archivedDeals(pipelineId: String!, search: String, page: Int, perPage: Int): [Deal] + archivedDealsCount(pipelineId: String!, search: String): Int dealsTotalAmounts( - date: ItemDate - pipelineId: String - customerIds: [String] - companyIds: [String] - assignedUserIds: [String] - productIds: [String] - nextDay: String - nextWeek: String - nextMonth: String - noCloseDate: String - overdue: String + ${commonQueryParams} + ${conformityQueryFields} ): DealTotalAmounts `; -const commonParams = ` - name: String!, - stageId: String, - assignedUserIds: [String], - companyIds: [String], - customerIds: [String], - closeDate: Date, - description: String, - order: Int, - productsData: JSON -`; - export const mutations = ` - dealsAdd(${commonParams}): Deal - dealsEdit(_id: String!, ${commonParams}): Deal - dealsChange( _id: String!, destinationStageId: String): Deal - dealsUpdateOrder(stageId: String!, orders: [OrderItem]): [Deal] + dealsAdd(name: String!, ${copyParams}, ${dealMutationParams}, ${commonMutationParams}): Deal + dealsEdit(_id: String!, name: String, ${dealMutationParams}, ${commonMutationParams}): Deal + dealsChange(${commonDragParams}): Deal dealsRemove(_id: String!): Deal dealsWatch(_id: String, isAdd: Boolean): Deal + dealsCopy(_id: String!, proccessId: String): Deal + dealsArchive(stageId: String!, proccessId: String): String `; diff --git a/src/data/schema/emailDelivery.ts b/src/data/schema/emailDelivery.ts new file mode 100644 index 000000000..3a236e9bc --- /dev/null +++ b/src/data/schema/emailDelivery.ts @@ -0,0 +1,30 @@ +export const types = ` + type EmailDelivery { + _id: String! + subject: String + status: String + body: String + to: [String] + cc: [String] + bcc: [String] + attachments: [JSON] + from: String + kind: String + userId: String + customerId: String + createdAt: Date + + fromUser: User + fromEmail: String + } + + type EmailDeliveryList { + list: [EmailDelivery] + totalCount: Int + } +`; + +export const queries = ` + emailDeliveryDetail(_id: String): EmailDelivery + transactionEmailDeliveries(searchValue: String, page: Int, perPage: Int): EmailDeliveryList +`; diff --git a/src/data/schema/engage.ts b/src/data/schema/engage.ts index c4f6b8cad..c712a5012 100644 --- a/src/data/schema/engage.ts +++ b/src/data/schema/engage.ts @@ -1,4 +1,10 @@ export const types = ` + type EngageMessageSms { + from: String, + content: String! + fromIntegrationId: String + } + type EngageMessage { _id: String! kind: String @@ -12,15 +18,17 @@ export const types = ` isDraft: Boolean isLive: Boolean stopDate: Date - createdDate: Date + createdAt: Date type: String messengerReceivedCustomerIds: [String] - stats: JSON + totalCustomersCount: Int + validCustomersCount: Int + brand: Brand email: JSON messenger: JSON - deliveryReports: JSON + shortMessage: EngageMessageSms scheduleDate: EngageScheduleDate segments: [Segment] @@ -28,25 +36,44 @@ export const types = ` brands: [Brand] fromUser: User getTags: [Tag] + fromIntegration: Integration + + stats: JSON + logs: JSON + smsStats: JSON } type EngageScheduleDate { type: String, month: String, day: String, - time: Date, + } + + type DeliveryReport { + _id: String!, + customerId: String, + mailId: String, + status: String, + engage: EngageMessage, + createdAt: Date + } + + type EngageDeliveryReport { + list: [DeliveryReport] + totalCount: Int } input EngageScheduleDateInput { type: String, month: String, day: String, - time: Date, } input EngageMessageEmail { content: String, subject: String!, + replyTo: String, + sender: String, attachments: [JSON] templateId: String } @@ -58,6 +85,12 @@ export const types = ` content: String, rules: [InputRule], } + + input EngageMessageSmsInput { + from: String, + content: String! + fromIntegrationId: String! + } `; const listParams = ` @@ -77,13 +110,16 @@ export const queries = ` engageMessagesTotalCount(${listParams}): Int engageMessageDetail(_id: String): EngageMessage engageMessageCounts(name: String!, kind: String, status: String): JSON + engagesConfigDetail: JSON + engageVerifiedEmails: [String] + engageReportsList(page: Int, perPage: Int): EngageDeliveryReport `; const commonParams = ` title: String!, kind: String!, method: String!, - fromUserId: String!, + fromUserId: String, isDraft: Boolean, isLive: Boolean, stopDate: Date, @@ -96,6 +132,7 @@ const commonParams = ` email: EngageMessageEmail, scheduleDate: EngageScheduleDateInput, messenger: EngageMessageMessenger, + shortMessage: EngageMessageSmsInput `; export const mutations = ` @@ -105,4 +142,8 @@ export const mutations = ` engageMessageSetLive(_id: String!): EngageMessage engageMessageSetPause(_id: String!): EngageMessage engageMessageSetLiveManual(_id: String!): EngageMessage + engagesUpdateConfigs(configsMap: JSON!): JSON + engageMessageVerifyEmail(email: String!): String + engageMessageRemoveVerifiedEmail(email: String!): String + engageMessageSendTestEmail(from: String!, to: String!, content: String!): String `; diff --git a/src/data/schema/field.ts b/src/data/schema/field.ts index 3c59c24a1..b9361d6b1 100644 --- a/src/data/schema/field.ts +++ b/src/data/schema/field.ts @@ -6,6 +6,7 @@ export const fieldsTypes = ` type: String validation: String text: String + name: String description: String options: [String] isRequired: Boolean @@ -31,7 +32,7 @@ export const fieldsTypes = ` export const fieldsQueries = ` fields(contentType: String!, contentTypeId: String): [Field] - fieldsCombinedByContentType(contentType: String!): JSON + fieldsCombinedByContentType(contentType: String!, usageType: String, excludedNames: [String]): JSON fieldsDefaultColumnsConfig(contentType: String!): [ColumnConfigItem] `; diff --git a/src/data/schema/form.ts b/src/data/schema/form.ts index a0ab440c0..2071129c4 100644 --- a/src/data/schema/form.ts +++ b/src/data/schema/form.ts @@ -11,18 +11,14 @@ export const types = ` _id: String! title: String code: String + type: String description: String buttonText: String - themeColor: String - callout: Callout createdUserId: String createdUser: User createdDate: Date - viewCount: Int - rules: [Rule] - contactsGathered: Int - tagIds: [String] - getTags: [Tag] + + fields: [Field] } `; @@ -30,9 +26,14 @@ const commonFields = ` title: String, description: String, buttonText: String, - themeColor: String, - callout: JSON, - rules: [InputRule] + type: String! +`; + +const commonFormSubmissionFields = ` + formId: String, + contentTypeId: String, + contentType: String, + formSubmissions: JSON `; export const queries = ` @@ -43,4 +44,5 @@ export const queries = ` export const mutations = ` formsAdd(${commonFields}): Form formsEdit(_id: String!, ${commonFields} ): Form + formSubmissionsSave(${commonFormSubmissionFields}): Boolean `; diff --git a/src/data/schema/growthHack.ts b/src/data/schema/growthHack.ts new file mode 100644 index 000000000..39f840d66 --- /dev/null +++ b/src/data/schema/growthHack.ts @@ -0,0 +1,87 @@ +import { commonDragParams, commonTypes } from './common'; + +export const types = ` + type GrowthHack { + _id: String! + hackStages: [String] + reach: Int + impact: Int + confidence: Int + ease: Int + voteCount: Int + votedUsers: [User] + isVoted: Boolean + formId: String + scoringType: String + formSubmissions: JSON + formFields: [Field] + ${commonTypes} + } +`; + +const commonQueryFields = ` + pipelineId: String + initialStageId: String + stageId: String + skip: Int + limit: Int + search: String + assignedUserIds: [String] + closeDateType: String + hackStage: [String] + priority: [String] + labelIds: [String] + userIds: [String] +`; + +export const queries = ` + growthHackDetail(_id: String!): GrowthHack + growthHacks( + ${commonQueryFields} + sortField: String + sortDirection: Int + ): [GrowthHack] + + growthHacksTotalCount( + ${commonQueryFields} + ): Int + + growthHacksPriorityMatrix( + pipelineId: String + search: String + assignedUserIds: [String] + closeDateType: String + ): JSON + + archivedGrowthHacks(pipelineId: String!, search: String, page: Int, perPage: Int): [GrowthHack] + archivedGrowthHacksCount(pipelineId: String!, search: String): Int +`; + +const commonParams = ` + proccessId: String, + aboveItemId: String, + name: String, + stageId: String, + assignedUserIds: [String], + attachments: [AttachmentInput], + closeDate: Date, + status: String, + description: String, + hackStages: [String], + priority: String, + reach: Int, + impact: Int, + confidence: Int, + ease: Int, +`; + +export const mutations = ` + growthHacksAdd(${commonParams}, labelIds: [String]): GrowthHack + growthHacksEdit(_id: String!, ${commonParams}): GrowthHack + growthHacksChange(${commonDragParams}): GrowthHack + growthHacksRemove(_id: String!): GrowthHack + growthHacksWatch(_id: String, isAdd: Boolean): GrowthHack + growthHacksVote(_id: String!, isVote: Boolean): GrowthHack + growthHacksCopy(_id: String!, proccessId: String): GrowthHack + growthHacksArchive(stageId: String!, proccessId: String): String +`; diff --git a/src/data/schema/index.ts b/src/data/schema/index.ts index dfa76b34a..ff3d14770 100755 --- a/src/data/schema/index.ts +++ b/src/data/schema/index.ts @@ -32,6 +32,8 @@ import { types as EmailTemplate, } from './emailTemplate'; +import { queries as EmailDeliveryQueries, types as EmailDelivery } from './emailDelivery'; + import { fieldsGroupsMutations as FieldGroupMutations, fieldsGroupsQueries as FieldGroupQueries, @@ -93,18 +95,36 @@ import { types as ImportHistoryTypes, } from './importHistory'; -import { - mutations as MessengerAppMutations, - queries as MessengerAppQueries, - types as MessengerAppTypes, -} from './messengerApp'; +import { mutations as MessengerAppMutations, types as MessengerAppTypes } from './messengerApp'; import { mutations as TicketMutations, queries as TicketQueries, types as TicketTypes } from './ticket'; import { mutations as TaskMutations, queries as TaskQueries, types as TaskTypes } from './task'; +import { mutations as GrowthHackMutations, queries as GrowthHackQueries, types as GrowthHackTypes } from './growthHack'; + import { queries as LogQueries, types as LogTypes } from './log'; +import { + mutations as PipelineTemplateMutations, + queries as PipelineTemplateQueries, + types as PipelineTemplateTypes, +} from './pipelineTemplate'; + +import { mutations as RobotMutations, queries as RobotQueries, types as RobotTypes } from './robot'; + +import { mutations as ConformityMutations, types as ConformityTypes } from './conformity'; + +import { mutations as ChecklistMutations, queries as ChecklistQueries, types as ChecklistTypes } from './checklist'; +import { + mutations as PipelineLabelMutations, + queries as PipelineLabelQueries, + types as PipelineLabelTypes, +} from './pipelineLabel'; + +import { mutations as WebhookMutations, queries as WebhookQueries, types as WebhookTypes } from './webhook'; +import { mutations as WidgetMutations, queries as WidgetQueries, types as WidgetTypes } from './widget'; + export const types = ` scalar JSON scalar Date @@ -120,9 +140,11 @@ export const types = ` ${Script} ${EmailTemplate} ${EngageTypes} + ${EmailDelivery} ${TagTypes} ${FieldTypes} ${FormTypes} + ${ConformityTypes} ${CustomerTypes} ${SegmentTypes} ${ConversationTypes} @@ -140,6 +162,13 @@ export const types = ` ${TicketTypes} ${TaskTypes} ${LogTypes} + ${GrowthHackTypes} + ${PipelineTemplateTypes} + ${ChecklistTypes} + ${RobotTypes} + ${PipelineLabelTypes} + ${WidgetTypes} + ${WebhookTypes} `; export const queries = ` @@ -152,6 +181,7 @@ export const queries = ` ${ResponseTemplateQueries} ${ScriptQueries} ${EmailTemplateQueries} + ${EmailDeliveryQueries} ${FieldQueries} ${EngageQueries} ${FormQueries} @@ -170,11 +200,17 @@ export const queries = ` ${ConfigQueries} ${FieldGroupQueries} ${ImportHistoryQueries} - ${MessengerAppQueries} ${PermissionQueries} ${TicketQueries} ${TaskQueries} ${LogQueries} + ${GrowthHackQueries} + ${PipelineTemplateQueries} + ${ChecklistQueries} + ${RobotQueries} + ${PipelineLabelQueries} + ${WidgetQueries} + ${WebhookQueries} } `; @@ -208,6 +244,14 @@ export const mutations = ` ${PermissionMutations} ${TicketMutations} ${TaskMutations} + ${GrowthHackMutations} + ${PipelineTemplateMutations} + ${ConformityMutations} + ${ChecklistMutations} + ${RobotMutations} + ${PipelineLabelMutations} + ${WidgetMutations} + ${WebhookMutations} } `; @@ -217,10 +261,19 @@ export const subscriptions = ` conversationMessageInserted(_id: String!): ConversationMessage conversationClientMessageInserted(userId: String!): ConversationMessage conversationClientTypingStatusChanged(_id: String!): ConversationClientTypingStatusChangedResponse - conversationAdminMessageInserted(customerId: String!): ConversationMessage + conversationAdminMessageInserted(customerId: String!): ConversationAdminMessageInsertedResponse + conversationExternalIntegrationMessageInserted: JSON + conversationBotTypingStatus(_id: String!): JSON customerConnectionChanged(_id: String): CustomerConnectionChangedResponse activityLogsChanged: Boolean importHistoryChanged(_id: String!): ImportHistory + notificationInserted(userId: String): Notification + onboardingChanged(userId: String!): OnboardingNotification + + pipelinesChanged(_id: String!): PipelineChangeResponse + + checklistsChanged(contentType: String!, contentTypeId: String!): Checklist + checklistDetailChanged(_id: String!): Checklist } `; diff --git a/src/data/schema/integration.ts b/src/data/schema/integration.ts index 740abd5ec..29ecb7578 100644 --- a/src/data/schema/integration.ts +++ b/src/data/schema/integration.ts @@ -9,13 +9,19 @@ export const types = ` formId: String tagIds: [String] tags: [Tag] - formData: JSON + leadData: JSON messengerData: JSON uiOptions: JSON + isActive: Boolean + webhookData: JSON brand: Brand form: Form channels: [Channel] + + websiteMessengerApps: [MessengerApp] + knowledgeBaseMessengerApps: [MessengerApp] + leadMessengerApps: [MessengerApp] } type integrationsTotalCount { @@ -26,7 +32,12 @@ export const types = ` byKind: JSON } - input IntegrationFormData { + type integrationsGetUsedTypes { + _id: String + name: String + } + + input IntegrationLeadData { loadType: String successAction: String fromEmail: String, @@ -37,6 +48,10 @@ export const types = ` adminEmailContent: String thankContent: String redirectUrl: String + themeColor: String + callout: JSON, + rules: [InputRule] + isRequireOnce: Boolean } input MessengerOnlineHoursSchema { @@ -55,6 +70,7 @@ export const types = ` input IntegrationMessengerData { _id: String notifyCustomer: Boolean + botEndpointUrl: String availabilityMethod: String isOnline: Boolean, onlineHours: [MessengerOnlineHoursSchema] @@ -67,12 +83,14 @@ export const types = ` showChat: Boolean showLauncher: Boolean forceLogoutWhenResolve: Boolean + showVideoCallRequest: Boolean } input MessengerUiOptions { color: String wallpaper: String logo: String + textColor: String } `; @@ -87,6 +105,8 @@ export const queries = ` tag: String ): [Integration] + integrationsGetUsedTypes: [integrationsGetUsedTypes] + integrationGetLineWebhookUrl(_id: String!): String integrationDetail(_id: String!): Integration integrationsTotalCount: integrationsTotalCount integrationsFetchApi(path: String!, params: JSON!): JSON @@ -97,6 +117,7 @@ export const mutations = ` name: String!, brandId: String!, languageCode: String + channelIds: [String] ): Integration integrationsEditMessengerIntegration( @@ -104,6 +125,7 @@ export const mutations = ` name: String!, brandId: String!, languageCode: String + channelIds: [String] ): Integration integrationsSaveMessengerAppearanceData( @@ -114,28 +136,58 @@ export const mutations = ` _id: String!, messengerData: IntegrationMessengerData): Integration - integrationsCreateFormIntegration( + integrationsCreateLeadIntegration( name: String!, brandId: String!, languageCode: String, formId: String!, - formData: IntegrationFormData!): Integration + leadData: IntegrationLeadData!): Integration - integrationsEditFormIntegration( + integrationsEditLeadIntegration( _id: String! name: String!, brandId: String!, languageCode: String, formId: String!, - formData: IntegrationFormData!): Integration + leadData: IntegrationLeadData!): Integration integrationsCreateExternalIntegration( kind: String!, name: String!, brandId: String!, - accountId: String!, + accountId: String, + channelIds: [String] data: JSON): Integration + integrationsEditCommonFields(_id: String!, name: String!, brandId: String!, channelIds: [String], data: JSON): Integration + integrationsRemove(_id: String!): JSON integrationsRemoveAccount(_id: String!): JSON + + integrationsArchive(_id: String!, status: Boolean!): Integration + + integrationSendMail( + erxesApiId: String! + subject: String! + body: String + to: [String]! + cc: [String] + bcc: [String] + from: String! + shouldResolve: Boolean + headerId: String + replyTo: [String] + inReplyTo: String + threadId: String + messageId: String + replyToMessageId: String + kind: String + references: [String] + attachments: [JSON] + customerId: String + ): JSON + + integrationsUpdateConfigs(configsMap: JSON!): JSON + + integrationsSendSms(integrationId: String!, content: String!, to: String!): JSON `; diff --git a/src/data/schema/internalNote.ts b/src/data/schema/internalNote.ts index c6c6c6307..f2630170c 100644 --- a/src/data/schema/internalNote.ts +++ b/src/data/schema/internalNote.ts @@ -5,18 +5,19 @@ export const types = ` contentTypeId: String content: String createdUserId: String - createdDate: Date + createdAt: Date createdUser: User } `; export const queries = ` + internalNoteDetail(_id: String!): InternalNote internalNotes(contentType: String!, contentTypeId: String): [InternalNote] `; export const mutations = ` internalNotesAdd(contentType: String!, contentTypeId: String, content: String, mentionedUserIds: [String]): InternalNote - internalNotesEdit(_id: String!, content: String): InternalNote + internalNotesEdit(_id: String!, content: String, mentionedUserIds: [String]): InternalNote internalNotesRemove(_id: String!): InternalNote `; diff --git a/src/data/schema/knowledgeBase.ts b/src/data/schema/knowledgeBase.ts index 3395f40ef..2181cb0b6 100644 --- a/src/data/schema/knowledgeBase.ts +++ b/src/data/schema/knowledgeBase.ts @@ -5,6 +5,8 @@ export const types = ` summary: String content: String status: String + reactionChoices: [String] + reactionCounts: JSON createdBy: String createdUser: User createdDate: Date @@ -17,6 +19,7 @@ export const types = ` summary: String content: String! status: String! + reactionChoices: [String] categoryIds: [String] } @@ -32,6 +35,8 @@ export const types = ` modifiedDate: Date firstTopic: KnowledgeBaseTopic + authors: [User] + numOfArticles: Float } input KnowledgeBaseCategoryDoc { @@ -66,6 +71,10 @@ export const types = ` backgroundImage: String languageCode: String } + + type KnowledgeBaseLoader { + loadType: String + } `; export const queries = ` diff --git a/src/data/schema/log.ts b/src/data/schema/log.ts index 62982aa41..ebdab6bd5 100644 --- a/src/data/schema/log.ts +++ b/src/data/schema/log.ts @@ -10,12 +10,22 @@ export const types = ` objectId: String unicode: String description: String + addedData: String + changedData: String + unchangedData: String + removedData: String + extraDesc: String } type LogList { logs: [Log] totalCount: Int } + + type SchemaField { + name: String + label: String + } `; export const queries = ` @@ -25,6 +35,9 @@ export const queries = ` userId: String, action: String, page: Int, - perPage: Int + perPage: Int, + type: String ): LogList + + getDbSchemaLabels(type: String): [SchemaField] `; diff --git a/src/data/schema/messengerApp.ts b/src/data/schema/messengerApp.ts index b712dd35c..6c6d96cf1 100644 --- a/src/data/schema/messengerApp.ts +++ b/src/data/schema/messengerApp.ts @@ -7,15 +7,28 @@ export const types = ` credentials: JSON accountId: String } -`; -export const queries = ` - messengerApps(kind: String): [MessengerApp] - messengerAppsCount(kind: String): Int + input WebSiteMessengerAppInput { + description: String + buttonText: String + url: String + } + + input KnowledgeBaseMessengerAppInput { + topicId: String + } + + input LeadMessengerAppInput { + formCode: String + } + + input MessengerAppsInput { + websites: [WebSiteMessengerAppInput] + knowledgebases: [KnowledgeBaseMessengerAppInput] + leads: [LeadMessengerAppInput] + } `; export const mutations = ` - messengerAppsAddKnowledgebase(name: String!, integrationId: String!, topicId: String!): MessengerApp - messengerAppsAddLead(name: String!, integrationId: String!, formId: String!): MessengerApp - messengerAppsRemove(_id: String!): JSON + messengerAppSave(integrationId: String!, messengerApps: MessengerAppsInput): String `; diff --git a/src/data/schema/notification.ts b/src/data/schema/notification.ts index e158150e1..e84f94d88 100644 --- a/src/data/schema/notification.ts +++ b/src/data/schema/notification.ts @@ -5,6 +5,7 @@ export const types = ` title: String link: String content: String + action: String createdUser: User receiver: String date: Date @@ -36,5 +37,5 @@ export const queries = ` export const mutations = ` notificationsSaveConfig (notifType: String!, isAllowed: Boolean): NotificationConfiguration - notificationsMarkAsRead (_ids: [String]) : JSON + notificationsMarkAsRead (_ids: [String], contentTypeId: String) : JSON `; diff --git a/src/data/schema/permission.ts b/src/data/schema/permission.ts index 9f67d890b..c7adeaf96 100644 --- a/src/data/schema/permission.ts +++ b/src/data/schema/permission.ts @@ -25,6 +25,7 @@ export const types = ` name: String! description: String! memberIds: [String] + members: [User] } `; @@ -32,7 +33,8 @@ const commonParams = ` module: String, action: String, userId: String, - groupId: String + groupId: String, + allowed: Boolean `; const commonUserGroupParams = ` @@ -61,5 +63,6 @@ export const mutations = ` permissionsRemove(ids: [String]!): JSON usersGroupsAdd(${commonUserGroupParams}): UsersGroup usersGroupsEdit(_id: String!, ${commonUserGroupParams}): UsersGroup - usersGroupsRemove(_id: String!): JSON + usersGroupsRemove(_id: String!): JSON + usersGroupsCopy(_id: String!, memberIds: [String]): UsersGroup `; diff --git a/src/data/schema/pipelineLabel.ts b/src/data/schema/pipelineLabel.ts new file mode 100644 index 000000000..06e7df0db --- /dev/null +++ b/src/data/schema/pipelineLabel.ts @@ -0,0 +1,28 @@ +export const types = ` + type PipelineLabel { + _id: String! + name: String! + colorCode: String + pipelineId: String + createdBy: String + createdAt: Date + } +`; + +const commonParams = ` + name: String! + colorCode: String! + pipelineId: String! +`; + +export const queries = ` + pipelineLabels(pipelineId: String!): [PipelineLabel] + pipelineLabelDetail(_id: String!): PipelineLabel +`; + +export const mutations = ` + pipelineLabelsAdd(${commonParams}): PipelineLabel + pipelineLabelsEdit(_id: String!, ${commonParams}): PipelineLabel + pipelineLabelsRemove(_id: String!): JSON + pipelineLabelsLabel(pipelineId: String!, targetId: String!, labelIds: [String!]!): String +`; diff --git a/src/data/schema/pipelineTemplate.ts b/src/data/schema/pipelineTemplate.ts new file mode 100644 index 000000000..f89d19f50 --- /dev/null +++ b/src/data/schema/pipelineTemplate.ts @@ -0,0 +1,45 @@ +export const types = ` + type PipelineTemplateStage { + _id: String! + name: String! + formId: String + order: Int + } + + input PipelineTemplateStageInput { + _id: String! + name: String! + formId: String + } + + type PipelineTemplate { + _id: String! + name: String! + description: String + type: String + isDefinedByErxes: Boolean + stages: [PipelineTemplateStage] + createdBy: String + createdAt: Date + } +`; + +const commonParams = ` + name: String! + description: String + type: String! + stages: [PipelineTemplateStageInput] +`; + +export const queries = ` + pipelineTemplates(type: String!): [PipelineTemplate] + pipelineTemplateDetail(_id: String!): PipelineTemplate + pipelineTemplatesTotalCount: Int +`; + +export const mutations = ` + pipelineTemplatesAdd(${commonParams}): PipelineTemplate + pipelineTemplatesEdit(_id: String!, ${commonParams}): PipelineTemplate + pipelineTemplatesRemove(_id: String!): JSON + pipelineTemplatesDuplicate(_id: String!): PipelineTemplate +`; diff --git a/src/data/schema/product.ts b/src/data/schema/product.ts index 6694a09d8..da6e35988 100644 --- a/src/data/schema/product.ts +++ b/src/data/schema/product.ts @@ -1,28 +1,69 @@ export const types = ` + type ProductCategory { + _id: String! + name: String + description: String + parentId: String + code: String! + order: String! + + isRoot: Boolean + productCount: Int + } + type Product { _id: String! name: String + code: String type: String description: String sku: String + unitPrice: Float + categoryId: String + customFieldsData: JSON createdAt: Date + getTags: [Tag] + tagIds: [String] + + category: ProductCategory } `; -const params = ` - name: String!, +const productParams = ` + name: String, + categoryId: String, type: String, description: String, sku: String, + unitPrice: Float, + code: String, + customFieldsData: JSON +`; + +const productCategoryParams = ` + name: String!, + code: String!, + description: String, + parentId: String, `; export const queries = ` - products(type: String, searchValue: String, page: Int, perPage: Int ids: [String]): [Product] + productCategories(parentId: String, searchValue: String): [ProductCategory] + productCategoriesTotalCount: Int + productCategoryDetail(_id: String): ProductCategory + + products(type: String, categoryId: String, searchValue: String, tag: String, page: Int, perPage: Int ids: [String]): [Product] productsTotalCount(type: String): Int + productDetail(_id: String): Product + productCountByTags: JSON `; export const mutations = ` - productsAdd(${params}): Product - productsEdit(_id: String!, ${params}): Product - productsRemove(_id: String!): JSON + productsAdd(${productParams}): Product + productsEdit(_id: String!, ${productParams}): Product + productsRemove(productIds: [String!]): JSON + + productCategoriesAdd(${productCategoryParams}): ProductCategory + productCategoriesEdit(_id: String!, ${productCategoryParams}): ProductCategory + productCategoriesRemove(_id: String!): JSON `; diff --git a/src/data/schema/robot.ts b/src/data/schema/robot.ts new file mode 100644 index 000000000..f33ab87d8 --- /dev/null +++ b/src/data/schema/robot.ts @@ -0,0 +1,39 @@ +export const types = ` + type RobotEntry { + _id: String + action: String + data: JSON + } + + type OnboardingGetAvailableFeaturesResponse { + name: String + settings: [String] + showSettings: Boolean + isComplete: Boolean + } + + type OnboardingNotification { + userId: String + type: String + } + + type OnboardingHistory { + _id: String + userId: String + isCompleted: Boolean + completedSteps: [String] + } +`; + +export const queries = ` + robotEntries(isNotified: Boolean, action: String, parentId: String): [RobotEntry] + onboardingStepsCompleteness(steps: [String]): JSON + onboardingGetAvailableFeatures: [OnboardingGetAvailableFeaturesResponse] +`; + +export const mutations = ` + robotEntriesMarkAsNotified(_id: String): [RobotEntry] + onboardingCheckStatus: String + onboardingForceComplete: JSON + onboardingCompleteShowStep(step: String): JSON +`; diff --git a/src/data/schema/segment.ts b/src/data/schema/segment.ts index 0fda24cfb..396354e38 100644 --- a/src/data/schema/segment.ts +++ b/src/data/schema/segment.ts @@ -1,11 +1,21 @@ export const types = ` - input SegmentCondition { - field: String, + input EventAttributeFilter { + name: String, operator: String, value: String, - dateUnit: String, + } + + input SegmentCondition { type: String, - brandId: String + + propertyName: String, + propertyOperator: String, + propertyValue: String, + + eventName: String, + eventOccurence: String, + eventOccurenceValue: Float, + eventAttributeFilters: [EventAttributeFilter], } type Segment { @@ -15,7 +25,6 @@ export const types = ` description: String subOf: String color: String - connector: String conditions: JSON getSubSegments: [Segment] @@ -23,9 +32,11 @@ export const types = ` `; export const queries = ` - segments(contentType: String!): [Segment] + segments(contentTypes: [String]!): [Segment] segmentDetail(_id: String): Segment segmentsGetHeads: [Segment] + segmentsEvents(contentType: String!): [JSON] + segmentsPreviewCount(contentType: String!, conditions: JSON, subOf: String): Int `; const commonFields = ` @@ -33,7 +44,6 @@ const commonFields = ` description: String, subOf: String, color: String, - connector: String, conditions: [SegmentCondition] `; diff --git a/src/data/schema/task.ts b/src/data/schema/task.ts index 05bd4443f..3b8d9a528 100644 --- a/src/data/schema/task.ts +++ b/src/data/schema/task.ts @@ -1,30 +1,19 @@ -const commonTypes = ` - order: Int - createdAt: Date -`; +import { commonDragParams, commonMutationParams, commonTypes, conformityQueryFields, copyParams } from './common'; export const types = ` type Task { _id: String! - name: String! - stageId: String - boardId: String - companyIds: [String] - customerIds: [String] - assignedUserIds: [String] - closeDate: Date - description: String - priority: String companies: [Company] customers: [Customer] - assignedUsers: [User] - isWatched: Boolean - stage: Stage - pipeline: Pipeline - modifiedAt: Date - modifiedBy: String + timeTrack: TimeTrack ${commonTypes} } + + type TimeTrack { + status: String, + timeSpent: Int, + startDate: String + } `; export const queries = ` @@ -38,32 +27,25 @@ export const queries = ` skip: Int search: String assignedUserIds: [String] - nextDay: String - nextWeek: String - nextMonth: String - noCloseDate: String - overdue: String + closeDateType: String priority: [String] + labelIds: [String] + sortField: String + sortDirection: Int + userIds: [String] + ${conformityQueryFields} ): [Task] -`; - -const commonParams = ` - name: String!, - stageId: String, - assignedUserIds: [String], - companyIds: [String], - customerIds: [String], - closeDate: Date, - description: String, - order: Int, - priority: String, + archivedTasks(pipelineId: String!, search: String, page: Int, perPage: Int): [Task] + archivedTasksCount(pipelineId: String!, search: String): Int `; export const mutations = ` - tasksAdd(${commonParams}): Task - tasksEdit(_id: String!, ${commonParams}): Task - tasksChange( _id: String!, destinationStageId: String): Task - tasksUpdateOrder(stageId: String!, orders: [OrderItem]): [Task] + tasksAdd(name: String!, ${copyParams}, ${commonMutationParams}): Task + tasksEdit(_id: String!, name: String, ${commonMutationParams}): Task + tasksChange(${commonDragParams}): Task tasksRemove(_id: String!): Task tasksWatch(_id: String, isAdd: Boolean): Task + tasksCopy(_id: String!, proccessId: String): Task + tasksArchive(stageId: String!, proccessId: String): String + taskUpdateTimeTracking(_id: String!, status: String!, timeSpent: Int!, startDate: String): JSON `; diff --git a/src/data/schema/ticket.ts b/src/data/schema/ticket.ts index ea50f1453..85e072871 100644 --- a/src/data/schema/ticket.ts +++ b/src/data/schema/ticket.ts @@ -1,29 +1,11 @@ -const commonTypes = ` - order: Int - createdAt: Date -`; +import { commonDragParams, commonMutationParams, commonTypes, conformityQueryFields, copyParams } from './common'; export const types = ` type Ticket { _id: String! - name: String! - stageId: String - boardId: String - companyIds: [String] - customerIds: [String] - assignedUserIds: [String] - closeDate: Date - description: String - priority: String source: String companies: [Company] customers: [Customer] - assignedUsers: [User] - isWatched: Boolean - stage: Stage - pipeline: Pipeline - modifiedAt: Date - modifiedBy: String ${commonTypes} } `; @@ -39,34 +21,29 @@ export const queries = ` skip: Int search: String assignedUserIds: [String] - nextDay: String - nextWeek: String - nextMonth: String - noCloseDate: String - overdue: String + closeDateType: String priority: [String] source: [String] + labelIds: [String] + sortField: String + sortDirection: Int + userIds: [String] + ${conformityQueryFields} ): [Ticket] + archivedTickets(pipelineId: String!, search: String, page: Int, perPage: Int): [Ticket] + archivedTicketsCount(pipelineId: String!, search: String): Int `; -const commonParams = ` - name: String!, - stageId: String, - assignedUserIds: [String], - companyIds: [String], - customerIds: [String], - closeDate: Date, - description: String, - order: Int, - priority: String, - source: String +const ticketMutationParams = ` + source: String, `; export const mutations = ` - ticketsAdd(${commonParams}): Ticket - ticketsEdit(_id: String!, ${commonParams}): Ticket - ticketsChange( _id: String!, destinationStageId: String): Ticket - ticketsUpdateOrder(stageId: String!, orders: [OrderItem]): [Ticket] + ticketsAdd(name: String!, ${copyParams}, ${ticketMutationParams}, ${commonMutationParams}): Ticket + ticketsEdit(_id: String!, name: String, ${ticketMutationParams}, ${commonMutationParams}): Ticket + ticketsChange(${commonDragParams}): Ticket ticketsRemove(_id: String!): Ticket ticketsWatch(_id: String, isAdd: Boolean): Ticket + ticketsCopy(_id: String!, proccessId: String): Ticket + ticketsArchive(stageId: String!, proccessId: String): String `; diff --git a/src/data/schema/user.ts b/src/data/schema/user.ts index cb12aa0bf..bac312a05 100644 --- a/src/data/schema/user.ts +++ b/src/data/schema/user.ts @@ -6,15 +6,7 @@ export const types = ` position: String location: String description: String - } - - input UserLinks { - linkedIn: String - twitter: String - facebook: String - youtube: String - github: String - website: String + operatorPhone: String } input EmailSignature { @@ -24,6 +16,7 @@ export const types = ` input InvitationEntry { email: String + password: String groupId: String } @@ -34,32 +27,30 @@ export const types = ` position: String location: String description: String - } - - type UserLinksType { - linkedIn: String - twitter: String - facebook: String - github: String - youtube: String - website: String + operatorPhone: String } type User { _id: String! + createdAt: Date username: String email: String isActive: Boolean details: UserDetailsType - links: UserLinksType + links: JSON status: String - hasSeenOnBoard: Boolean emailSignatures: JSON getNotificationByEmail: Boolean groupIds: [String] + brandIds: [String] + doNotDisturb: String + brands: [Brand] isOwner: Boolean permissionActions: JSON + configs: JSON + configsConstants: [JSON] + onboardingHistory: OnboardingHistory } type UserConversationListResponse { @@ -72,19 +63,23 @@ const commonParams = ` username: String!, email: String!, details: UserDetails, - links: UserLinks, + links: JSON, channelIds: [String], groupIds: [String] + brandIds: [String] `; const commonSelector = ` searchValue: String, isActive: Boolean, - ids: [String] + requireUsername: Boolean, + ids: [String], + brandIds: [String] `; export const queries = ` users(page: Int, perPage: Int, status: String ${commonSelector}): [User] + allUsers(isActive: Boolean): [User] userDetail(_id: String): User usersTotalCount(${commonSelector}): Int currentUser: User @@ -92,15 +87,17 @@ export const queries = ` `; export const mutations = ` + usersCreateOwner(email: String!, password: String!, firstName: String!, lastName: String, subscribeEmail: Boolean): String login(email: String!, password: String! deviceToken: String): String logout: String forgotPassword(email: String!): String! resetPassword(token: String!, newPassword: String!): JSON + usersResetMemberPassword(_id: String!, newPassword: String!): User usersEditProfile( username: String!, email: String!, details: UserDetails, - links: UserLinks + links: JSON password: String! ): User usersEdit(_id: String!, ${commonParams}): User diff --git a/src/data/schema/webhook.ts b/src/data/schema/webhook.ts new file mode 100644 index 000000000..943653239 --- /dev/null +++ b/src/data/schema/webhook.ts @@ -0,0 +1,33 @@ +export const types = ` + +input WebhookActionInput { + type: String + action: String + label: String +}, + +type WebhookAction { + type: String + action: String + label: String +}, + +type Webhook { + _id: String! + url: String! + token: String + actions: [WebhookAction] + status: String +}`; + +export const queries = ` + webhooks: [Webhook] + webhookDetail(_id: String!): Webhook + webhooksTotalCount: Int +`; + +export const mutations = ` + webhooksAdd(url: String!, actions: [WebhookActionInput]): Webhook + webhooksEdit(_id: String!,url: String!, actions: [WebhookActionInput]): Webhook + webhooksRemove(_id: String!): JSON +`; diff --git a/src/data/schema/widget.ts b/src/data/schema/widget.ts new file mode 100644 index 000000000..ecbf0f748 --- /dev/null +++ b/src/data/schema/widget.ts @@ -0,0 +1,129 @@ +export const types = ` + type MessengerConnectResponse { + integrationId: String + uiOptions: JSON + languageCode: String + messengerData: JSON + customerId: String + brand: Brand + } + + type ConversationDetailResponse { + _id: String + messages: [ConversationMessage] + operatorStatus: String + participatedUsers: [User] + isOnline: Boolean + supporters: [User] + } + + type FormConnectResponse { + integration: Integration + form: Form + } + + type SaveFormResponse { + status: String! + errors: [Error] + messageId: String + } + + type Error { + fieldId: String + code: String + text: String + } + + type MessengerSupportersResponse { + supporters: [User] + isOnline: Boolean + serverTime: String + } + + input FieldValueInput { + _id: String! + type: String + validation: String + text: String + value: String + } +`; + +export const queries = ` + widgetsConversations(integrationId: String!, customerId: String!): [Conversation] + widgetsConversationDetail(_id: String, integrationId: String!): ConversationDetailResponse + widgetsGetMessengerIntegration(brandCode: String!): Integration + widgetsMessages(conversationId: String): [ConversationMessage] + widgetsUnreadCount(conversationId: String): Int + widgetsTotalUnreadCount(integrationId: String!, customerId: String!): Int + widgetsMessengerSupporters(integrationId: String!): MessengerSupportersResponse + widgetsKnowledgeBaseArticles(topicId: String!, searchString: String) : [KnowledgeBaseArticle] + widgetsKnowledgeBaseTopicDetail(_id: String!): KnowledgeBaseTopic +`; + +export const mutations = ` + widgetsMessengerConnect( + brandCode: String! + email: String + phone: String + code: String + isUser: Boolean + + companyData: JSON + data: JSON + + cachedCustomerId: String + deviceToken: String + ): MessengerConnectResponse + + widgetsSaveBrowserInfo( + customerId: String! + browserInfo: JSON! + ): ConversationMessage + + widgetsInsertMessage( + integrationId: String! + customerId: String! + conversationId: String + message: String, + attachments: [AttachmentInput], + contentType: String + ): ConversationMessage + + widgetBotRequest( + customerId: String! + conversationId: String! + integrationId: String!, + message: String! + payload: String! + type: String! + ): JSON + + widgetsReadConversationMessages(conversationId: String): JSON + widgetsSaveCustomerGetNotified(customerId: String!, type: String!, value: String!): JSON + + widgetsLeadConnect( + brandCode: String!, + formCode: String!, + cachedCustomerId: String + ): FormConnectResponse + + widgetsSaveLead( + integrationId: String! + formId: String! + submissions: [FieldValueInput] + browserInfo: JSON! + cachedCustomerId: String + ): SaveFormResponse + + widgetsSendEmail( + toEmails: [String] + fromEmail: String + title: String + content: String + ): String + + widgetsKnowledgebaseIncReactionCount(articleId: String!, reactionChoice: String!): String + widgetsLeadIncreaseViewCount(formId: String!): JSON + widgetsSendTypingInfo(conversationId: String!, text: String): String +`; diff --git a/src/data/types.ts b/src/data/types.ts new file mode 100644 index 000000000..a2a8d554b --- /dev/null +++ b/src/data/types.ts @@ -0,0 +1,18 @@ +import * as express from 'express'; +import { IUserDocument } from '../db/models/definitions/users'; + +export interface IContext { + res: express.Response; + requestInfo: any; + user: IUserDocument; + docModifier: (doc: T) => any; + brandIdSelector: {}; + userBrandIdsSelector: {}; + commonQuerySelector: {}; + commonQuerySelectorElk: {}; + singleBrandIdSelector: {}; + dataSources: { + EngagesAPI: any; + IntegrationsAPI: any; + }; +} diff --git a/src/data/utils.ts b/src/data/utils.ts index 1df9e1afa..52ae71917 100644 --- a/src/data/utils.ts +++ b/src/data/utils.ts @@ -1,20 +1,47 @@ import * as AWS from 'aws-sdk'; -import * as EmailValidator from 'email-deep-validator'; import * as fileType from 'file-type'; import * as admin from 'firebase-admin'; import * as fs from 'fs'; import * as Handlebars from 'handlebars'; import * as nodemailer from 'nodemailer'; +import * as path from 'path'; import * as requestify from 'requestify'; +import * as strip from 'strip'; import * as xlsxPopulate from 'xlsx-populate'; -import { Customers, Notifications, Users } from '../db/models'; -import { IUserDocument } from '../db/models/definitions/users'; +import { Configs, Customers, EmailDeliveries, Notifications, Users, Webhooks } from '../db/models'; +import { IBrandDocument } from '../db/models/definitions/brands'; +import { WEBHOOK_STATUS } from '../db/models/definitions/constants'; +import { ICustomer } from '../db/models/definitions/customers'; +import { EMAIL_DELIVERY_STATUS } from '../db/models/definitions/emailDeliveries'; +import { IUser, IUserDocument } from '../db/models/definitions/users'; +import { OnboardingHistories } from '../db/models/Robot'; import { debugBase, debugEmail, debugExternalApi } from '../debuggers'; +import memoryStorage from '../inmemoryStorage'; +import { graphqlPubsub } from '../pubsub'; +import { fieldsCombinedByContentType } from './modules/fields/utils'; + +export const uploadsFolderPath = path.join(__dirname, '../private/uploads'); + +export const initFirebase = (code: string): void => { + if (code.length === 0) { + return; + } + + const codeString = code.trim(); + + if (codeString[0] === '{' && codeString[codeString.length - 1] === '}') { + const serviceAccount = JSON.parse(codeString); + + if (serviceAccount.private_key) { + admin.initializeApp({ credential: admin.credential.cert(serviceAccount) }); + } + } +}; /* * Check that given file is not harmful */ -export const checkFile = async file => { +export const checkFile = async (file, source?: string) => { if (!file) { throw new Error('Invalid file'); } @@ -22,7 +49,7 @@ export const checkFile = async file => { const { size } = file; // 20mb - if (size > 20000000) { + if (size > 20 * 1024 * 1024) { return 'Too large file'; } @@ -32,23 +59,40 @@ export const checkFile = async file => { // determine file type using magic numbers const ft = fileType(buffer); + const unsupportedMimeTypes = ['text/csv', 'image/svg+xml', 'text/plain', 'application/vnd.ms-excel']; + + const oldMsOfficeDocs = ['application/msword', 'application/vnd.ms-excel', 'application/vnd.ms-powerpoint']; + + // allow csv, svg to be uploaded + if (!ft && unsupportedMimeTypes.includes(file.type)) { + return 'ok'; + } + if (!ft) { - return 'Invalid file'; + return 'Invalid file type'; } const { mime } = ft; - if ( - ![ - 'image/png', - 'image/jpeg', - 'image/jpg', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'application/pdf', - ].includes(mime) - ) { - return 'Invalid file'; + // allow old ms office docs to be uploaded + if (mime === 'application/x-msi' && oldMsOfficeDocs.includes(file.type)) { + return 'ok'; + } + + const defaultMimeTypes = [ + 'image/png', + 'image/jpeg', + 'image/jpg', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/pdf', + 'image/gif', + ]; + + const UPLOAD_FILE_TYPES = await getConfig(source === 'widgets' ? 'WIDGETS_UPLOAD_FILE_TYPES' : 'UPLOAD_FILE_TYPES'); + + if (!((UPLOAD_FILE_TYPES && UPLOAD_FILE_TYPES.split(',')) || defaultMimeTypes).includes(mime)) { + return 'Invalid configured file type'; } return 'ok'; @@ -57,29 +101,41 @@ export const checkFile = async file => { /** * Create AWS instance */ -const createAWS = () => { - const AWS_ACCESS_KEY_ID = getEnv({ name: 'AWS_ACCESS_KEY_ID' }); - const AWS_SECRET_ACCESS_KEY = getEnv({ name: 'AWS_SECRET_ACCESS_KEY' }); - const AWS_BUCKET = getEnv({ name: 'AWS_BUCKET' }); +const createAWS = async () => { + const AWS_ACCESS_KEY_ID = await getConfig('AWS_ACCESS_KEY_ID'); + const AWS_SECRET_ACCESS_KEY = await getConfig('AWS_SECRET_ACCESS_KEY'); + const AWS_BUCKET = await getConfig('AWS_BUCKET'); + const AWS_COMPATIBLE_SERVICE_ENDPOINT = await getConfig('AWS_COMPATIBLE_SERVICE_ENDPOINT'); + const AWS_FORCE_PATH_STYLE = await getConfig('AWS_FORCE_PATH_STYLE'); if (!AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY || !AWS_BUCKET) { throw new Error('AWS credentials are not configured'); } - // initialize s3 - return new AWS.S3({ + const options: { accessKeyId: string; secretAccessKey: string; endpoint?: string; s3ForcePathStyle?: boolean } = { accessKeyId: AWS_ACCESS_KEY_ID, secretAccessKey: AWS_SECRET_ACCESS_KEY, - }); + }; + + if (AWS_FORCE_PATH_STYLE === 'true') { + options.s3ForcePathStyle = true; + } + + if (AWS_COMPATIBLE_SERVICE_ENDPOINT) { + options.endpoint = AWS_COMPATIBLE_SERVICE_ENDPOINT; + } + + // initialize s3 + return new AWS.S3(options); }; /** * Create Google Cloud Storage instance */ -const createGCS = () => { - const GOOGLE_APPLICATION_CREDENTIALS = getEnv({ name: 'GOOGLE_APPLICATION_CREDENTIALS' }); - const GOOGLE_PROJECT_ID = getEnv({ name: 'GOOGLE_PROJECT_ID' }); - const BUCKET = getEnv({ name: 'GOOGLE_CLOUD_STORAGE_BUCKET' }); +const createGCS = async () => { + const GOOGLE_APPLICATION_CREDENTIALS = await getConfig('GOOGLE_APPLICATION_CREDENTIALS'); + const GOOGLE_PROJECT_ID = await getConfig('GOOGLE_PROJECT_ID'); + const BUCKET = await getConfig('GOOGLE_CLOUD_STORAGE_BUCKET'); if (!GOOGLE_PROJECT_ID || !GOOGLE_APPLICATION_CREDENTIALS || !BUCKET) { throw new Error('Google Cloud Storage credentials are not configured'); @@ -97,16 +153,19 @@ const createGCS = () => { /* * Save binary data to amazon s3 */ -export const uploadFileAWS = async (file: { name: string; path: string }): Promise => { - const AWS_BUCKET = getEnv({ name: 'AWS_BUCKET' }); - const AWS_PREFIX = getEnv({ name: 'AWS_PREFIX', defaultValue: '' }); - const IS_PUBLIC = getEnv({ name: 'FILE_SYSTEM_PUBLIC', defaultValue: 'true' }); +export const uploadFileAWS = async ( + file: { name: string; path: string; type: string }, + forcePrivate: boolean = false, +): Promise => { + const IS_PUBLIC = forcePrivate ? false : await getConfig('FILE_SYSTEM_PUBLIC', 'true'); + const AWS_PREFIX = await getConfig('AWS_PREFIX'); + const AWS_BUCKET = await getConfig('AWS_BUCKET'); // initialize s3 - const s3 = createAWS(); + const s3 = await createAWS(); // generate unique name - const fileName = `${AWS_PREFIX}${Math.random()}${file.name}`; + const fileName = `${AWS_PREFIX}${Math.random()}${file.name.replace(/ /g, '')}`; // read file const buffer = await fs.readFileSync(file.path); @@ -115,6 +174,7 @@ export const uploadFileAWS = async (file: { name: string; path: string }): Promi const response: any = await new Promise((resolve, reject) => { s3.upload( { + ContentType: file.type, Bucket: AWS_BUCKET, Key: fileName, Body: buffer, @@ -133,15 +193,62 @@ export const uploadFileAWS = async (file: { name: string; path: string }): Promi return IS_PUBLIC === 'true' ? response.Location : fileName; }; +/* + * Delete file from amazon s3 + */ +export const deleteFileAWS = async (fileName: string) => { + const AWS_BUCKET = await getConfig('AWS_BUCKET'); + + const params = { Bucket: AWS_BUCKET, Key: fileName }; + + // initialize s3 + const s3 = await createAWS(); + + return new Promise((resolve, reject) => { + s3.deleteObject(params, err => { + if (err) { + return reject(err); + } + + return resolve('ok'); + }); + }); +}; + +/* + * Save file to local disk + */ +export const uploadFileLocal = async (file: { name: string; path: string; type: string }): Promise => { + const oldPath = file.path; + + if (!fs.existsSync(uploadsFolderPath)) { + fs.mkdirSync(uploadsFolderPath); + } + + const fileName = `${Math.random()}${file.name.replace(/ /g, '')}`; + const newPath = `${uploadsFolderPath}/${fileName}`; + const rawData = fs.readFileSync(oldPath); + + return new Promise((resolve, reject) => { + fs.writeFile(newPath, rawData, err => { + if (err) { + return reject(err); + } + + return resolve(fileName); + }); + }); +}; + /* * Save file to google cloud storage */ export const uploadFileGCS = async (file: { name: string; path: string; type: string }): Promise => { - const BUCKET = getEnv({ name: 'GOOGLE_CLOUD_STORAGE_BUCKET' }); - const IS_PUBLIC = getEnv({ name: 'FILE_SYSTEM_PUBLIC', defaultValue: 'true' }); + const BUCKET = await getConfig('GOOGLE_CLOUD_STORAGE_BUCKET'); + const IS_PUBLIC = await getConfig('FILE_SYSTEM_PUBLIC'); // initialize GCS - const storage = createGCS(); + const storage = await createGCS(); // select bucket const bucket = storage.bucket(BUCKET); @@ -175,15 +282,50 @@ export const uploadFileGCS = async (file: { name: string; path: string; type: st return IS_PUBLIC === 'true' ? metadata.mediaLink : name; }; +const deleteFileLocal = async (fileName: string) => { + return new Promise((resolve, reject) => { + fs.unlink(`${uploadsFolderPath}/${fileName}`, error => { + if (error) { + return reject(error); + } + + return resolve('deleted'); + }); + }); +}; + +const deleteFileGCS = async (fileName: string) => { + const BUCKET = await getConfig('GOOGLE_CLOUD_STORAGE_BUCKET'); + + // initialize GCS + const storage = await createGCS(); + + // select bucket + const bucket = storage.bucket(BUCKET); + + return new Promise((resolve, reject) => { + bucket + .file(fileName) + .delete() + .then(err => { + if (err) { + return reject(err); + } + + return resolve('ok'); + }); + }); +}; + /** * Read file from GCS, AWS */ export const readFileRequest = async (key: string): Promise => { - const UPLOAD_SERVICE_TYPE = getEnv({ name: 'UPLOAD_SERVICE_TYPE', defaultValue: 'AWS' }); + const UPLOAD_SERVICE_TYPE = await getConfig('UPLOAD_SERVICE_TYPE', 'AWS'); if (UPLOAD_SERVICE_TYPE === 'GCS') { - const GCS_BUCKET = getEnv({ name: 'GOOGLE_CLOUD_STORAGE_BUCKET' }); - const storage = createGCS(); + const GCS_BUCKET = await getConfig('GOOGLE_CLOUD_STORAGE_BUCKET'); + const storage = await createGCS(); const bucket = storage.bucket(GCS_BUCKET); @@ -195,41 +337,66 @@ export const readFileRequest = async (key: string): Promise => { return contents; } - const AWS_BUCKET = getEnv({ name: 'AWS_BUCKET' }); - const s3 = createAWS(); + if (UPLOAD_SERVICE_TYPE === 'AWS') { + const AWS_BUCKET = await getConfig('AWS_BUCKET'); + const s3 = await createAWS(); + + return new Promise((resolve, reject) => { + s3.getObject( + { + Bucket: AWS_BUCKET, + Key: key, + }, + (error, response) => { + if (error) { + return reject(error); + } + + return resolve(response.Body); + }, + ); + }); + } - return new Promise((resolve, reject) => { - s3.getObject( - { - Bucket: AWS_BUCKET, - Key: key, - }, - (error, response) => { + if (UPLOAD_SERVICE_TYPE === 'local') { + return new Promise((resolve, reject) => { + fs.readFile(`${uploadsFolderPath}/${key}`, (error, response) => { if (error) { return reject(error); } - return resolve(response.Body); - }, - ); - }); + return resolve(response); + }); + }); + } }; /* * Save binary data to amazon s3 */ -export const uploadFile = async (file, fromEditor = false): Promise => { - const IS_PUBLIC = getEnv({ name: 'FILE_SYSTEM_PUBLIC', defaultValue: 'true' }); - const DOMAIN = getEnv({ name: 'DOMAIN' }); - const UPLOAD_SERVICE_TYPE = getEnv({ name: 'UPLOAD_SERVICE_TYPE', defaultValue: 'AWS' }); +export const uploadFile = async (apiUrl: string, file, fromEditor = false): Promise => { + const IS_PUBLIC = await getConfig('FILE_SYSTEM_PUBLIC'); + const UPLOAD_SERVICE_TYPE = await getConfig('UPLOAD_SERVICE_TYPE', 'AWS'); + + let nameOrLink = ''; - const nameOrLink = UPLOAD_SERVICE_TYPE === 'AWS' ? await uploadFileAWS(file) : await uploadFileGCS(file); + if (UPLOAD_SERVICE_TYPE === 'AWS') { + nameOrLink = await uploadFileAWS(file); + } + + if (UPLOAD_SERVICE_TYPE === 'GCS') { + nameOrLink = await uploadFileGCS(file); + } + + if (UPLOAD_SERVICE_TYPE === 'local') { + nameOrLink = await uploadFileLocal(file); + } if (fromEditor) { const editorResult = { fileName: file.name, uploaded: 1, url: nameOrLink }; if (IS_PUBLIC !== 'true') { - editorResult.url = `${DOMAIN}/read-file?key=${nameOrLink}`; + editorResult.url = `${apiUrl}/read-file?key=${nameOrLink}`; } return editorResult; @@ -238,6 +405,22 @@ export const uploadFile = async (file, fromEditor = false): Promise => { return nameOrLink; }; +export const deleteFile = async (fileName: string): Promise => { + const UPLOAD_SERVICE_TYPE = await getConfig('UPLOAD_SERVICE_TYPE', 'AWS'); + + if (UPLOAD_SERVICE_TYPE === 'AWS') { + return deleteFileAWS(fileName); + } + + if (UPLOAD_SERVICE_TYPE === 'GCS') { + return deleteFileGCS(fileName); + } + + if (UPLOAD_SERVICE_TYPE === 'local') { + return deleteFileLocal(fileName); + } +}; + /** * Read contents of a file */ @@ -261,11 +444,11 @@ const applyTemplate = async (data: any, templateName: string) => { /** * Create default or ses transporter */ -export const createTransporter = ({ ses }) => { +export const createTransporter = async ({ ses }) => { if (ses) { - const AWS_SES_ACCESS_KEY_ID = getEnv({ name: 'AWS_SES_ACCESS_KEY_ID' }); - const AWS_SES_SECRET_ACCESS_KEY = getEnv({ name: 'AWS_SES_SECRET_ACCESS_KEY' }); - const AWS_REGION = getEnv({ name: 'AWS_REGION' }); + const AWS_SES_ACCESS_KEY_ID = await getConfig('AWS_SES_ACCESS_KEY_ID'); + const AWS_SES_SECRET_ACCESS_KEY = await getConfig('AWS_SES_SECRET_ACCESS_KEY'); + const AWS_REGION = await getConfig('AWS_REGION'); AWS.config.update({ region: AWS_REGION, @@ -278,40 +461,150 @@ export const createTransporter = ({ ses }) => { }); } - const MAIL_SERVICE = getEnv({ name: 'MAIL_SERVICE' }); - const MAIL_PORT = getEnv({ name: 'MAIL_PORT' }); - const MAIL_USER = getEnv({ name: 'MAIL_USER' }); - const MAIL_PASS = getEnv({ name: 'MAIL_PASS' }); - const MAIL_HOST = getEnv({ name: 'MAIL_HOST' }); + const MAIL_SERVICE = await getConfig('MAIL_SERVICE'); + const MAIL_PORT = await getConfig('MAIL_PORT'); + const MAIL_USER = await getConfig('MAIL_USER'); + const MAIL_PASS = await getConfig('MAIL_PASS'); + const MAIL_HOST = await getConfig('MAIL_HOST'); + + let auth; + + if (MAIL_USER && MAIL_PASS) { + auth = { + user: MAIL_USER, + pass: MAIL_PASS, + }; + } return nodemailer.createTransport({ service: MAIL_SERVICE, host: MAIL_HOST, port: MAIL_PORT, - auth: { - user: MAIL_USER, - pass: MAIL_PASS, - }, + auth, }); }; -/** - * Send email - */ -export const sendEmail = async ({ - toEmails, - fromEmail, - title, - template = {}, -}: { +export interface IEmailParams { toEmails?: string[]; fromEmail?: string; title?: string; - template?: { name?: string; data?: any; isCustom?: boolean }; -}) => { + customHtml?: string; + customHtmlData?: any; + template?: { name?: string; data?: any }; + modifier?: (data: any, email: string) => void; +} + +interface IReplacer { + key: string; + value: string; +} + +/** + * Replace editor dynamic content tags + */ +export const replaceEditorAttributes = async (args: { + content: string; + customer?: ICustomer; + user?: IUser; + customerFields?: string[]; + brand?: IBrandDocument; +}): Promise<{ replacers: IReplacer[]; replacedContent?: string; customerFields?: string[] }> => { + const { content, customer, user, brand } = args; + + const replacers: IReplacer[] = []; + + let replacedContent = content || ''; + let customerFields = args.customerFields; + + if (!customerFields || customerFields.length === 0) { + const possibleCustomerFields = await fieldsCombinedByContentType({ + contentType: 'customer', + }); + + customerFields = ['firstName', 'lastName']; + + for (const field of possibleCustomerFields) { + if (content.includes(`{{ customer.${field.name} }}`)) { + if (field.name.includes('trackedData')) { + customerFields.push('trackedData'); + continue; + } + + if (field.name.includes('customFieldsData')) { + customerFields.push('customFieldsData'); + continue; + } + + customerFields.push(field.name); + } + } + } + + // replace customer fields + if (customer) { + replacers.push({ + key: '{{ customer.name }}', + value: Customers.getCustomerName(customer), + }); + + for (const field of customerFields) { + if (field.includes('trackedData') || field.includes('customFieldsData')) { + const dbFieldName = field.includes('trackedData') ? 'trackedData' : 'customFieldsData'; + + for (const subField of customer[dbFieldName] || []) { + replacers.push({ + key: `{{ customer.${dbFieldName}.${subField.field} }}`, + value: subField.value || '', + }); + } + + continue; + } + + replacers.push({ + key: `{{ customer.${field} }}`, + value: customer[field] || '', + }); + } + } + + // replace user fields + if (user) { + replacers.push({ key: '{{ user.email }}', value: user.email || '' }); + + if (user.details) { + replacers.push({ key: '{{ user.fullName }}', value: user.details.fullName || '' }); + replacers.push({ key: '{{ user.position }}', value: user.details.position || '' }); + } + } + + // replace brand fields + if (brand) { + replacers.push({ key: '{{ brandName }}', value: brand.name || '' }); + } + + for (const replacer of replacers) { + const regex = new RegExp(replacer.key, 'gi'); + + replacedContent = replacedContent.replace(regex, replacer.value); + } + + return { replacedContent, replacers, customerFields }; +}; + +/** + * Send email + */ +export const sendEmail = async (params: IEmailParams) => { + const { toEmails = [], fromEmail, title, customHtml, customHtmlData, template = {}, modifier } = params; + const NODE_ENV = getEnv({ name: 'NODE_ENV' }); - const DEFAULT_EMAIL_SERVICE = getEnv({ name: 'DEFAULT_EMAIL_SERVICE', defaultValue: '' }); - const COMPANY_EMAIL_FROM = getEnv({ name: 'COMPANY_EMAIL_FROM' }); + const DEFAULT_EMAIL_SERVICE = await getConfig('DEFAULT_EMAIL_SERVICE', 'SES'); + const COMPANY_EMAIL_FROM = await getConfig('COMPANY_EMAIL_FROM', ''); + const AWS_SES_CONFIG_SET = await getConfig('AWS_SES_CONFIG_SET', ''); + const AWS_ACCESS_KEY_ID = await getConfig('AWS_ACCESS_KEY_ID', ''); + const AWS_SES_SECRET_ACCESS_KEY = await getConfig('AWS_SES_SECRET_ACCESS_KEY', ''); + const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN' }); // do not send email it is running in test mode if (NODE_ENV === 'test') { @@ -322,52 +615,95 @@ export const sendEmail = async ({ let transporter; try { - transporter = createTransporter({ ses: DEFAULT_EMAIL_SERVICE === 'SES' }); + transporter = await createTransporter({ ses: DEFAULT_EMAIL_SERVICE === 'SES' }); } catch (e) { return debugEmail(e.message); } - const { isCustom, data, name } = template; + const { data = {}, name } = template; - // generate email content by given template - let html = await applyTemplate(data, name || ''); + // for unsubscribe url + data.domain = MAIN_APP_DOMAIN; - if (!isCustom) { - html = await applyTemplate({ content: html }, 'base'); - } + for (const toEmail of toEmails) { + if (modifier) { + modifier(data, toEmail); + } + + // generate email content by given template + let html = await applyTemplate(data, name || 'base'); - return (toEmails || []).map(toEmail => { - const mailOptions = { + if (customHtml) { + html = Handlebars.compile(customHtml)(customHtmlData || {}); + } + + const mailOptions: any = { from: fromEmail || COMPANY_EMAIL_FROM, to: toEmail, subject: title, html, }; + let headers: { [key: string]: string } = {}; + + if (AWS_ACCESS_KEY_ID.length > 0 && AWS_SES_SECRET_ACCESS_KEY.length > 0) { + const emailDelivery = await EmailDeliveries.create({ + kind: 'transaction', + to: toEmail, + from: fromEmail || COMPANY_EMAIL_FROM, + subject: title, + body: html, + status: EMAIL_DELIVERY_STATUS.PENDING, + }); + + headers = { + 'X-SES-CONFIGURATION-SET': AWS_SES_CONFIG_SET || 'erxes', + EmailDeliveryId: emailDelivery._id, + }; + } else { + headers['X-SES-CONFIGURATION-SET'] = 'erxes'; + } + + mailOptions.headers = headers; + return transporter.sendMail(mailOptions, (error, info) => { debugEmail(error); debugEmail(info); }); - }); + } }; /** - * Send a notification + * Returns user's name or email */ -export const sendNotification = async ({ - createdUser, - receivers, - ...doc -}: { - createdUser?: string; +export const getUserDetail = (user: IUser) => { + return (user.details && user.details.fullName) || user.email; +}; + +export interface ISendNotification { + createdUser: IUserDocument; receivers: string[]; title: string; content: string; notifType: string; link: string; -}) => { + action: string; + contentType: string; + contentTypeId: string; +} + +/** + * Send a notification + */ +export const sendNotification = async (doc: ISendNotification) => { + const { createdUser, receivers, title, content, notifType, action, contentType, contentTypeId } = doc; + let link = doc.link; + + // remove duplicated ids + const receiverIds = [...new Set(receivers)]; + // collecting emails - const recipients = await Users.find({ _id: { $in: receivers } }); + const recipients = await Users.find({ _id: { $in: receiverIds }, isActive: true, doNotDisturb: { $ne: 'Yes' } }); // collect recipient emails const toEmails: string[] = []; @@ -379,29 +715,58 @@ export const sendNotification = async ({ } // loop through receiver ids - for (const receiverId of receivers) { + for (const receiverId of receiverIds) { try { // send web and mobile notification - - await Notifications.createNotification({ ...doc, receiver: receiverId }, createdUser); + const notification = await Notifications.createNotification( + { link, title, content, notifType, receiver: receiverId, action, contentType, contentTypeId }, + createdUser._id, + ); + + graphqlPubsub.publish('notificationInserted', { + notificationInserted: { + _id: notification._id, + userId: receiverId, + title: notification.title, + content: notification.content, + }, + }); } catch (e) { // Any other error is serious if (e.message !== 'Configuration does not exist') { throw e; } } - } + } // end receiverIds loop + + const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN' }); + + link = `${MAIN_APP_DOMAIN}${link}`; + + // for controlling email template data filling + const modifier = (data: any, email: string) => { + const user = recipients.find(item => item.email === email); + + if (user) { + data.uid = user._id; + } + }; - return sendEmail({ + await sendEmail({ toEmails, title: 'Notification', template: { name: 'notification', data: { - notification: doc, + notification: { ...doc, link }, + action, + userName: getUserDetail(createdUser), }, }, + modifier, }); + + return true; }; /** @@ -427,26 +792,10 @@ interface IRequestParams { method?: string; headers?: { [key: string]: string }; params?: { [key: string]: string }; - body?: { [key: string]: string }; + body?: { [key: string]: any }; form?: { [key: string]: string }; } -export interface ILogQueryParams { - start?: string; - end?: string; - userId?: string; - action?: string; - page?: number; - perPage?: number; -} - -interface ILogParams { - type: string; - newData?: string; - description?: string; - object: any; -} - /** * Sends post request to specific url */ @@ -454,13 +803,6 @@ export const sendRequest = async ( { url, method, headers, form, body, params }: IRequestParams, errorMessage?: string, ) => { - const NODE_ENV = getEnv({ name: 'NODE_ENV' }); - const DOMAIN = getEnv({ name: 'DOMAIN' }); - - if (NODE_ENV === 'test') { - return; - } - debugExternalApi(` Sending request to url: ${url} @@ -472,7 +814,7 @@ export const sendRequest = async ( try { const response = await requestify.request(url, { method, - headers: { 'Content-Type': 'application/json', origin: DOMAIN, ...(headers || {}) }, + headers: { 'Content-Type': 'application/json', ...(headers || {}) }, form, body, params, @@ -487,170 +829,36 @@ export const sendRequest = async ( return responseBody; } catch (e) { - if (e.code === 'ECONNREFUSED') { - debugExternalApi(errorMessage); + if (e.code === 'ECONNREFUSED' || e.code === 'ENOTFOUND') { throw new Error(errorMessage); } else { - debugExternalApi(`Error occurred : ${e.body}`); - throw new Error(e.body); + const message = e.body || e.message; + throw new Error(message); } } }; -/** - * Send request to integrations api - */ -export const fetchIntegrationApi = ({ path, method, body, params }: IRequestParams) => { - const INTEGRATIONS_API_DOMAIN = getEnv({ name: 'INTEGRATIONS_API_DOMAIN' }); - - return sendRequest( - { url: `${INTEGRATIONS_API_DOMAIN}${path}`, method, body, params }, - 'Failed to connect integration api. Check INTEGRATIONS_API_DOMAIN env or integration api is not running', - ); -}; - -/** - * Send request to crons api - */ -export const fetchCronsApi = ({ path, method, body, params }: IRequestParams) => { - const CRONS_API_DOMAIN = getEnv({ name: 'CRONS_API_DOMAIN' }); - - return sendRequest( - { url: `${CRONS_API_DOMAIN}${path}`, method, body, params }, - 'Failed to connect crons api. Check CRONS_API_DOMAIN env or crons api is not running', - ); -}; - -/** - * Send request to workers api - */ -export const fetchWorkersApi = ({ path, method, body, params }: IRequestParams) => { - const WORKERS_API_DOMAIN = getEnv({ name: 'WORKERS_API_DOMAIN' }); - - return sendRequest( - { url: `${WORKERS_API_DOMAIN}${path}`, method, body, params }, - 'Failed to connect workers api. Check WORKERS_API_DOMAIN env or workers api is not running', - ); -}; - -/** - * Prepares a create log request to log server - * @param params Log document params - * @param user User information from mutation context - */ -export const putCreateLog = (params: ILogParams, user: IUserDocument) => { - const doc = { ...params, action: 'create', object: JSON.stringify(params.object) }; - - return putLog(doc, user); -}; - -/** - * Prepares a create log request to log server - * @param params Log document params - * @param user User information from mutation context - */ -export const putUpdateLog = (params: ILogParams, user: IUserDocument) => { - const doc = { ...params, action: 'update', object: JSON.stringify(params.object) }; - - return putLog(doc, user); -}; - -/** - * Prepares a create log request to log server - * @param params Log document params - * @param user User information from mutation context - */ -export const putDeleteLog = (params: ILogParams, user: IUserDocument) => { - const doc = { ...params, action: 'delete', object: JSON.stringify(params.object) }; - - return putLog(doc, user); -}; - -/** - * Sends a request to logs api - * @param {Object} body Request - * @param {Object} user User information from mutation context - */ -const putLog = (body: ILogParams, user: IUserDocument) => { - const LOGS_DOMAIN = getEnv({ name: 'LOGS_API_DOMAIN' }); - - if (!LOGS_DOMAIN) { - return; - } - - const doc = { - ...body, - createdBy: user._id, - unicode: user.username || user.email || user._id, - }; - - return sendRequest( - { url: `${LOGS_DOMAIN}/logs/create`, method: 'post', body: { params: JSON.stringify(doc) } }, - 'Failed to connect to logs api. Check whether LOGS_API_DOMAIN env is missing or logs api is not running', - ); -}; - -/** - * Sends a request to logs api - * @param {Object} param0 Request - */ -export const fetchLogs = (params: ILogQueryParams) => { - const LOGS_DOMAIN = getEnv({ name: 'LOGS_API_DOMAIN' }); - - if (!LOGS_DOMAIN) { - return { - logs: [], - totalCount: 0, - }; - } - - return sendRequest( - { url: `${LOGS_DOMAIN}/logs`, method: 'get', body: { params: JSON.stringify(params) } }, - 'Failed to connect to logs api. Check whether LOGS_API_DOMAIN env is missing or logs api is not running', - ); -}; - -/** - * Validates email using MX record resolver - * @param email as String - */ -export const validateEmail = async email => { - const NODE_ENV = getEnv({ name: 'NODE_ENV' }); - - if (NODE_ENV === 'test') { - return true; - } - - const emailValidator = new EmailValidator(); - const { validDomain, validMailbox } = await emailValidator.verify(email); - - if (!validDomain) { - return false; - } - - if (!validMailbox && validMailbox === null) { - return false; - } - - return true; -}; +export const registerOnboardHistory = ({ type, user }: { type: string; user: IUserDocument }) => + OnboardingHistories.getOrCreate({ type, user }) + .then(({ status }) => { + if (status === 'created') { + graphqlPubsub.publish('onboardingChanged', { + onboardingChanged: { userId: user._id, type }, + }); + } + }) + .catch(e => debugBase(e)); -export const authCookieOptions = () => { +export const authCookieOptions = (secure: boolean) => { const oneDay = 1 * 24 * 3600 * 1000; // 1 day const cookieOptions = { httpOnly: true, expires: new Date(Date.now() + oneDay), maxAge: oneDay, - secure: false, + secure, }; - const HTTPS = getEnv({ name: 'HTTPS', defaultValue: 'false' }); - - if (HTTPS === 'true') { - cookieOptions.secure = true; - } - return cookieOptions; }; @@ -661,10 +869,6 @@ export const getEnv = ({ name, defaultValue }: { name: string; defaultValue?: st return defaultValue; } - if (!value) { - debugBase(`Missing environment variable configuration for ${name}`); - } - return value || ''; }; @@ -680,11 +884,13 @@ export const sendMobileNotification = async ({ title, body, customerId, + conversationId, }: { receivers: string[]; customerId?: string; title: string; body: string; + conversationId: string; }): Promise => { if (!admin.apps.length) { return; @@ -704,17 +910,21 @@ export const sendMobileNotification = async ({ if (tokens.length > 0) { // send notification for (const token of tokens) { - await transporter.send({ token, notification: { title, body } }); + await transporter.send({ token, notification: { title, body }, data: { conversationId } }); } } }; -export const paginate = (collection, params: { page?: number; perPage?: number }) => { - const { page = 0, perPage = 0 } = params || {}; +export const paginate = (collection, params: { ids?: string[]; page?: number; perPage?: number }) => { + const { page = 0, perPage = 0, ids } = params || { ids: null }; const _page = Number(page || '1'); const _limit = Number(perPage || '20'); + if (ids) { + return collection; + } + return collection.limit(_limit).skip((_page - 1) * _limit); }; @@ -747,19 +957,220 @@ export const getToday = (date: Date): Date => { export const getNextMonth = (date: Date): { start: number; end: number } => { const today = getToday(date); + const currentMonth = new Date().getMonth(); + + if (currentMonth === 11) { + today.setFullYear(today.getFullYear() + 1); + } - const month = (new Date().getMonth() + 1) % 12; + const month = (currentMonth + 1) % 12; const start = today.setMonth(month, 1); const end = today.setMonth(month + 1, 0); return { start, end }; }; +/** + * Send to webhook + */ + +export const sendToWebhook = async (action: string, type: string, params: any) => { + const webhooks = await Webhooks.find({ 'actions.action': action, 'actions.type': type }); + + if (!webhooks) { + return; + } + + let data = params; + for (const webhook of webhooks) { + if (!webhook.url || webhook.url.length === 0) { + continue; + } + + if (action === 'delete') { + data = { type, object: { _id: params.object._id } }; + } + + sendRequest({ + url: webhook.url, + headers: { + 'Erxes-token': webhook.token || '', + }, + method: 'post', + body: { data: JSON.stringify(data), action, type }, + }) + .then(async () => { + await Webhooks.updateStatus(webhook._id, WEBHOOK_STATUS.AVAILABLE); + }) + .catch(async () => { + await Webhooks.updateStatus(webhook._id, WEBHOOK_STATUS.UNAVAILABLE); + }); + } +}; + export default { sendEmail, - validateEmail, sendNotification, sendMobileNotification, readFile, createTransporter, + sendToWebhook, +}; + +export const cleanHtml = (content?: string) => strip(content || '').substring(0, 100); + +export const validSearchText = (values: string[]) => { + const value = values.join(' '); + + if (value.length < 512) { + return value; + } + + return value.substring(0, 511); +}; + +const stringToRegex = (value: string) => { + const specialChars = [...'{}[]\\^$.|?*+()']; + + const result = [...value].map(char => (specialChars.includes(char) ? '.?\\' + char : '.?' + char)); + + return '.*' + result.join('').substring(2) + '.*'; +}; + +export const regexSearchText = (searchValue: string, searchKey = 'searchText') => { + const result: any[] = []; + + searchValue = searchValue.replace(/\s\s+/g, ' '); + + const words = searchValue.split(' '); + + for (const word of words) { + result.push({ [searchKey]: new RegExp(`${stringToRegex(word)}`, 'mui') }); + } + + return { $and: result }; +}; + +/** + * Check user ids whether its added or removed from array of ids + */ +export const checkUserIds = (oldUserIds: string[] = [], newUserIds: string[] = []) => { + const removedUserIds = oldUserIds.filter(e => !newUserIds.includes(e)); + + const addedUserIds = newUserIds.filter(e => !oldUserIds.includes(e)); + + return { addedUserIds, removedUserIds }; +}; + +/* + * Handle engage unsubscribe request + */ +export const handleUnsubscription = async (query: { cid: string; uid: string }) => { + const { cid, uid } = query; + + if (cid) { + await Customers.updateOne({ _id: cid }, { $set: { doNotDisturb: 'Yes' } }); + } + + if (uid) { + await Users.updateOne({ _id: uid }, { $set: { doNotDisturb: 'Yes' } }); + } +}; + +export const getConfigs = async () => { + const configsCache = await memoryStorage().get('configs_erxes_api'); + + if (configsCache && configsCache !== '{}') { + return JSON.parse(configsCache); + } + + const configsMap = {}; + const configs = await Configs.find({}); + + for (const config of configs) { + configsMap[config.code] = config.value; + } + + memoryStorage().set('configs_erxes_api', JSON.stringify(configsMap)); + + return configsMap; +}; + +export const getConfig = async (code, defaultValue?) => { + const configs = await getConfigs(); + + if (!configs[code]) { + return defaultValue; + } + + return configs[code]; +}; + +export const resetConfigsCache = () => { + memoryStorage().set('configs_erxes_api', ''); +}; + +export const frontendEnv = ({ name, req, requestInfo }: { name: string; req?: any; requestInfo?: any }): string => { + const cookies = req ? req.cookies : requestInfo.cookies; + const keys = Object.keys(cookies); + + const envs: { [key: string]: string } = {}; + + for (const key of keys) { + envs[key.replace('REACT_APP_', '')] = cookies[key]; + } + + return envs[name]; +}; + +export const getSubServiceDomain = ({ name }: { name: string }): string => { + const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN' }); + + const defaultMappings = { + API_DOMAIN: `${MAIN_APP_DOMAIN}/api`, + WIDGETS_DOMAIN: `${MAIN_APP_DOMAIN}/widgets`, + INTEGRATIONS_API_DOMAIN: `${MAIN_APP_DOMAIN}/integrations`, + LOGS_API_DOMAIN: `${MAIN_APP_DOMAIN}/logs`, + ENGAGES_API_DOMAIN: `${MAIN_APP_DOMAIN}/engages`, + VERIFIER_API_DOMAIN: `${MAIN_APP_DOMAIN}/verifier`, + }; + + const domain = getEnv({ name }); + + if (domain) { + return domain; + } + + return defaultMappings[name]; +}; + +export const chunkArray = (myArray, chunkSize: number) => { + let index = 0; + + const arrayLength = myArray.length; + const tempArray: any[] = []; + + for (index = 0; index < arrayLength; index += chunkSize) { + const myChunk = myArray.slice(index, index + chunkSize); + + // Do something if you want with the group + tempArray.push(myChunk); + } + + return tempArray; +}; + +/** + * Create s3 stream for excel file + */ +export const s3Stream = async (key: string, errorCallback: (error: any) => void): Promise => { + const AWS_BUCKET = await getConfig('AWS_BUCKET'); + + const s3 = await createAWS(); + + const stream = s3.getObject({ Bucket: AWS_BUCKET, Key: key }).createReadStream(); + + stream.on('error', errorCallback); + + return stream; }; diff --git a/src/data/verifierUtils.ts b/src/data/verifierUtils.ts new file mode 100644 index 000000000..bf23d942d --- /dev/null +++ b/src/data/verifierUtils.ts @@ -0,0 +1,180 @@ +import { Transform } from 'stream'; +import { Customers } from '../db/models'; +import { IValidationResponse, IVisitorContact } from '../db/models/definitions/customers'; +import { debugBase, debugExternalApi } from '../debuggers'; +import { getEnv, getSubServiceDomain, sendRequest } from './utils'; + +export const validateSingle = async (contact: IVisitorContact) => { + const EMAIL_VERIFIER_ENDPOINT = getEnv({ name: 'EMAIL_VERIFIER_ENDPOINT', defaultValue: '' }); + + const { email, phone } = contact; + + let body = {}; + + const hostname = getSubServiceDomain({ name: 'API_DOMAIN' }); + + phone ? (body = { phone, hostname }) : (body = { email, hostname }); + + const requestOptions = { + url: `${EMAIL_VERIFIER_ENDPOINT}/verify-single`, + method: 'POST', + body, + }; + + try { + await sendRequest(requestOptions); + } catch (e) { + debugExternalApi(`An error occurred while sending request to the email verifier. Error: ${e.message}`); + throw e; + } +}; + +export const updateContactValidationStatus = async (data: IValidationResponse) => { + const { email, phone, status } = data; + + if (email) { + await Customers.updateMany({ primaryEmail: email }, { $set: { emailValidationStatus: status } }); + } + + if (phone) { + await Customers.updateMany({ primaryPhone: phone }, { $set: { phoneValidationStatus: status } }); + } +}; + +export const validateBulk = async (verificationType: string) => { + const hostname = getSubServiceDomain({ name: 'API_DOMAIN' }); + + const EMAIL_VERIFIER_ENDPOINT = getEnv({ name: 'EMAIL_VERIFIER_ENDPOINT', defaultValue: '' }); + + if (verificationType === 'email') { + const emails: Array<{}> = []; + + const customerTransformerToEmailStream = new Transform({ + objectMode: true, + + transform(customer, _encoding, callback) { + emails.push(customer.primaryEmail); + + callback(); + }, + }); + + const customersEmailStream = (Customers.find( + { + primaryEmail: { $exists: true, $ne: null }, + $or: [{ emailValidationStatus: 'unknown' }, { emailValidationStatus: { $exists: false } }], + }, + { primaryEmail: 1, _id: 0 }, + ).limit(1000) as any).stream(); + + return new Promise((resolve, reject) => { + const pipe = customersEmailStream.pipe(customerTransformerToEmailStream); + + pipe.on('finish', async () => { + try { + const requestOptions = { + url: `${EMAIL_VERIFIER_ENDPOINT}/verify-bulk`, + method: 'POST', + body: { emails, hostname }, + }; + + sendRequest(requestOptions) + .then(res => { + debugBase(`Response: ${res}`); + }) + .catch(error => { + throw error; + }); + } catch (e) { + return reject(e); + } + + resolve('done'); + }); + }); + } + + const phones: Array<{}> = []; + + const customerTransformerStream = new Transform({ + objectMode: true, + + transform(customer, _encoding, callback) { + phones.push(customer.primaryPhone); + + callback(); + }, + }); + + const customersStream = (Customers.find( + { + primaryPhone: { $exists: true, $ne: null }, + $or: [{ phoneValidationStatus: 'unknown' }, { phoneValidationStatus: { $exists: false } }], + }, + { primaryPhone: 1, _id: 0 }, + ).limit(1000) as any).stream(); + + return new Promise((resolve, reject) => { + const pipe = customersStream.pipe(customerTransformerStream); + + pipe.on('finish', async () => { + try { + const requestOptions = { + url: `${EMAIL_VERIFIER_ENDPOINT}/verify-bulk`, + method: 'POST', + body: { phones, hostname }, + }; + + sendRequest(requestOptions) + .then(res => { + debugBase(`Response: ${res}`); + }) + .catch(error => { + throw error; + }); + } catch (e) { + return reject(e); + } + + resolve('done'); + }); + }); +}; + +export const updateContactsValidationStatus = async (type: string, data: []) => { + if (type === 'email') { + const bulkOps: Array<{ + updateMany: { + filter: { primaryEmail: string }; + update: { emailValidationStatus: string }; + }; + }> = []; + + for (const { email, status } of data) { + bulkOps.push({ + updateMany: { + filter: { primaryEmail: email }, + update: { emailValidationStatus: status }, + }, + }); + } + await Customers.bulkWrite(bulkOps); + } + + const phoneBulkOps: Array<{ + updateMany: { + filter: { primaryPhone: string }; + update: { phoneValidationStatus: string }; + }; + }> = []; + + for (const { phone, status } of data) { + phoneBulkOps.push({ + updateMany: { + filter: { primaryPhone: phone }, + update: { phoneValidationStatus: status }, + }, + }); + } + await Customers.bulkWrite(phoneBulkOps); +}; diff --git a/src/db/connection.ts b/src/db/connection.ts index 53defc1d7..f2fe3278d 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -13,7 +13,12 @@ dotenv.config(); const NODE_ENV = getEnv({ name: 'NODE_ENV' }); const MONGO_URL = getEnv({ name: 'MONGO_URL', defaultValue: '' }); -mongoose.set('useFindAndModify', false); +export const connectionOptions = { + useNewUrlParser: true, + useCreateIndex: true, + autoReconnect: true, + useFindAndModify: false, +}; mongoose.Promise = global.Promise; @@ -30,34 +35,58 @@ mongoose.connection debugDb(`Database connection error: ${MONGO_URL}`, error); }); -export function connect(URL?: string, poolSize?: number) { - return mongoose.connect( - URL || MONGO_URL, - { useNewUrlParser: true, useCreateIndex: true, poolSize: poolSize || 100, autoReconnect: true }, - ); -} +export const connect = async (URL?: string, options?) => { + return mongoose.connect(URL || MONGO_URL, { + ...connectionOptions, + ...(options || { poolSize: 100 }), + }); +}; export function disconnect() { return mongoose.connection.close(); } +/** + * Health check status + */ +export const mongoStatus = () => { + return new Promise((resolve, reject) => { + mongoose.connection.db.admin().ping((err, result) => { + if (err) { + return reject(err); + } + + return resolve(result); + }); + }); +}; + const schema = makeExecutableSchema({ typeDefs, resolvers, }); -export const graphqlRequest = async (source: string = '', name: string = '', args?: any, context?: any) => { - const user = await userFactory({}); - - const rootValue = {}; - +export const graphqlRequest = async (source: string = '', name: string = '', args?: any, context: any = {}) => { const res = { cookie: () => { return 'cookie'; }, }; - const response: any = await graphql(schema, source, rootValue, context || { user, res }, args); + const finalContext: any = {}; + + finalContext.requestInfo = { secure: false, cookies: [] }; + finalContext.dataSources = context.dataSources; + finalContext.user = context.user || (await userFactory({})); + finalContext.res = context.res || res; + finalContext.commonQuerySelector = {}; + finalContext.userBrandIdsSelector = {}; + finalContext.brandIdSelector = {}; + finalContext.docModifier = doc => doc; + + const rootValue = {}; + + const response: any = await graphql(schema, source, rootValue, finalContext, args); if (response.errors || !response.data) { throw response.errors; diff --git a/src/db/factories.ts b/src/db/factories.ts index 6d57d1aed..37b00e736 100644 --- a/src/db/factories.ts +++ b/src/db/factories.ts @@ -1,24 +1,30 @@ import { dateType } from 'aws-sdk/clients/sts'; // tslint:disable-line import * as faker from 'faker'; import * as Random from 'meteor-random'; +import * as momentTz from 'moment-timezone'; import { FIELDS_GROUPS_CONTENT_TYPES } from '../data/constants'; -import { IActionPerformer, IActivity, IContentType } from '../db/models/definitions/activityLogs'; import { ActivityLogs, Boards, Brands, Channels, + ChecklistItems, + Checklists, Companies, Configs, + Conformities, ConversationMessages, Conversations, Customers, Deals, + EmailDeliveries, EmailTemplates, EngageMessages, Fields, FieldsGroups, Forms, + FormSubmissions, + GrowthHacks, ImportHistory, Integrations, InternalNotes, @@ -28,10 +34,14 @@ import { MessengerApps, NotificationConfigurations, Notifications, + OnboardingHistories, Permissions, + PipelineLabels, Pipelines, + ProductCategories, Products, ResponseTemplates, + Scripts, Segments, Stages, Tags, @@ -39,47 +49,61 @@ import { Tickets, Users, UsersGroups, + Webhooks, } from './models'; +import { ICustomField } from './models/definitions/common'; import { - ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, - ACTIVITY_PERFORMER_TYPES, - ACTIVITY_TYPES, + BOARD_STATUSES, BOARD_TYPES, + CONVERSATION_OPERATOR_STATUS, + CONVERSATION_STATUSES, + FORM_TYPES, + MESSAGE_TYPES, NOTIFICATION_TYPES, + PROBABILITY, PRODUCT_TYPES, - STATUSES, + WEBHOOK_ACTIONS, } from './models/definitions/constants'; import { IEmail, IMessenger } from './models/definitions/engages'; import { IMessengerAppCrendentials } from './models/definitions/messengerApps'; import { IUserDocument } from './models/definitions/users'; +import PipelineTemplates from './models/PipelineTemplates'; + +export const getUniqueValue = async (collection: any, fieldName: string = 'code', defaultValue?: string) => { + const getRandomValue = (type: string) => (type === 'email' ? faker.internet.email() : Random.id()); + + let uniqueValue = defaultValue || getRandomValue(fieldName); + + let duplicated = await collection.findOne({ [fieldName]: uniqueValue }); + + while (duplicated) { + uniqueValue = getRandomValue(fieldName); + + duplicated = await collection.findOne({ [fieldName]: uniqueValue }); + } + + return uniqueValue; +}; interface IActivityLogFactoryInput { - performer?: IActionPerformer; - performedBy?: IActionPerformer; - activity?: IActivity; - contentType?: IContentType; + contentType?: string; + contentId?: string; + action?: string; + content?: any; + createdBy?: string; } -export const activityLogFactory = (params: IActivityLogFactoryInput) => { - const doc = { - activity: { - type: ACTIVITY_TYPES.INTERNAL_NOTE, - action: ACTIVITY_ACTIONS.CREATE, - id: faker.random.uuid(), - content: faker.random.word(), - }, - performer: { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: faker.random.uuid(), - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.CUSTOMER, - id: faker.random.uuid(), - }, - }; +export const activityLogFactory = async (params: IActivityLogFactoryInput = {}) => { + const activity = new ActivityLogs({ + contentType: params.contentType || 'customer', + action: params.action || 'create', + contentId: params.contentId || faker.random.uuid(), + content: params.content || 'content', + createdBy: params.createdBy || faker.random.uuid(), + }); - return ActivityLogs.createDoc({ ...doc, ...params }); + return activity.save(); }; interface IUserFactoryInput { @@ -98,11 +122,14 @@ interface IUserFactoryInput { isOwner?: boolean; isActive?: boolean; groupIds?: string[]; + brandIds?: string[]; + deviceTokens?: string[]; registrationToken?: string; registrationTokenExpires?: Date; + doNotDisturb?: string; } -export const userFactory = (params: IUserFactoryInput = {}) => { +export const userFactory = async (params: IUserFactoryInput = {}) => { const user = new Users({ username: params.username || faker.internet.userName(), details: { @@ -120,11 +147,14 @@ export const userFactory = (params: IUserFactoryInput = {}) => { github: params.github || faker.random.word(), website: params.website || faker.random.word(), }, - email: params.email || faker.internet.email(), + email: await getUniqueValue(Users, 'email', params.email), password: params.password || '$2a$10$qfBFBmWmUjeRcR.nBBfgDO/BEbxgoai5qQhyjsrDUMiZC6dG7sg1q', isOwner: typeof params.isOwner !== 'undefined' ? params.isOwner : true, - isActive: params.isActive || true, + isActive: typeof params.isActive !== 'undefined' ? params.isActive : true, groupIds: params.groupIds || [], + brandIds: params.brandIds, + deviceTokens: params.deviceTokens, + doNotDisturb: params.doNotDisturb, }); return user.save(); @@ -159,11 +189,15 @@ interface IEngageMessageFactoryInput { messenger?: IMessenger; title?: string; email?: IEmail; + smsContent?: string; + fromUserId?: string; + fromIntegrationId?: string; } export const engageMessageFactory = (params: IEngageMessageFactoryInput = {}) => { const engageMessage = new EngageMessages({ kind: params.kind || 'manual', + customerIds: params.customerIds || [], method: params.method || 'messenger', title: params.title || faker.random.word(), fromUserId: params.userId || faker.random.uuid(), @@ -174,6 +208,10 @@ export const engageMessageFactory = (params: IEngageMessageFactoryInput = {}) => isDraft: params.isDraft || false, messenger: params.messenger, email: params.email, + smsContent: { + content: params.smsContent || 'Sms content', + fromIntegrationId: params.fromIntegrationId, + }, }); return engageMessage.save(); @@ -181,13 +219,14 @@ export const engageMessageFactory = (params: IEngageMessageFactoryInput = {}) => interface IBrandFactoryInput { code?: string; + name?: string; description?: string; } -export const brandFactory = (params: IBrandFactoryInput = {}) => { +export const brandFactory = async (params: IBrandFactoryInput = {}) => { const brand = new Brands({ - name: faker.random.word(), - code: params.code || faker.random.word(), + name: params.name || faker.random.word(), + code: await getUniqueValue(Brands, 'code', params.code), userId: Random.id(), description: params.description || faker.random.word(), createdAt: new Date(), @@ -200,13 +239,53 @@ export const brandFactory = (params: IBrandFactoryInput = {}) => { return brand.save(); }; +interface ITemplateInput { + stages?: any[]; +} + +export const pipelineTemplateFactory = (params: ITemplateInput = {}) => { + const pipelineTemplate = new PipelineTemplates({ + name: faker.random.word(), + description: faker.random.word(), + type: BOARD_TYPES.GROWTH_HACK, + stages: params.stages || [ + { name: faker.random.word(), formId: faker.random.word() }, + { name: faker.random.word(), formId: faker.random.word() }, + ], + }); + + return pipelineTemplate.save(); +}; + +interface ILabelInput { + name?: string; + colorCode?: string; + pipelineId?: string; + type?: string; + createdBy?: string; +} + +export const pipelineLabelFactory = (params: ILabelInput = {}) => { + const pipelineLabel = new PipelineLabels({ + name: params.name || faker.random.word(), + colorCode: params.colorCode || faker.random.word(), + pipelineId: params.pipelineId || faker.random.word(), + type: params.type || BOARD_TYPES.DEAL, + createdBy: params.createdBy || faker.random.uuid().toString(), + }); + + return pipelineLabel.save(); +}; + interface IEmailTemplateFactoryInput { content?: string; + customerId?: string; } export const emailTemplateFactory = (params: IEmailTemplateFactoryInput = {}) => { const emailTemplate = new EmailTemplates({ name: faker.random.word(), + customerId: params.customerId || Random.id(), content: params.content || faker.random.word(), }); @@ -214,13 +293,14 @@ export const emailTemplateFactory = (params: IEmailTemplateFactoryInput = {}) => }; interface IResponseTemplateFactoryInput { + name?: string; content?: string; brandId?: string; } export const responseTemplateFactory = (params: IResponseTemplateFactoryInput = {}) => { const responseTemplate = new ResponseTemplates({ - name: faker.random.word(), + name: params.name || faker.random.word(), content: params.content || faker.random.word(), brandId: params.brandId || Random.id(), files: [faker.random.image()], @@ -232,8 +312,7 @@ export const responseTemplateFactory = (params: IResponseTemplateFactoryInput = interface IConditionsInput { field?: string; operator?: string; - value?: string; - dateUnit?: string; + value?: any; type?: string; } @@ -242,7 +321,6 @@ interface ISegmentFactoryInput { description?: string; subOf?: string; color?: string; - connector?: string; conditions?: IConditionsInput[]; } @@ -252,7 +330,6 @@ export const segmentFactory = (params: ISegmentFactoryInput = {}) => { field: 'messengerData.sessionCount', operator: 'e', value: '10', - dateUnit: 'days', type: 'string', }, ]; @@ -263,7 +340,6 @@ export const segmentFactory = (params: ISegmentFactoryInput = {}) => { description: params.description || faker.random.word(), subOf: params.subOf, color: params.color || '#809b87', - connector: params.connector || 'any', conditions: params.conditions || defaultConditions, }); @@ -287,6 +363,44 @@ export const internalNoteFactory = (params: IInternalNoteFactoryInput) => { return internalNote.save(); }; +interface IChecklistFactoryInput { + contentType?: string; + contentTypeId?: string; + title?: string; + createdUserId?: string; +} + +export const checklistFactory = (params: IChecklistFactoryInput) => { + const checklist = new Checklists({ + contentType: params.contentType || ACTIVITY_CONTENT_TYPES.DEAL, + contentTypeId: params.contentTypeId || faker.random.uuid().toString(), + title: params.title || faker.random.uuid().toString(), + createdUserId: params.createdUserId || faker.random.uuid().toString(), + }); + + return checklist.save(); +}; + +interface IChecklistItemFactoryInput { + checklistId?: string; + content?: string; + isChecked?: boolean; + createdUserId?: string; + order?: number; +} + +export const checklistItemFactory = (params: IChecklistItemFactoryInput) => { + const checklistItem = new ChecklistItems({ + checklistId: params.checklistId || faker.random.uuid().toString, + content: params.content || faker.random.uuid().toString, + isChecked: params.isChecked || false, + createdUserId: params.createdUserId || faker.random.uuid().toString(), + order: params.order || 0, + }); + + return checklistItem.save(); +}; + interface ICompanyFactoryInput { primaryName?: string; names?: string[]; @@ -294,38 +408,52 @@ interface ICompanyFactoryInput { industry?: string; website?: string; tagIds?: string[]; + scopeBrandIds?: string[]; plan?: string; - leadStatus?: string; status?: string; - lifecycleState?: string; createdAt?: Date; modifiedAt?: Date; phones?: string[]; emails?: string[]; primaryPhone?: string; primaryEmail?: string; + parentCompanyId?: string; + ownerId?: string; + mergedIds?: string[]; + code?: string; } export const companyFactory = (params: ICompanyFactoryInput = {}) => { - const company = new Companies({ + const companyDoc = { primaryName: params.primaryName || faker.random.word(), - names: params.names || [faker.random.word()], + names: params.names || [], size: params.size || faker.random.number(), industry: params.industry || 'Airlines', website: params.website || faker.internet.domainName(), - tagIds: params.tagIds || [faker.random.number()], + tagIds: params.tagIds || [], plan: params.plan || faker.random.word(), - leadStatus: params.leadStatus || 'open', - status: params.status || STATUSES.ACTIVE, - lifecycleState: params.lifecycleState || 'lead', - createdAt: params.createdAt || new Date(), - modifiedAt: params.modifiedAt || new Date(), + status: params.status || 'Active', phones: params.phones || [], emails: params.emails || [], + scopeBrandIds: params.scopeBrandIds || [], primaryPhone: params.primaryPhone || '', primaryEmail: params.primaryEmail || '', + parentCompanyId: params.parentCompanyId || faker.random.uuid().toString(), + ownerId: params.ownerId || faker.random.uuid().toString(), + mergedIds: params.mergedIds || [], + code: params.code || '', + }; + + const searchText = Companies.fillSearchText({ ...companyDoc }); + + Object.assign(companyDoc, { + createdAt: params.createdAt || new Date(), + modifiedAt: params.modifiedAt || new Date(), + searchText, }); + const company = new Companies(companyDoc); + return company.save(); }; @@ -333,6 +461,8 @@ interface ICustomerFactoryInput { integrationId?: string; firstName?: string; lastName?: string; + sex?: number; + birthDate?: Date; primaryEmail?: string; primaryPhone?: string; emails?: string[]; @@ -340,33 +470,54 @@ interface ICustomerFactoryInput { doNotDisturb?: string; leadStatus?: string; status?: string; - lifecycleState?: string; - messengerData?: any; customFieldsData?: any; - companyIds?: string[]; + trackedData?: any; tagIds?: string[]; ownerId?: string; - hasValidEmail?: boolean; + profileScore?: number; + code?: string; + isOnline?: boolean; + lastSeenAt?: number; + sessionCount?: number; + visitorContactInfo?: any; + deviceTokens?: string[]; + emailValidationStatus?: string; + phoneValidationStatus?: string; + mergedIds?: string[]; + relatedIntegrationIds?: string[]; } -export const customerFactory = (params: ICustomerFactoryInput = {}, useModelMethod = false) => { +export const customerFactory = async (params: ICustomerFactoryInput = {}, useModelMethod = false) => { + const createdAt = faker.date.past(); + const doc = { + createdAt, integrationId: params.integrationId, - firstName: params.firstName || faker.random.word(), - lastName: params.lastName || faker.random.word(), - primaryEmail: params.primaryEmail || faker.internet.email(), - primaryPhone: params.primaryPhone || faker.phone.phoneNumber(), - emails: params.emails || [faker.internet.email()], - phones: params.phones || [faker.phone.phoneNumber()], - leadStatus: params.leadStatus || 'open', - status: params.status || STATUSES.ACTIVE, - lifecycleState: params.lifecycleState || 'lead', - messengerData: params.messengerData || {}, - customFieldsData: params.customFieldsData || {}, - companyIds: params.companyIds || [faker.random.number(), faker.random.number()], + firstName: params.firstName, + lastName: params.lastName, + sex: params.sex, + birthDate: params.birthDate, + primaryEmail: params.primaryEmail, + primaryPhone: params.primaryPhone, + emails: params.emails || [], + phones: params.phones || [], + leadStatus: params.leadStatus || 'new', + status: params.status || 'Active', + lastSeenAt: faker.date.between(createdAt, new Date()), + isOnline: params.isOnline || false, + sessionCount: faker.random.number(), + customFieldsData: params.customFieldsData || [], + trackedData: params.trackedData || [], tagIds: params.tagIds || [Random.id()], ownerId: params.ownerId || Random.id(), - hasValidEmail: params.hasValidEmail || false, + emailValidationStatus: params.emailValidationStatus || 'unknown', + phoneValidationStatus: params.phoneValidationStatus || 'unknown', + profileScore: params.profileScore || 0, + code: await getUniqueValue(Customers, 'code', params.code), + visitorContactInfo: params.visitorContactInfo, + deviceTokens: params.deviceTokens || [], + mergedIds: params.mergedIds || [], + relatedIntegrationIds: params.relatedIntegrationIds || [], }; if (useModelMethod) { @@ -401,7 +552,7 @@ export const fieldFactory = async (params: IFieldFactoryInput) => { throw new Error('Failed to create fieldGroup'); } - const field = new Fields({ + return Fields.create({ contentType: params.contentType || 'form', contentTypeId: params.contentTypeId || faker.random.uuid(), type: params.type || 'input', @@ -412,18 +563,15 @@ export const fieldFactory = async (params: IFieldFactoryInput) => { order: params.order || 0, isVisible: params.visible || true, groupId: params.groupId || (groupObj ? groupObj._id : ''), + isDefinedByErxes: params.isDefinedByErxes, }); - - await field.save(); - await Fields.updateOne({ _id: field._id }, { $set: { ...params } }); - - return Fields.findOne({ _id: field._id }); }; interface IConversationFactoryInput { customerId?: string; assignedUserId?: string; integrationId?: string; + operatorStatus?: string; userId?: string; content?: string; participatedUserIds?: string[]; @@ -436,13 +584,16 @@ interface IConversationFactoryInput { number?: number; firstRespondedUserId?: string; firstRespondedDate?: dateType; + isCustomerRespondedLast?: boolean; } export const conversationFactory = (params: IConversationFactoryInput = {}) => { const doc = { - content: params.content || faker.lorem.sentence(), + content: params.content || faker.random.word(), customerId: params.customerId || Random.id(), integrationId: params.integrationId || Random.id(), + status: params.status || CONVERSATION_STATUSES.NEW, + operatorStatus: params.operatorStatus || CONVERSATION_OPERATOR_STATUS.OPERATOR, }; return Conversations.createConversation({ @@ -461,6 +612,8 @@ interface IConversationMessageFactoryInput { isCustomerRead?: boolean; engageData?: any; formWidgetData?: any; + kind?: string; + contentType?: string; } export const conversationMessageFactory = async (params: IConversationMessageFactoryInput) => { @@ -477,16 +630,17 @@ export const conversationMessageFactory = async (params: IConversationMessageFac } return ConversationMessages.createMessage({ - content: params.content || faker.random.word(), - attachments: {}, + content: params.content, + attachments: [], mentionedUserIds: params.mentionedUserIds || [Random.id()], conversationId, - internal: params.internal || true, + internal: params.internal === undefined || params.internal === null ? true : params.internal, customerId: params.customerId || Random.id(), userId, - isCustomerRead: params.isCustomerRead || true, + isCustomerRead: params.isCustomerRead, engageData: params.engageData || {}, formWidgetData: params.formWidgetData || {}, + contentType: params.contentType || MESSAGE_TYPES.TEXT, }); }; @@ -495,8 +649,11 @@ interface IIntegrationFactoryInput { kind?: string; brandId?: string; formId?: string; - formData?: any | string; + leadData?: any; tagIds?: string[]; + isActive?: boolean; + messengerData?: any; + languageCode?: string; } export const integrationFactory = async (params: IIntegrationFactoryInput = {}) => { @@ -505,41 +662,65 @@ export const integrationFactory = async (params: IIntegrationFactoryInput = {}) const doc = { name: params.name || faker.random.word(), kind, - brandId: params.brandId || Random.id(), - formId: params.formId || Random.id(), - messengerData: { welcomeMessage: 'welcome', notifyCustomer: true }, - formData: params.formData === 'form' ? params.formData : kind === 'form' ? { thankContent: 'thankContent' } : null, - tagIds: params.tagIds || [], + languageCode: params.languageCode, + brandId: params.brandId || faker.random.uuid().toString(), + formId: params.formId, + messengerData: params.messengerData, + leadData: params.leadData ? params.leadData : { thankContent: 'thankContent' }, + tagIds: params.tagIds, + isActive: params.isActive === undefined || params.isActive === null ? true : params.isActive, }; - return Integrations.create(doc); -}; + if (params.messengerData && !params.messengerData.timezone) { + doc.messengerData.timezone = momentTz.tz.guess(true); + } -interface IFormSubmission { - customerId: string; - submittedAt: Date; -} + const user = await userFactory({}); + + return Integrations.createIntegration(doc, user._id); +}; interface IFormFactoryInput { title?: string; code?: string; + type?: string; description?: string; createdUserId?: string; - submissions?: IFormSubmission[]; } export const formFactory = async (params: IFormFactoryInput = {}) => { - const { title, description, code, submissions, createdUserId } = params; + const { title, description, code, type, createdUserId } = params; return Forms.create({ title: title || faker.random.word(), description: description || faker.random.word(), - code: code || Random.id(), - submissions: submissions || [], + code: await getUniqueValue(Forms, 'code', code), + type: type || FORM_TYPES.GROWTH_HACK, createdUserId: createdUserId || (await userFactory({})), }); }; +interface IFormSubmissionFactoryInput { + customerId?: string; + formId?: string; + contentType?: string; + contentTypeId?: string; + formFieldId?: string; + value?: string; +} + +export const formSubmissionFactory = async (params: IFormSubmissionFactoryInput = {}) => { + return FormSubmissions.create({ + submittedAt: new Date(), + customerId: params.customerId || faker.random.word(), + contentType: params.contentType, + contentTypeId: params.contentTypeId, + formId: params.formId || faker.random.word(), + formFieldId: params.formFieldId, + value: params.value, + }); +}; + interface INotificationConfigurationFactoryInput { isAllowed?: boolean; notifType?: string; @@ -567,7 +748,9 @@ interface INotificationFactoryInput { content?: string; link?: string; createdUser?: any; - requireRead?: boolean; + isRead?: boolean; + contentTypeId?: string; + contentType?: string; } export const notificationFactory = async (params: INotificationFactoryInput) => { @@ -584,6 +767,9 @@ export const notificationFactory = async (params: INotificationFactoryInput) => link: params.link || 'new Notification link', receiver: receiver._id || faker.random.word(), createdUser: params.createdUser || faker.random.word(), + isRead: params.isRead || false, + contentTypeId: params.contentTypeId, + contentType: params.contentType, }); }; @@ -599,7 +785,7 @@ export const channelFactory = async (params: IChannelFactoryInput = {}) => { const obj = { name: faker.random.word(), description: faker.lorem.sentence, - integrationIds: params.integrationIds || [], + integrationIds: params.integrationIds, memberIds: params.userId || [user._id], userId: user._id, conversationCount: 0, @@ -613,22 +799,26 @@ export const channelFactory = async (params: IChannelFactoryInput = {}) => { interface IKnowledgeBaseTopicFactoryInput { userId?: string; + color?: string; categoryIds?: string[]; + brandId?: string; } export const knowledgeBaseTopicFactory = async (params: IKnowledgeBaseTopicFactoryInput = {}) => { const doc = { title: faker.random.word(), description: faker.lorem.sentence, - brandId: faker.random.word(), - catgoryIds: [faker.random.word()], + brandId: params.brandId || faker.random.word(), + color: params.color, }; - return KnowledgeBaseTopics.create({ - ...doc, - ...params, - userId: params.userId || faker.random.word(), - }); + return KnowledgeBaseTopics.createDoc( + { + ...doc, + ...params, + }, + params.userId || faker.random.word(), + ); }; interface IKnowledgeBaseCategoryFactoryInput { @@ -641,7 +831,7 @@ export const knowledgeBaseCategoryFactory = async (params: IKnowledgeBaseCategor const doc = { title: faker.random.word(), description: faker.lorem.sentence, - articleIds: params.articleIds || [faker.random.word(), faker.random.word()], + articleIds: params.articleIds, icon: faker.random.word(), }; @@ -651,6 +841,9 @@ export const knowledgeBaseCategoryFactory = async (params: IKnowledgeBaseCategor interface IKnowledgeBaseArticleCategoryInput { categoryIds?: string[]; userId?: string; + reactionChoices?: string[]; + status?: string; + modifiedBy?: string; } export const knowledgeBaseArticleFactory = async (params: IKnowledgeBaseArticleCategoryInput = {}) => { @@ -659,18 +852,22 @@ export const knowledgeBaseArticleFactory = async (params: IKnowledgeBaseArticleC summary: faker.lorem.sentence, content: faker.lorem.sentence, icon: faker.random.word(), + reactionChoices: params.reactionChoices || ['wow'], + status: params.status || 'draft', + modifiedBy: params.modifiedBy, }; return KnowledgeBaseArticles.createDoc({ ...doc, ...params }, params.userId || faker.random.word()); }; interface IBoardFactoryInput { + name?: string; type?: string; } export const boardFactory = (params: IBoardFactoryInput = {}) => { const board = new Boards({ - name: faker.random.word(), + name: params.name || faker.random.word(), userId: Random.id(), type: params.type || BOARD_TYPES.DEAL, }); @@ -682,136 +879,294 @@ interface IPipelineFactoryInput { boardId?: string; type?: string; bgColor?: string; + hackScoringType?: string; + visibility?: string; + memberIds?: string[]; + watchedUserIds?: string[]; + startDate?: Date; + endDate?: Date; + templateId?: string; } -export const pipelineFactory = (params: IPipelineFactoryInput = {}) => { - const pipeline = new Pipelines({ +export const pipelineFactory = async (params: IPipelineFactoryInput = {}) => { + const type = params.type || BOARD_TYPES.DEAL; + let boardId = params.boardId; + + if (!boardId) { + const board = await boardFactory({ type }); + + boardId = board._id; + } + + return Pipelines.create({ name: faker.random.word(), - boardId: params.boardId || faker.random.word(), - type: params.type || BOARD_TYPES.DEAL, - visibility: 'public', + boardId, + type, + visibility: params.visibility || 'public', bgColor: params.bgColor || 'fff', + hackScoringType: params.hackScoringType, + memberIds: params.memberIds, + watchedUserIds: params.watchedUserIds, + startDate: params.startDate, + endDate: params.endDate, + templateId: params.templateId, }); - - return pipeline.save(); }; interface IStageFactoryInput { pipelineId?: string; type?: string; + probability?: string; + formId?: string; + status?: string; + order?: number; } -export const stageFactory = (params: IStageFactoryInput = {}) => { +export const stageFactory = async (params: IStageFactoryInput = {}) => { + const type = params.type || BOARD_TYPES.DEAL; + + const board = await boardFactory({ type }); + const pipeline = await pipelineFactory({ type, boardId: board._id }); + const stage = new Stages({ name: faker.random.word(), - pipelineId: params.pipelineId || faker.random.word(), + pipelineId: params.pipelineId || pipeline._id, type: params.type || BOARD_TYPES.DEAL, + probability: params.probability || PROBABILITY.TEN, + formId: params.formId, + order: params.order, + status: params.status || BOARD_STATUSES.ACTIVE, }); return stage.save(); }; -interface ITicketFactoryInput { +interface IDealFactoryInput { + name?: string; stageId?: string; productsData?: any; closeDate?: Date; - customerIds?: string[]; - companyIds?: string[]; noCloseDate?: boolean; assignedUserIds?: string[]; + watchedUserIds?: string[]; + labelIds?: string[]; + modifiedBy?: string; + order?: number; + probability?: string; + searchText?: string; + userId?: string; + initialStageId?: string; + sourceConversationId?: string; } -export const dealFactory = (params: ITicketFactoryInput = {}) => { +export const dealFactory = async (params: IDealFactoryInput = {}) => { + const board = await boardFactory({ type: BOARD_TYPES.DEAL }); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + const stageId = params.stageId || stage._id; + const deal = new Deals({ ...params, - name: faker.random.word(), - stageId: params.stageId || faker.random.word(), - companyIds: params.companyIds || [faker.random.word()], - customerIds: params.customerIds || [faker.random.word()], + initialStageId: stageId, + name: params.name || faker.random.word(), + stageId, amount: faker.random.objectElement(), ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), description: faker.random.word(), + productsData: params.productsData, assignedUserIds: params.assignedUserIds || [faker.random.word()], + userId: params.userId || faker.random.word(), + watchedUserIds: params.watchedUserIds, + labelIds: params.labelIds || [], + order: params.order, + probability: params.probability, + searchText: params.searchText, + sourceConversationId: params.sourceConversationId, + createdAt: new Date(), }); return deal.save(); }; interface ITaskFactoryInput { + name?: string; stageId?: string; closeDate?: Date; - customerIds?: string[]; - companyIds?: string[]; noCloseDate?: boolean; assignedUserIds?: string[]; + priority?: string; + watchedUserIds?: string[]; + labelIds?: string[]; + sourceConversationId?: string; + initialStageId?: string; } -export const taskFactory = (params: ITaskFactoryInput = {}) => { +const attachmentFactory = () => ({ + name: faker.random.word(), + url: faker.image.imageUrl(), + type: faker.system.mimeType(), + size: faker.random.number(), +}); + +export const taskFactory = async (params: ITaskFactoryInput = {}) => { + const board = await boardFactory({ type: BOARD_TYPES.TASK }); + const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TASK }); + const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TASK }); + const task = new Tasks({ ...params, - name: faker.random.word(), - stageId: params.stageId || faker.random.word(), - companyIds: params.companyIds || [faker.random.word()], - customerIds: params.customerIds || [faker.random.word()], + name: params.name || faker.random.word(), + stageId: params.stageId || stage._id, ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), description: faker.random.word(), - assignedUserIds: params.assignedUserIds || [faker.random.word()], + assignedUserIds: params.assignedUserIds, + priority: params.priority, + watchedUserIds: params.watchedUserIds, + labelIds: params.labelIds || [], + sourceConversationId: params.sourceConversationId, + attachments: [attachmentFactory(), attachmentFactory()], }); return task.save(); }; interface ITicketFactoryInput { + name?: string; + stageId?: string; + closeDate?: Date; + noCloseDate?: boolean; + assignedUserIds?: string[]; + priority?: string; + source?: string; + watchedUserIds?: string[]; + labelIds?: string[]; + sourceConversationId?: string; +} + +export const ticketFactory = async (params: ITicketFactoryInput = {}) => { + const board = await boardFactory({ type: BOARD_TYPES.TICKET }); + const pipeline = await pipelineFactory({ boardId: board._id, type: BOARD_TYPES.TICKET }); + const stage = await stageFactory({ pipelineId: pipeline._id, type: BOARD_TYPES.TICKET }); + + const ticket = new Tickets({ + ...params, + name: params.name || faker.random.word(), + stageId: params.stageId || stage._id, + ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), + description: faker.random.word(), + assignedUserIds: params.assignedUserIds, + priority: params.priority, + source: params.source, + watchedUserIds: params.watchedUserIds, + labelIds: params.labelIds || [], + sourceConversationId: params.sourceConversationId, + }); + + return ticket.save(); +}; + +interface IGrowthHackFactoryInput { + name?: string; stageId?: string; closeDate?: Date; customerIds?: string[]; companyIds?: string[]; noCloseDate?: boolean; assignedUserIds?: string[]; + watchedUserIds?: string[]; + hackStages?: string[]; + priority?: string; + ease?: number; + impact?: number; + votedUserIds?: string[]; + labelIds?: string[]; + initialStageId?: string; + order?: number; } -export const ticketFactory = (params: ITicketFactoryInput = {}) => { - const ticket = new Tickets({ +export const growthHackFactory = async (params: IGrowthHackFactoryInput = {}) => { + const board = await boardFactory({ type: BOARD_TYPES.GROWTH_HACK }); + const pipeline = await pipelineFactory({ boardId: board._id }); + const stage = await stageFactory({ pipelineId: pipeline._id }); + + const growthHack = new GrowthHacks({ ...params, - name: faker.random.word(), - stageId: params.stageId || faker.random.word(), + name: params.name || faker.random.word(), + stageId: params.stageId || stage._id, companyIds: params.companyIds || [faker.random.word()], customerIds: params.customerIds || [faker.random.word()], ...(!params.noCloseDate ? { closeDate: params.closeDate || new Date() } : {}), description: faker.random.word(), assignedUserIds: params.assignedUserIds || [faker.random.word()], + hackStages: params.hackStages || [faker.random.word()], + votedUserIds: params.votedUserIds || [faker.random.uuid().toString()], + watchedUserIds: params.watchedUserIds, + ease: params.ease || 0, + impact: params.impact || 0, + priority: params.priority, + labelIds: params.labelIds || [], + order: params.order || Math.random(), }); - return ticket.save(); + return growthHack.save(); }; interface IProductFactoryInput { name?: string; type?: string; description?: string; + tagIds?: string[]; + categoryId?: string; + customFieldsData?: ICustomField[]; } -export const productFactory = (params: IProductFactoryInput = {}) => { +export const productFactory = async (params: IProductFactoryInput = {}) => { const product = new Products({ name: params.name || faker.random.word(), + categoryId: params.categoryId || faker.random.word(), type: params.type || PRODUCT_TYPES.PRODUCT, + customFieldsData: params.customFieldsData, description: params.description || faker.random.word(), sku: faker.random.word(), + code: await getUniqueValue(Products, 'code'), createdAt: new Date(), + tagIds: params.tagIds || [], }); return product.save(); }; +interface IProductCategoryFactoryInput { + name?: string; + description?: string; + parentId?: string; + code?: string; + order?: string; +} + +export const productCategoryFactory = async (params: IProductCategoryFactoryInput = {}) => { + const productCategory = new ProductCategories({ + name: params.name || faker.random.word(), + description: params.description || faker.random.word(), + parentId: params.parentId, + code: await getUniqueValue(ProductCategories, 'code', params.code), + order: params.order || faker.random.word(), + createdAt: new Date(), + }); + + return productCategory.save(); +}; + interface IConfigFactoryInput { code?: string; value?: string[]; } -export const configFactory = (params: IConfigFactoryInput = {}) => { +export const configFactory = async (params: IConfigFactoryInput = {}) => { const config = new Configs({ ...params, - code: faker.random.word(), + code: await getUniqueValue(Configs, 'code', params.code), value: [faker.random.word()], }); @@ -830,7 +1185,6 @@ export const fieldGroupFactory = async (params: IFieldGroupFactoryInput) => { contentType: params.contentType || FIELDS_GROUPS_CONTENT_TYPES.CUSTOMER, description: faker.random.word(), isDefinedByErxes: params.isDefinedByErxes || false, - order: 1, isVisible: true, }; @@ -846,6 +1200,7 @@ interface IImportHistoryFactoryInput { failed?: number; total?: number; success?: string; + errorMsgs?: string[]; ids?: string[]; } @@ -856,8 +1211,9 @@ export const importHistoryFactory = async (params: IImportHistoryFactoryInput) = failed: params.failed || faker.random.number(), total: params.total || faker.random.number(), success: params.success || faker.random.number(), - ids: params.ids || [], + ids: params.ids, contentType: params.contentType || 'customer', + errorMsgs: params.errorMsgs, }; return ImportHistory.create({ ...doc, ...params, userId: user._id }); @@ -866,17 +1222,37 @@ export const importHistoryFactory = async (params: IImportHistoryFactoryInput) = interface IMessengerApp { name?: string; kind?: string; - credentials: IMessengerAppCrendentials; + credentials?: IMessengerAppCrendentials; } export function messengerAppFactory(params: IMessengerApp) { return MessengerApps.create({ name: params.name || faker.random.word(), - kind: params.kind || 'knowledgebase', + kind: params.kind, credentials: params.credentials, }); } +interface IScript { + name?: string; + messengerId?: string; + messengerBrandCode?: string; + leadIds?: string[]; + leadMaps?: Array<{ formCode: string; brandCode: string }>; + kbTopicId?: string; +} + +export function scriptFactory(params: IScript) { + return Scripts.create({ + name: params.name || faker.random.word(), + messengerId: params.messengerId, + messengerBrandCode: params.messengerBrandCode, + leadIds: params.leadIds, + leadMaps: params.leadMaps, + kbTopicId: params.kbTopicId, + }); +} + interface IPermissionParams { module?: string; action?: string; @@ -888,22 +1264,124 @@ interface IPermissionParams { export const permissionFactory = async (params: IPermissionParams = {}) => { const permission = new Permissions({ - module: faker.random.word(), + module: params.module || faker.random.word(), action: params.action || faker.random.word(), - allowed: params.allowed || false, - userId: params.userId || Random.id(), + allowed: typeof params.allowed === 'undefined' ? true : params.allowed, + userId: params.userId, requiredActions: params.requiredActions || [], - groupId: params.groupId || faker.random.word(), + groupId: params.groupId, }); return permission.save(); }; -export const usersGroupFactory = () => { +interface IUserGroupParams { + isVisible?: boolean; +} + +export const usersGroupFactory = async (params: IUserGroupParams = {}) => { const usersGroup = new UsersGroups({ - name: faker.random.word(), + name: await getUniqueValue(UsersGroups, 'name'), description: faker.random.word(), + isVisible: params.isVisible === undefined || params.isVisible === null ? true : params.isVisible, }); return usersGroup.save(); }; + +interface IConformityFactoryInput { + mainType: string; + mainTypeId: string; + relType: string; + relTypeId: string; +} + +export const conformityFactory = (params: IConformityFactoryInput) => { + return Conformities.addConformity(params); +}; + +interface IEmailDeliveryFactoryInput { + attachments?: string[]; + subject?: string; + status?: string; + body?: string; + to?: string[]; + cc?: string[]; + bcc?: string[]; + from?: string; + kind?: string; + userId?: string; + customerId?: string; +} + +export const emailDeliveryFactory = async (params: IEmailDeliveryFactoryInput = {}) => { + const emailDelviry = new EmailDeliveries({ + attachments: params.attachments || [], + subject: params.subject || 'subject', + status: params.status || 'pending', + body: params.body || 'body', + to: params.to || ['to'], + cc: params.cc || ['cc'], + bcc: params.bcc || ['bcc'], + from: params.from || 'from', + kind: params.kind || 'kind', + userId: params.userId || faker.random.uuid(), + customerId: params.customerId || faker.random.uuid(), + }); + + return emailDelviry.save(); +}; + +interface IMessageEngageDataParams { + messageId?: string; + brandId?: string; + content?: string; + fromUserId?: string; + kind?: string; + sentAs?: string; +} + +export function engageDataFactory(params: IMessageEngageDataParams) { + return { + messageId: params.messageId || Random.id(), + brandId: params.brandId || Random.id(), + content: params.content || faker.lorem.sentence(), + fromUserId: params.fromUserId || Random.id(), + kind: params.kind || 'popup', + sentAs: params.sentAs || 'post', + }; +} + +interface IWebhookActionInput { + label?: string; + type?: string; + action?: any; +} + +interface IWebhookParams { + url?: string; + actions?: IWebhookActionInput[]; + token?: string; +} + +export function webhookFactory(params: IWebhookParams) { + const webhook = new Webhooks({ + url: params.url || `https://${faker.random.word()}.com`, + actions: params.actions || WEBHOOK_ACTIONS, + token: params.token || faker.unique, + }); + + return webhook.save(); +} + +interface IOnboardHistoryParams { + userId: string; + isCompleted?: boolean; + completedSteps?: string[]; +} + +export const onboardHistoryFactory = async (params: IOnboardHistoryParams) => { + const onboard = new OnboardingHistories(params); + + return onboard.save(); +}; diff --git a/src/db/models/ActivityLogs.ts b/src/db/models/ActivityLogs.ts index 208276547..dde516350 100644 --- a/src/db/models/ActivityLogs.ts +++ b/src/db/models/ActivityLogs.ts @@ -1,361 +1,258 @@ import { Model, model } from 'mongoose'; -import { Customers } from '.'; -import { graphqlPubsub } from '../../pubsub'; -import { - activityLogSchema, - IActionPerformer, - IActivity, - IActivityLogDocument, - IContentType, -} from './definitions/activityLogs'; -import { ICompanyDocument } from './definitions/companies'; -import { - ACTIVITY_ACTIONS, - ACTIVITY_CONTENT_TYPES, - ACTIVITY_PERFORMER_TYPES, - ACTIVITY_TYPES, -} from './definitions/constants'; -import { IConversationDocument } from './definitions/conversations'; -import { ICustomerDocument } from './definitions/customers'; -import { IDealDocument } from './definitions/deals'; -import { IEmailDeliveriesDocument } from './definitions/emailDeliveries'; -import { IInternalNoteDocument } from './definitions/internalNotes'; +import { activityLogSchema, IActivityLogDocument, IActivityLogInput } from './definitions/activityLogs'; + +import { IItemCommonFieldsDocument } from './definitions/boards'; +import { ACTIVITY_ACTIONS } from './definitions/constants'; import { ISegmentDocument } from './definitions/segments'; -import { ITaskDocument } from './definitions/tasks'; -import { ITicketDocument } from './definitions/tickets'; - -interface ICreateDocInput { - performer?: IActionPerformer; - performedBy?: IActionPerformer; - activity: IActivity; - contentType: IContentType; - // TODO: remove - coc?: IContentType; -} export interface IActivityLogModel extends Model { - createDoc(doc: ICreateDocInput): Promise; + addActivityLog(doc: IActivityLogInput): Promise; + removeActivityLog(contentId: string): void; + + createSegmentLog(segment: ISegmentDocument, customer: string[], type: string, maxBulk?: number); createLogFromWidget(type: string, payload): Promise; - createConversationLog(conversation: IConversationDocument): Promise; - createCustomerLog(customer: ICustomerDocument): Promise; - createCompanyLog(company: ICompanyDocument): Promise; - createEmailDeliveryLog(email: IEmailDeliveriesDocument): Promise; - createInternalNoteLog(internalNote: IInternalNoteDocument): Promise; - createDealLog(deal: IDealDocument): Promise; - createSegmentLog(segment: ISegmentDocument, customer?: ICustomerDocument): Promise; - createTicketLog(ticket: ITicketDocument): Promise; - createTaskLog(task: ITaskDocument): Promise; + createCocLog({ coc, contentType }: { coc: any; contentType: string }): Promise; + createBoardItemLog({ + item, + contentType, + }: { + item: IItemCommonFieldsDocument; + contentType: string; + }): Promise; + createBoardItemMovementLog( + item: IItemCommonFieldsDocument, + type: string, + userId: string, + content: object, + ): Promise; + createAssigneLog({ + contentId, + userId, + contentType, + content, + }: { + contentId: string; + userId: string; + contentType: string; + content: object; + }): Promise; + createChecklistLog({ + item, + contentType, + action, + }: { + item: any; + contentType: string; + action: string; + }): Promise; + + createArchiveLog({ + item, + contentType, + action, + userId, + }: { + item: any; + contentType: string; + action: string; + userId: string; + }): Promise; } export const loadClass = () => { - const cocFindOne = (conversationId: string, cocId: string, cocType: string) => { - return ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.CONVERSATION, - 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': conversationId, - 'contentType.type': cocType, - 'performedBy.type': ACTIVITY_PERFORMER_TYPES.CUSTOMER, - 'contentType.id': cocId, - }); - }; - - const cocCreate = (conversationId: string, content: string, cocId: string, cocType: string) => { - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.CONVERSATION, - action: ACTIVITY_ACTIONS.CREATE, - content, - id: conversationId, - }, - performer: { - type: ACTIVITY_PERFORMER_TYPES.CUSTOMER, - id: cocId, - }, - contentType: { - type: cocType, - id: cocId, - }, - }); - }; - class ActivityLog { - /** - * Create an ActivityLog document - */ - public static async createDoc(doc: ICreateDocInput) { - const { performer } = doc; - - let performedBy = { - type: ACTIVITY_PERFORMER_TYPES.SYSTEM, - }; - - if (performer) { - performedBy = performer; - } - - const log = await ActivityLogs.create({ performedBy, ...doc }); - - graphqlPubsub.publish('activityLogsChanged', { activityLogsChanged: true }); - - return log; + public static addActivityLog(doc: IActivityLogInput) { + return ActivityLogs.create(doc); } - public static createLogFromWidget(type: string, payload) { - switch (type) { - case 'create-customer': - ActivityLogs.createCustomerLog(payload); - break; - case 'create-company': - ActivityLogs.createCompanyLog(payload); - break; - case 'create-conversation': - ActivityLogs.createConversationLog(payload); - break; - } + public static async removeActivityLog(contentId: IActivityLogInput) { + await ActivityLogs.deleteMany({ contentId }); } - /** - * Create a conversation log for a given customer, - * if the customer is related to companies, - * then create conversation log with all related companies - */ - public static async createConversationLog(conversation: IConversationDocument) { - const customer = await Customers.findOne({ _id: conversation.customerId }); - - if (!customer || !customer._id) { - return; - } - - if (customer.companyIds && customer.companyIds.length > 0) { - for (const companyId of customer.companyIds) { - // check against duplication - const log = await cocFindOne(conversation._id, companyId, ACTIVITY_CONTENT_TYPES.COMPANY); - - if (!log) { - await cocCreate(conversation._id, conversation.content || '', companyId, ACTIVITY_CONTENT_TYPES.COMPANY); - } - } - } - - // check against duplication ====== - const foundLog = await cocFindOne(conversation._id, customer._id, ACTIVITY_CONTENT_TYPES.CUSTOMER); - - if (!foundLog) { - return cocCreate(conversation._id, conversation.content || '', customer._id, ACTIVITY_CONTENT_TYPES.CUSTOMER); - } + public static async createAssigneLog({ + contentId, + userId, + contentType, + content, + }: { + contentId: string; + userId: string; + contentType: string; + content: object; + }) { + return ActivityLogs.addActivityLog({ + contentType, + contentId, + action: 'assignee', + content, + createdBy: userId || '', + }); } - public static createCustomerLog(customer: ICustomerDocument) { - let performer; - - if (customer.ownerId) { - performer = { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: customer.ownerId, - }; - } - + public static createBoardItemLog({ item, contentType }: { item: IItemCommonFieldsDocument; contentType: string }) { let action = ACTIVITY_ACTIONS.CREATE; - let content = `${customer.firstName || ''} ${customer.lastName || ''}`; + let content = ''; - if (customer.mergedIds && customer.mergedIds.length > 0) { - action = ACTIVITY_ACTIONS.MERGE; - content = customer.mergedIds.toString(); + if (item.sourceConversationId) { + action = ACTIVITY_ACTIONS.CONVERT; + content = item.sourceConversationId; } - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.CUSTOMER, - action, - content, - id: customer._id, - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.CUSTOMER, - id: customer._id, - }, - performer, + return ActivityLogs.addActivityLog({ + contentType, + contentId: item._id, + action, + createdBy: item.userId || '', + content, }); } - public static createCompanyLog(company: ICompanyDocument) { - let performer; + public static createBoardItemMovementLog( + item: IItemCommonFieldsDocument, + contentType: string, + userId: string, + content: object, + ) { + return ActivityLogs.addActivityLog({ + contentType, + contentId: item._id, + action: ACTIVITY_ACTIONS.MOVED, + createdBy: userId, + content, + }); + } - if (company.ownerId) { - performer = { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: company.ownerId, - }; + public static async createLogFromWidget(type: string, payload) { + switch (type) { + case 'create-customer': + return ActivityLogs.createCocLog({ coc: payload, contentType: 'customer' }); + case 'create-company': + return ActivityLogs.createCocLog({ coc: payload, contentType: 'company' }); } + } + public static createCocLog({ coc, contentType }: { coc: any; contentType: string }) { let action = ACTIVITY_ACTIONS.CREATE; - let content = company.primaryName || ''; + let content = ''; - if (company.mergedIds && company.mergedIds.length > 0) { + if (coc.mergedIds && coc.mergedIds.length > 0) { action = ACTIVITY_ACTIONS.MERGE; - content = company.mergedIds.toString(); + content = coc.mergedIds; } - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.COMPANY, - action, - content, - id: company._id, - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.COMPANY, - id: company._id, - }, - performer, - }); - } - - public static createEmailDeliveryLog(email: IEmailDeliveriesDocument) { - return ActivityLogs.createDoc({ - activity: { - id: Math.random().toString(), - type: ACTIVITY_TYPES.EMAIL, - action: ACTIVITY_ACTIONS.SEND, - content: email.body, - }, - contentType: { - type: email.cocType, - id: email.cocId || '', - }, - performer: { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: email.userId, - }, + return ActivityLogs.addActivityLog({ + contentType, + content, + contentId: coc._id, + action, + createdBy: coc.ownerId || coc.integrationId, }); } - public static createInternalNoteLog(internalNote: IInternalNoteDocument) { - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.INTERNAL_NOTE, - action: ACTIVITY_ACTIONS.CREATE, - content: internalNote.content, - id: internalNote._id, - }, - contentType: { - type: internalNote.contentType, - id: internalNote.contentTypeId, - }, - performer: { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: internalNote.createdUserId, + /** + * Create a customer or company segment logs + */ + public static async createSegmentLog( + segment: ISegmentDocument, + contentIds: string[], + type: string, + maxBulk: number = 10000, + ) { + const foundSegments = await ActivityLogs.find( + { + contentType: type, + action: 'segment', + contentId: { $in: contentIds }, + 'content.id': segment._id, }, - }); - } - - public static createDealLog(deal: IDealDocument) { - let performer; - - if (deal.userId) { - performer = { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: deal.userId, - }; + { contentId: 1 }, + ); + + const foundContentIds = foundSegments.map(s => s.contentId); + + const diffContentIds = contentIds.filter(x => !foundContentIds.includes(x)); + + let bulkOpt: Array<{ + contentType: string; + contentId: string; + action: string; + content: {}; + }> = []; + + let bulkCounter = 0; + + for (const contentId of diffContentIds) { + bulkCounter = bulkCounter + 1; + + bulkOpt.push({ + contentType: type, + contentId, + action: 'segment', + content: { + id: segment._id, + content: segment.name, + }, + }); + + if (bulkCounter === maxBulk) { + await ActivityLogs.insertMany(bulkOpt); + bulkOpt = []; + bulkCounter = 0; + } } - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.DEAL, - action: ACTIVITY_ACTIONS.CREATE, - content: deal.name || '', - id: deal._id, - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.DEAL, - id: deal._id, - }, - performer, - }); - } - - public static createTicketLog(ticket: ITicketDocument) { - let performer; - - if (ticket.userId) { - performer = { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: ticket.userId, - }; + if (bulkOpt.length === 0) { + return; } - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.TICKET, - action: ACTIVITY_ACTIONS.CREATE, - content: ticket.name || '', - id: ticket._id, - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.TICKET, - id: ticket._id, - }, - performer, - }); + return ActivityLogs.insertMany(bulkOpt); } - public static createTaskLog(task: ITaskDocument) { - let performer; - - if (task.userId) { - performer = { - type: ACTIVITY_PERFORMER_TYPES.USER, - id: task.userId, - }; - } - - return ActivityLogs.createDoc({ - activity: { - type: ACTIVITY_TYPES.TASK, - action: ACTIVITY_ACTIONS.CREATE, - content: task.name || '', - id: task._id, - }, - contentType: { - type: ACTIVITY_CONTENT_TYPES.TASK, - id: task._id, - }, - performer, + public static async createArchiveLog({ + item, + contentType, + action, + userId, + }: { + item: any; + contentType: string; + action: string; + userId: string; + }) { + return ActivityLogs.addActivityLog({ + contentType, + contentId: item._id, + action: 'archive', + content: action, + createdBy: userId, }); } - /** - * Create a customer or company segment log - */ - public static async createSegmentLog(segment: ISegmentDocument, customer?: ICustomerDocument) { - if (!customer) { - throw new Error('customer must be supplied'); - } - - const foundSegment = await ActivityLogs.findOne({ - 'activity.type': ACTIVITY_TYPES.SEGMENT, - 'activity.action': ACTIVITY_ACTIONS.CREATE, - 'activity.id': segment._id, - 'contentType.type': segment.contentType, - 'contentType.id': customer._id, - }); - - if (foundSegment) { - // since this type of activity log already exists, new one won't be created - return foundSegment; + public static async createChecklistLog({ + item, + contentType, + action, + }: { + item: any; + contentType: string; + action: string; + }) { + if (action === 'delete') { + await ActivityLogs.updateMany( + { 'content._id': item._id }, + { $set: { 'content.name': item.title || item.content } }, + ); } - return this.createDoc({ - activity: { - type: ACTIVITY_TYPES.SEGMENT, - action: ACTIVITY_ACTIONS.CREATE, - content: segment.name, - id: segment._id, - }, - contentType: { - type: segment.contentType, - id: customer._id, + return ActivityLogs.addActivityLog({ + contentType, + contentId: item.contentTypeId || item.checklistId, + action, + content: { + _id: item._id, + name: item.title || item.content, }, + createdBy: item.createdUserId || '', }); } } diff --git a/src/db/models/Boards.ts b/src/db/models/Boards.ts index 630ae5235..cc5db7568 100644 --- a/src/db/models/Boards.ts +++ b/src/db/models/Boards.ts @@ -1,6 +1,6 @@ import { Model, model } from 'mongoose'; -import { Deals, Tasks, Tickets } from './'; -import { updateOrder, watchItem } from './boardUtils'; +import { Forms } from './'; +import { getCollection, updateOrder, watchItem } from './boardUtils'; import { boardSchema, IBoard, @@ -12,7 +12,7 @@ import { pipelineSchema, stageSchema, } from './definitions/boards'; -import { BOARD_TYPES } from './definitions/constants'; +import { getDuplicatedStages } from './PipelineTemplates'; export interface IOrderInput { _id: string; @@ -22,7 +22,35 @@ export interface IOrderInput { // Not mongoose document, just stage shaped plain object type IPipelineStage = IStage & { _id: string }; -const createOrUpdatePipelineStages = async (stages: IPipelineStage[], pipelineId: string) => { +const removeStageWithItems = async (type: string, pipelineId: string, prevItemIds: string[] = []) => { + const selector = { pipelineId, _id: { $nin: prevItemIds } }; + + const stageIds = await Stages.find(selector).distinct('_id'); + + const collection = getCollection(type); + + await collection.deleteMany({ stageId: { $in: stageIds } }); + + return Stages.deleteMany(selector); +}; + +const removePipelineStagesWithItems = async (type: string, pipelineId: string) => { + const stageIds = await Stages.find({ pipelineId }).distinct('_id'); + + const collection = getCollection(type); + + await collection.deleteMany({ stageId: { $in: stageIds } }); + + return Stages.deleteMany({ pipelineId }); +}; + +const removeStageItems = async (type: string, stageId: string) => { + const collection = getCollection(type); + + return collection.deleteMany({ stageId }); +}; + +const createOrUpdatePipelineStages = async (stages: IPipelineStage[], pipelineId: string, type: string) => { let order = 0; const validStageIds: string[] = []; @@ -37,6 +65,8 @@ const createOrUpdatePipelineStages = async (stages: IPipelineStage[], pipelineId const prevEntries = await Stages.find({ _id: { $in: prevItemIds } }); const prevEntriesIds = prevEntries.map(entry => entry._id); + await removeStageWithItems(type, pipelineId, prevItemIds); + for (const stage of stages) { order++; @@ -67,6 +97,7 @@ const createOrUpdatePipelineStages = async (stages: IPipelineStage[], pipelineId validStageIds.push(createdStage._id); } } + if (bulkOpsPrevEntry.length > 0) { await Stages.bulkWrite(bulkOpsPrevEntry); } @@ -75,13 +106,27 @@ const createOrUpdatePipelineStages = async (stages: IPipelineStage[], pipelineId }; export interface IBoardModel extends Model { + getBoard(_id: string): Promise; createBoard(doc: IBoard): Promise; updateBoard(_id: string, doc: IBoard): Promise; - removeBoard(_id: string): void; + removeBoard(_id: string): object; } export const loadBoardClass = () => { class Board { + /* + * Get a Board + */ + public static async getBoard(_id: string) { + const board = await Boards.findOne({ _id }); + + if (!board) { + throw new Error('Board not found'); + } + + return board; + } + /** * Create a board */ @@ -108,10 +153,14 @@ export const loadBoardClass = () => { throw new Error('Board not found'); } - const count = await Pipelines.find({ boardId: _id }).countDocuments(); + const pipelines = await Pipelines.find({ boardId: _id }); + + for (const pipeline of pipelines) { + await removePipelineStagesWithItems(pipeline.type, pipeline._id); + } - if (count > 0) { - throw new Error("Can't remove a board"); + for (const pipeline of pipelines) { + await Pipelines.removePipeline(pipeline._id, true); } return Boards.deleteOne({ _id }); @@ -125,11 +174,11 @@ export const loadBoardClass = () => { export interface IPipelineModel extends Model { getPipeline(_id: string): Promise; - createPipeline(doc: IPipeline, stages: IPipelineStage[]): Promise; - updatePipeline(_id: string, doc: IPipeline, stages: IPipelineStage[]): Promise; + createPipeline(doc: IPipeline, stages?: IPipelineStage[]): Promise; + updatePipeline(_id: string, doc: IPipeline, stages?: IPipelineStage[]): Promise; updateOrder(orders: IOrderInput[]): Promise; watchPipeline(_id: string, isAdd: boolean, userId: string): void; - removePipeline(_id: string): void; + removePipeline(_id: string, checked?: boolean): object; } export const loadPipelineClass = () => { @@ -150,22 +199,42 @@ export const loadPipelineClass = () => { /** * Create a pipeline */ - public static async createPipeline(doc: IPipeline, stages: IPipelineStage[]) { + public static async createPipeline(doc: IPipeline, stages?: IPipelineStage[]) { const pipeline = await Pipelines.create(doc); - if (stages) { - await createOrUpdatePipelineStages(stages, pipeline._id); + if (doc.templateId) { + const duplicatedStages = await getDuplicatedStages({ + templateId: doc.templateId, + pipelineId: pipeline._id, + type: doc.type, + }); + + await createOrUpdatePipelineStages(duplicatedStages, pipeline._id, pipeline.type); + } else if (stages) { + await createOrUpdatePipelineStages(stages, pipeline._id, pipeline.type); } return pipeline; } /** - * Update Pipeline + * Update a pipeline */ - public static async updatePipeline(_id: string, doc: IPipeline, stages: IPipelineStage[]) { - if (stages) { - await createOrUpdatePipelineStages(stages, _id); + public static async updatePipeline(_id: string, doc: IPipeline, stages?: IPipelineStage[]) { + if (doc.templateId) { + const pipeline = await Pipelines.getPipeline(_id); + + if (doc.templateId !== pipeline.templateId) { + const duplicatedStages = await getDuplicatedStages({ + templateId: doc.templateId, + pipelineId: _id, + type: doc.type, + }); + + await createOrUpdatePipelineStages(duplicatedStages, _id, doc.type); + } + } else if (stages) { + await createOrUpdatePipelineStages(stages, _id, doc.type); } await Pipelines.updateOne({ _id }, { $set: doc }); @@ -181,25 +250,25 @@ export const loadPipelineClass = () => { } /** - * Remove Pipeline + * Remove a pipeline */ - public static async removePipeline(_id: string) { - const pipeline = await Pipelines.findOne({ _id }); + public static async removePipeline(_id: string, checked?: boolean) { + const pipeline = await Pipelines.getPipeline(_id); - if (!pipeline) { - throw new Error('Pipeline not found'); + if (!checked) { + await removePipelineStagesWithItems(pipeline.type, pipeline._id); } - const count = await Stages.find({ pipelineId: _id }).countDocuments(); + const stages = await Stages.find({ pipelineId: pipeline._id }); - if (count > 0) { - throw new Error("Can't remove a pipeline"); + for (const stage of stages) { + await Stages.removeStage(stage._id); } return Pipelines.deleteOne({ _id }); } - public static async watchPipeline(_id: string, isAdd: boolean, userId: string) { + public static watchPipeline(_id: string, isAdd: boolean, userId: string) { return watchItem(Pipelines, _id, isAdd, userId); } } @@ -212,10 +281,9 @@ export const loadPipelineClass = () => { export interface IStageModel extends Model { getStage(_id: string): Promise; createStage(doc: IStage): Promise; + removeStage(_id: string): object; updateStage(_id: string, doc: IStage): Promise; - changeStage(_id: string, pipelineId: string): Promise; updateOrder(orders: IOrderInput[]): Promise; - removeStage(_id: string): void; } export const loadStageClass = () => { @@ -249,33 +317,6 @@ export const loadStageClass = () => { return Stages.findOne({ _id }); } - /** - * Change Stage - */ - public static async changeStage(_id: string, pipelineId: string) { - const stage = await Stages.updateOne({ _id }, { $set: { pipelineId } }); - - switch (stage.type) { - case BOARD_TYPES.DEAL: { - await Deals.updateMany({ stageId: _id }, { $set: { pipelineId } }); - - break; - } - case BOARD_TYPES.TICKET: { - await Tickets.updateMany({ stageId: _id }, { $set: { pipelineId } }); - - break; - } - case BOARD_TYPES.TASK: { - await Tasks.updateMany({ stageId: _id }, { $set: { pipelineId } }); - - break; - } - } - - return Stages.findOne({ _id }); - } - /* * Update given stages orders */ @@ -283,38 +324,14 @@ export const loadStageClass = () => { return updateOrder(Stages, orders); } - /** - * Remove Stage - */ public static async removeStage(_id: string) { - const stage = await Stages.findOne({ _id }); - - if (!stage) { - throw new Error('Stage not found'); - } - - let count; + const stage = await Stages.getStage(_id); + const pipeline = await Pipelines.getPipeline(stage.pipelineId); - switch (stage.type) { - case BOARD_TYPES.DEAL: { - count = await Deals.find({ stageId: _id }).countDocuments(); - - break; - } - case BOARD_TYPES.TICKET: { - count = await Tickets.find({ stageId: _id }).countDocuments(); - - break; - } - case BOARD_TYPES.TASK: { - count = await Tasks.find({ stageId: _id }).countDocuments(); - - break; - } - } + await removeStageItems(pipeline.type, _id); - if (count > 0) { - throw new Error("Can't remove a stage"); + if (stage.formId) { + await Forms.removeForm(stage.formId); } return Stages.deleteOne({ _id }); diff --git a/src/db/models/Brands.ts b/src/db/models/Brands.ts index 8ad44e8c8..30e7099fb 100644 --- a/src/db/models/Brands.ts +++ b/src/db/models/Brands.ts @@ -1,15 +1,15 @@ import * as Random from 'meteor-random'; import { Model, model } from 'mongoose'; -import { debugBase } from '../../debuggers'; import { Integrations } from './'; import { brandSchema, IBrand, IBrandDocument, IBrandEmailConfig } from './definitions/brands'; import { IIntegrationDocument } from './definitions/integrations'; export interface IBrandModel extends Model { + getBrand(_id: string): IBrandDocument; generateCode(code: string): string; createBrand(doc: IBrand): IBrandDocument; updateBrand(_id: string, fields: IBrand): IBrandDocument; - removeBrand(_id: string): void; + removeBrand(_id: string): IBrandDocument; updateEmailConfig(_id: string, emailConfig: IBrandEmailConfig): IBrandDocument; @@ -18,6 +18,19 @@ export interface IBrandModel extends Model { export const loadClass = () => { class Brand { + /* + * Get a Brand + */ + public static async getBrand(_id: string) { + const brand = await Brands.findOne({ _id }); + + if (!brand) { + throw new Error('Brand not found'); + } + + return brand; + } + public static async generateCode(code?: string) { let generatedCode = code || Random.id().substr(0, 6); @@ -27,10 +40,6 @@ export const loadClass = () => { while (prevBrand) { generatedCode = Random.id().substr(0, 6); - if (code) { - debugBase('User defined brand code already exists. New code is generated.'); - } - prevBrand = await Brands.findOne({ code: generatedCode }); } @@ -72,7 +81,7 @@ export const loadClass = () => { public static async manageIntegrations({ _id, integrationIds }: { _id: string; integrationIds: string[] }) { await Integrations.updateMany({ _id: { $in: integrationIds } }, { $set: { brandId: _id } }, { multi: true }); - return Integrations.find({ _id: { $in: integrationIds } }); + return Integrations.findIntegrations({ _id: { $in: integrationIds } }); } } diff --git a/src/db/models/Channels.ts b/src/db/models/Channels.ts index cdcdca9c7..cc3707256 100644 --- a/src/db/models/Channels.ts +++ b/src/db/models/Channels.ts @@ -2,6 +2,7 @@ import { Model, model } from 'mongoose'; import { channelSchema, IChannel, IChannelDocument } from './definitions/channels'; export interface IChannelModel extends Model { + getChannel(_id: string): Promise; createChannel(doc: IChannel, userId?: string): IChannelDocument; updateChannel(_id: string, doc: IChannel): IChannelDocument; updateUserChannels(channelIds: string[], userId: string): IChannelDocument[]; @@ -10,6 +11,19 @@ export interface IChannelModel extends Model { export const loadClass = () => { class Channel { + /* + * Get a Channel + */ + public static async getChannel(_id: string) { + const channel = await Channels.findOne({ _id }); + + if (!channel) { + throw new Error('Channel not found'); + } + + return channel; + } + public static createChannel(doc: IChannel, userId?: string) { if (!userId) { throw new Error('userId must be supplied'); diff --git a/src/db/models/Checklists.ts b/src/db/models/Checklists.ts new file mode 100644 index 000000000..15c583eb0 --- /dev/null +++ b/src/db/models/Checklists.ts @@ -0,0 +1,213 @@ +import { Model, model } from 'mongoose'; + +import ActivityLogs from './ActivityLogs'; +import { + checklistItemSchema, + checklistSchema, + IChecklist, + IChecklistDocument, + IChecklistItem, + IChecklistItemDocument, +} from './definitions/checklists'; +import { IUserDocument } from './definitions/users'; + +export interface IChecklistModel extends Model { + getChecklist(_id: string): Promise; + removeChecklists(contentType: string, contentTypeId: string): void; + createChecklist( + { contentType, contentTypeId, ...fields }: IChecklist, + user: IUserDocument, + ): Promise; + + updateChecklist(_id: string, doc: IChecklist): Promise; + + removeChecklist(_id: string): void; +} + +export interface IChecklistItemModel extends Model { + getChecklistItem(_id: string): Promise; + createChecklistItem({ checklistId, ...fields }: IChecklistItem, user: IUserDocument): Promise; + + updateChecklistItem(_id: string, doc: IChecklistItem): Promise; + removeChecklistItem(_id: string): void; + updateItemOrder(_id: string, destinationOrder: number): Promise; +} + +export const loadClass = () => { + class Checklist { + public static async getChecklist(_id: string) { + const checklist = await Checklists.findOne({ _id }); + + if (!checklist) { + throw new Error('Checklist not found'); + } + + return checklist; + } + + public static async removeChecklists(contentType: string, contentTypeId: string) { + const checklists = await Checklists.find({ contentType, contentTypeId }); + + if (checklists && checklists.length === 0) { + return; + } + + const checklistIds = checklists.map(list => list._id); + + await ChecklistItems.deleteMany({ checklistId: { $in: checklistIds } }); + + await Checklists.deleteMany({ _id: { $in: checklistIds } }); + } + + /* + * Create new checklist + */ + public static async createChecklist({ contentType, contentTypeId, ...fields }: IChecklist, user: IUserDocument) { + const checklist = await Checklists.create({ + contentType, + contentTypeId, + createdUserId: user._id, + createdDate: new Date(), + ...fields, + }); + + ActivityLogs.createChecklistLog({ item: checklist, contentType: 'checklist', action: 'create' }); + + return checklist; + } + + /* + * Update checklist + */ + public static async updateChecklist(_id: string, doc: IChecklist) { + await Checklists.updateOne({ _id }, { $set: doc }); + + return Checklists.findOne({ _id }); + } + + /* + * Remove checklist + */ + public static async removeChecklist(_id: string) { + const checklistObj = await Checklists.findOne({ _id }); + + if (!checklistObj) { + throw new Error(`Checklist not found with id ${_id}`); + } + + await ChecklistItems.deleteMany({ + checklistId: checklistObj._id, + }); + + ActivityLogs.createChecklistLog({ item: checklistObj, contentType: 'checklist', action: 'delete' }); + + return checklistObj.remove(); + } + } + + checklistSchema.loadClass(Checklist); + + return checklistSchema; +}; + +export const loadItemClass = () => { + class ChecklistItem { + public static async getChecklistItem(_id: string) { + const checklistItem = await ChecklistItems.findOne({ _id }); + + if (!checklistItem) { + throw new Error('Checklist item not found'); + } + + return checklistItem; + } + + /* + * Create new checklistItem + */ + public static async createChecklistItem({ checklistId, ...fields }: IChecklistItem, user: IUserDocument) { + const itemsCount = await ChecklistItems.count({ checklistId }); + + const checklistItem = await ChecklistItems.create({ + checklistId, + createdUserId: user._id, + createdDate: new Date(), + order: itemsCount + 1, + ...fields, + }); + + await ActivityLogs.createChecklistLog({ + item: checklistItem, + contentType: 'checklistItem', + action: 'create', + }); + + return checklistItem; + } + + /* + * Update checklistItem + */ + public static async updateChecklistItem(_id: string, doc: IChecklistItem) { + await ChecklistItems.updateOne({ _id }, { $set: doc }); + + const checklistItem = await ChecklistItems.findOne({ _id }); + const activityAction = doc.isChecked ? 'checked' : 'unChecked'; + + await ActivityLogs.createChecklistLog({ + item: checklistItem, + contentType: 'checklistItem', + action: activityAction, + }); + + return checklistItem; + } + + /* + * Remove checklist + */ + public static async removeChecklistItem(_id: string) { + const checklistItem = await ChecklistItems.findOne({ _id }); + + if (!checklistItem) { + throw new Error(`Checklist's item not found with id ${_id}`); + } + + await ActivityLogs.createChecklistLog({ + item: checklistItem, + contentType: 'checklistItem', + action: 'delete', + }); + + return checklistItem.remove(); + } + + public static async updateItemOrder(_id: string, destinationOrder: number) { + const currentItem = await ChecklistItems.findOne({ _id }).lean(); + + await ChecklistItems.updateOne( + { checklistId: currentItem.checklistId, order: destinationOrder }, + { $set: { order: currentItem.order } }, + ); + + await ChecklistItems.updateOne({ _id }, { $set: { order: destinationOrder } }); + + return ChecklistItems.findOne({ _id }).lean(); + } + } + + checklistItemSchema.loadClass(ChecklistItem); + + return checklistItemSchema; +}; + +loadClass(); +loadItemClass(); + +// tslint:disable-next-line +const Checklists = model('checklists', checklistSchema); + +// tslint:disable-next-line +const ChecklistItems = model('checklist_items', checklistItemSchema); + +export { Checklists, ChecklistItems }; diff --git a/src/db/models/Companies.ts b/src/db/models/Companies.ts index 090a840d9..b8cb3bdbb 100644 --- a/src/db/models/Companies.ts +++ b/src/db/models/Companies.ts @@ -1,25 +1,30 @@ import { Model, model } from 'mongoose'; -import { ActivityLogs, Customers, Deals, Fields, InternalNotes } from './'; +import { validSearchText } from '../../data/utils'; +import { ActivityLogs, Conformities, Fields, InternalNotes } from './'; +import { ICustomField } from './definitions/common'; import { companySchema, ICompany, ICompanyDocument } from './definitions/companies'; -import { STATUSES } from './definitions/constants'; import { IUserDocument } from './definitions/users'; -import Tickets from './Tickets'; export interface ICompanyModel extends Model { + getCompanyName(company: ICompany): string; + checkDuplication( companyFields: { primaryName?: string; + code?: string; }, idsToExclude?: string[] | string, ): never; + fillSearchText(doc: ICompany): string; + + getCompany(_id: string): Promise; + createCompany(doc: ICompany, user?: IUserDocument): Promise; updateCompany(_id: string, doc: ICompany): Promise; - updateCustomers(_id: string, customerIds: string[]): Promise; - - removeCompany(_id: string): void; + removeCompanies(_ids: string[]): Promise<{ n: number; ok: number }>; mergeCompanies(companyIds: string[], companyFields: ICompany): Promise; @@ -34,19 +39,21 @@ export const loadClass = () => { public static async checkDuplication( companyFields: { primaryName?: string; + code?: string; }, idsToExclude?: string[] | string, ) { - const query: { status: {}; [key: string]: any } = { status: { $ne: STATUSES.DELETED } }; + const query: { status: {}; [key: string]: any } = { status: { $ne: 'deleted' } }; + let previousEntry; // Adding exclude operator to the query if (idsToExclude) { - query._id = idsToExclude instanceof Array ? { $nin: idsToExclude } : { $ne: idsToExclude }; + query._id = { $nin: idsToExclude }; } if (companyFields.primaryName) { // check duplication from primaryName - let previousEntry = await Companies.find({ + previousEntry = await Companies.find({ ...query, primaryName: companyFields.primaryName, }); @@ -65,6 +72,47 @@ export const loadClass = () => { throw new Error('Duplicated name'); } } + if (companyFields.code) { + // check duplication from code + previousEntry = await Companies.find({ + ...query, + code: companyFields.code, + }); + + if (previousEntry.length > 0) { + throw new Error('Duplicated code'); + } + } + } + + public static fillSearchText(doc: ICompany) { + return validSearchText([ + (doc.names || []).join(' '), + (doc.emails || []).join(' '), + (doc.phones || []).join(' '), + doc.website || '', + doc.industry || '', + doc.plan || '', + doc.description || '', + doc.code || '', + ]); + } + + public static getCompanyName(company: ICompany) { + return company.primaryName || company.primaryEmail || company.primaryPhone || 'Unknown'; + } + + /** + * Retreives company + */ + public static async getCompany(_id: string) { + const company = await Companies.findOne({ _id }); + + if (!company) { + throw new Error('Company not found'); + } + + return company; } /** @@ -79,16 +127,17 @@ export const loadClass = () => { } // clean custom field values - doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); const company = await Companies.create({ ...doc, createdAt: new Date(), modifiedAt: new Date(), + searchText: Companies.fillSearchText(doc), }); // create log - await ActivityLogs.createCompanyLog(company); + await ActivityLogs.createCocLog({ coc: company, contentType: 'company' }); return company; } @@ -100,27 +149,14 @@ export const loadClass = () => { // Checking duplicated fields of company await Companies.checkDuplication(doc, [_id]); + // clean custom field values if (doc.customFieldsData) { - // clean custom field values - doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); } - await Companies.updateOne({ _id }, { $set: { ...doc, modifiedAt: new Date() } }); + const searchText = Companies.fillSearchText(Object.assign(await Companies.getCompany(_id), doc) as ICompany); - return Companies.findOne({ _id }); - } - - /** - * Update company customers - */ - public static async updateCustomers(_id: string, customerIds: string[]) { - // Removing companyIds from users - await Customers.updateMany({ companyIds: { $in: [_id] } }, { $pull: { companyIds: _id } }); - - // Adding companyId to the each customers - for (const customerId of customerIds) { - await Customers.findByIdAndUpdate({ _id: customerId }, { $addToSet: { companyIds: _id } }, { upsert: true }); - } + await Companies.updateOne({ _id }, { $set: { ...doc, searchText, modifiedAt: new Date() } }); return Companies.findOne({ _id }); } @@ -128,13 +164,15 @@ export const loadClass = () => { /** * Remove company */ - public static async removeCompany(companyId: string) { + public static async removeCompanies(companyIds: string[]) { // Removing modules associated with company - await InternalNotes.removeCompanyInternalNotes(companyId); + await InternalNotes.removeCompaniesInternalNotes(companyIds); - await Customers.updateMany({ companyIds: { $in: [companyId] } }, { $pull: { companyIds: companyId } }); + for (const companyId of companyIds) { + await Conformities.removeConformity({ mainType: 'company', mainTypeId: companyId }); + } - return Companies.deleteOne({ _id: companyId }); + return Companies.deleteMany({ _id: { $in: companyIds } }); } /** @@ -144,6 +182,8 @@ export const loadClass = () => { // Checking duplicated fields of company await this.checkDuplication(companyFields, companyIds); + let scopeBrandIds: string[] = []; + let customFieldsData: ICustomField[] = []; let tagIds: string[] = []; let names: string[] = []; let emails: string[] = []; @@ -151,47 +191,48 @@ export const loadClass = () => { // Merging company tags for (const companyId of companyIds) { - const companyObj = await Companies.findOne({ _id: companyId }); + const companyObj = await Companies.getCompany(companyId); - if (companyObj) { - const companyTags = companyObj.tagIds || []; - const companyNames = companyObj.names || []; - const companyEmails = companyObj.emails || []; - const companyPhones = companyObj.phones || []; + const companyTags = companyObj.tagIds || []; + const companyNames = companyObj.names || []; + const companyEmails = companyObj.emails || []; + const companyPhones = companyObj.phones || []; + const companyScopeBrandIds = companyObj.scopeBrandIds || []; - // Merging company's tag into 1 array - tagIds = tagIds.concat(companyTags); + // Merging scopeBrandIds + scopeBrandIds = scopeBrandIds.concat(companyScopeBrandIds); - // Merging company names - names = names.concat(companyNames); + // merge custom fields data + customFieldsData = [...customFieldsData, ...(companyObj.customFieldsData || [])]; - // Merging company emails - emails = emails.concat(companyEmails); + // Merging company's tag into 1 array + tagIds = tagIds.concat(companyTags); - // Merging company phones - phones = phones.concat(companyPhones); + // Merging company names + names = names.concat(companyNames); - companyObj.status = STATUSES.DELETED; + // Merging company emails + emails = emails.concat(companyEmails); - await Companies.findByIdAndUpdate(companyId, { $set: { status: STATUSES.DELETED } }); - } + // Merging company phones + phones = phones.concat(companyPhones); + + companyObj.status = 'deleted'; + + await Companies.findByIdAndUpdate(companyId, { $set: { status: 'deleted' } }); } - // Removing Duplicated Tags from company + // Removing Duplicates tagIds = Array.from(new Set(tagIds)); - - // Removing Duplicated names from company names = Array.from(new Set(names)); - - // Removing Duplicated names from company emails = Array.from(new Set(emails)); - - // Removing Duplicated names from company phones = Array.from(new Set(phones)); // Creating company with properties const company = await Companies.createCompany({ ...companyFields, + scopeBrandIds, + customFieldsData, tagIds, mergedIds: companyIds, names, @@ -199,18 +240,11 @@ export const loadClass = () => { phones, }); - // Updating customer companies - await Customers.updateMany({ companyIds: { $in: companyIds } }, { $push: { companyIds: company._id } }); - - await Customers.updateMany({ companyIds: { $in: companyIds } }, { $pullAll: { companyIds } }); + // Updating customer companies, deals, tasks, tickets + await Conformities.changeConformity({ type: 'company', newTypeId: company._id, oldTypeIds: companyIds }); // Removing modules associated with current companies await InternalNotes.changeCompany(company._id, companyIds); - await Deals.changeCompany(company._id, companyIds); - await Tickets.changeCompany(company._id, companyIds); - - // create log - await ActivityLogs.createCompanyLog(company); return company; } diff --git a/src/db/models/Configs.ts b/src/db/models/Configs.ts index 2b521632e..074cece46 100644 --- a/src/db/models/Configs.ts +++ b/src/db/models/Configs.ts @@ -1,12 +1,28 @@ import { Model, model } from 'mongoose'; -import { configSchema, IConfigDocument } from './definitions/configs'; +import { configSchema, IConfig, IConfigDocument } from './definitions/configs'; +import { COMPANY_INDUSTRY_TYPES, CUSTOMER_SELECT_OPTIONS, SOCIAL_LINKS } from './definitions/constants'; export interface IConfigModel extends Model { - createOrUpdateConfig({ code, value }: { code: string; value: string[] }): IConfigDocument; + getConfig(code: string): Promise; + createOrUpdateConfig({ code, value }: IConfig): IConfigDocument; + constants(); } export const loadClass = () => { class Config { + /* + * Get a Config + */ + public static async getConfig(code: string) { + const config = await Configs.findOne({ code }); + + if (!config) { + throw new Error('Config not found'); + } + + return config; + } + /** * Create or update config */ @@ -21,6 +37,14 @@ export const loadClass = () => { return Configs.create({ code, value }); } + + public static constants() { + return { + sex_choices: CUSTOMER_SELECT_OPTIONS.SEX, + company_industry_types: COMPANY_INDUSTRY_TYPES.map(v => ({ label: v, value: v })), + social_links: SOCIAL_LINKS, + }; + } } configSchema.loadClass(Config); diff --git a/src/db/models/Conformities.ts b/src/db/models/Conformities.ts new file mode 100644 index 000000000..c23f49e53 --- /dev/null +++ b/src/db/models/Conformities.ts @@ -0,0 +1,236 @@ +import { Model, model } from 'mongoose'; +import { + conformitySchema, + IConformityAdd, + IConformityChange, + IConformityDocument, + IConformityEdit, + IConformityFilter, + IConformityRelated, + IConformityRemove, + IConformitySaved, +} from './definitions/conformities'; + +const getSavedAnyConformityMatch = ({ mainType, mainTypeId }: { mainType: string; mainTypeId: string }) => { + return { + $or: [ + { + $and: [{ mainType }, { mainTypeId }], + }, + { + $and: [{ relType: mainType }, { relTypeId: mainTypeId }], + }, + ], + }; +}; + +const getProjectCondition = (mainType: string, mainVar: string, relVar: string) => { + return { + $cond: { + if: { $eq: ['$mainType', mainType] }, + then: '$'.concat(relVar), + else: '$'.concat(mainVar), + }, + }; +}; + +export interface IConformityModel extends Model { + addConformity(doc: IConformityAdd): Promise; + editConformity(doc: IConformityEdit): void; + changeConformity(doc: IConformityChange): void; + removeConformity(doc: IConformityRemove): void; + savedConformity(doc: IConformitySaved): Promise; + relatedConformity(doc: IConformityRelated): Promise; + filterConformity(doc: IConformityFilter): Promise; +} + +export const loadConformityClass = () => { + class Conformity { + /** + * Create a conformity + */ + public static addConformity(doc: IConformityAdd) { + return Conformities.create(doc); + } + + public static async editConformity(doc: IConformityEdit) { + const newRelTypeIds = doc.relTypeIds || []; + const oldRelTypeIds = await Conformity.savedConformity({ + mainType: doc.mainType, + mainTypeId: doc.mainTypeId, + relTypes: [doc.relType], + }); + + const removedTypeIds = oldRelTypeIds.filter(e => !newRelTypeIds.includes(e)); + const addedTypeIds = newRelTypeIds.filter(e => !oldRelTypeIds.includes(e)); + + // insert on new relTypeIds + const insertTypes = await addedTypeIds.map(relTypeId => ({ + mainType: doc.mainType, + mainTypeId: doc.mainTypeId, + relType: doc.relType, + relTypeId, + })); + await Conformities.insertMany(insertTypes); + + // delete on removedTypeIds + await Conformities.deleteMany({ + $or: [ + { + $and: [ + { mainType: doc.mainType }, + { mainTypeId: doc.mainTypeId }, + { relType: doc.relType }, + { relTypeId: { $in: removedTypeIds } }, + ], + }, + { + $and: [ + { mainType: doc.relType }, + { mainTypeId: { $in: removedTypeIds } }, + { relType: doc.mainType }, + { relTypeId: doc.mainTypeId }, + ], + }, + ], + }); + + return; + } + + public static async savedConformity(doc: IConformitySaved) { + const relTypes = doc.relTypes || []; + + const relTypeIds = await Conformities.aggregate([ + { + $match: { + $or: [ + { + $and: [{ mainType: doc.mainType }, { mainTypeId: doc.mainTypeId }, { relType: { $in: relTypes } }], + }, + { + $and: [{ mainType: { $in: relTypes } }, { relType: doc.mainType }, { relTypeId: doc.mainTypeId }], + }, + ], + }, + }, + { + $project: { + relTypeId: getProjectCondition(doc.mainType, 'mainTypeId', 'relTypeId'), + }, + }, + ]); + + return relTypeIds.map(item => String(item.relTypeId)); + } + + public static async changeConformity(doc: IConformityChange) { + await Conformities.updateMany( + { $and: [{ mainType: doc.type }, { mainTypeId: { $in: doc.oldTypeIds } }] }, + { $set: { mainTypeId: doc.newTypeId } }, + ); + + await Conformities.updateMany( + { $and: [{ relType: doc.type }, { relTypeId: { $in: doc.oldTypeIds } }] }, + { $set: { relTypeId: doc.newTypeId } }, + ); + } + + public static async filterConformity(doc: IConformityFilter) { + const relTypeIds = await Conformities.aggregate([ + { + $match: { + $or: [ + { + $and: [{ mainType: doc.mainType }, { mainTypeId: { $in: doc.mainTypeIds } }, { relType: doc.relType }], + }, + { + $and: [{ mainType: doc.relType }, { relType: doc.mainType }, { relTypeId: { $in: doc.mainTypeIds } }], + }, + ], + }, + }, + { + $project: { + relTypeId: getProjectCondition(doc.mainType, 'mainTypeId', 'relTypeId'), + }, + }, + ]); + + return relTypeIds.map(item => String(item.relTypeId)); + } + + public static async relatedConformity(doc: IConformityRelated) { + const match = getSavedAnyConformityMatch({ + mainType: doc.mainType, + mainTypeId: doc.mainTypeId, + }); + + const savedRelatedObjects = await Conformities.aggregate([ + { $match: match }, + { + $project: { + savedRelType: getProjectCondition(doc.mainType, 'mainType', 'relType'), + savedRelTypeId: getProjectCondition(doc.mainType, 'mainTypeId', 'relTypeId'), + }, + }, + ]); + + const savedList = savedRelatedObjects.map(item => item.savedRelType + '-' + item.savedRelTypeId); + + const relTypeIds = await Conformities.aggregate([ + { + $project: { + mainType: 1, + mainTypeId: 1, + relType: 1, + relTypeId: 1, + mainStr: { $concat: ['$mainType', '-', '$mainTypeId'] }, + relStr: { $concat: ['$relType', '-', '$relTypeId'] }, + }, + }, + { + $match: { + $or: [ + { + $and: [{ mainType: doc.relType }, { relStr: { $in: savedList } }], + }, + { + $and: [{ relType: doc.relType }, { mainStr: { $in: savedList } }], + }, + ], + }, + }, + { + $project: { + relTypeId: getProjectCondition(doc.relType || '', 'relTypeId', 'mainTypeId'), + }, + }, + ]); + + return relTypeIds.map(item => item.relTypeId); + } + + /** + * Remove conformity + */ + public static async removeConformity(doc: IConformityRemove) { + const match = getSavedAnyConformityMatch({ + mainType: doc.mainType, + mainTypeId: doc.mainTypeId, + }); + + await Conformities.deleteMany(match); + } + } + + conformitySchema.loadClass(Conformity); + return conformitySchema; +}; + +loadConformityClass(); + +// tslint:disable-next-line +const Conformities = model('conformity', conformitySchema); + +export default Conformities; diff --git a/src/db/models/ConversationMessages.ts b/src/db/models/ConversationMessages.ts index 4e7ef4970..506bdf708 100644 --- a/src/db/models/ConversationMessages.ts +++ b/src/db/models/ConversationMessages.ts @@ -1,18 +1,34 @@ import { Model, model } from 'mongoose'; import * as strip from 'strip'; import { Conversations } from '.'; +import { MESSAGE_TYPES } from './definitions/constants'; import { IMessage, IMessageDocument, messageSchema } from './definitions/conversationMessages'; export interface IMessageModel extends Model { + getMessage(_id: string): Promise; createMessage(doc: IMessage): Promise; addMessage(doc: IMessage, userId?: string): Promise; - getNonAsnweredMessage(conversationId: string): Promise; - getAdminMessages(conversationId: string): Promise; + getNonAsnweredMessage(conversationId: string); + getAdminMessages(conversationId: string); + widgetsGetUnreadMessagesCount(conversationId: string): Promise; markSentAsReadMessages(conversationId: string): Promise; + forceReadCustomerPreviousEngageMessages(customerId: string): Promise; } export const loadClass = () => { class Message { + /** + * Retreives message + */ + public static async getMessage(_id: string) { + const message = await Messages.findOne({ _id }); + + if (!message) { + throw new Error('Conversation message not found'); + } + + return message; + } /** * Create a message */ @@ -20,24 +36,24 @@ export const loadClass = () => { const message = await Messages.create({ internal: false, ...doc, - createdAt: new Date(), + createdAt: doc.createdAt || new Date(), }); const messageCount = await Messages.find({ conversationId: message.conversationId, }).countDocuments(); - await Conversations.updateOne( - { _id: message.conversationId }, - { - $set: { - messageCount, + // update conversation ==== + const convDocModifier: { messageCount?: number; updatedAt: Date; isCustomerRespondedLast?: boolean } = { + updatedAt: new Date(), + }; - // updating updatedAt - updatedAt: new Date(), - }, - }, - ); + if (!doc.fromBot) { + convDocModifier.messageCount = messageCount; + convDocModifier.isCustomerRespondedLast = doc.customerId ? true : false; + } + + await Conversations.updateConversation(message.conversationId, convDocModifier); if (message.userId) { // add created user to participators @@ -45,9 +61,7 @@ export const loadClass = () => { } // add mentioned users to participators - if (message.mentionedUserIds) { - await Conversations.addManyParticipatedUsers(message.conversationId, message.mentionedUserIds); - } + await Conversations.addManyParticipatedUsers(message.conversationId, message.mentionedUserIds || []); return message; } @@ -71,8 +85,11 @@ export const loadClass = () => { doc.content = content; doc.attachments = attachments; + // tags wrapped inside empty

tag should be allowed + const contentValid = content.indexOf(' { firstRespondedDate?: Date; } = {}; - if (!doc.fromBot) { + if (!doc.fromBot && !doc.internal) { modifier.content = doc.content; } @@ -92,7 +109,7 @@ export const loadClass = () => { modifier.firstRespondedDate = new Date(); } - await Conversations.updateOne({ _id: doc.conversationId }, { $set: modifier }); + await Conversations.updateConversation(doc.conversationId, modifier); return this.createMessage({ ...doc, userId }); } @@ -114,13 +131,22 @@ export const loadClass = () => { return Messages.find({ conversationId, userId: { $exists: true }, - isCustomerRead: false, + isCustomerRead: { $ne: true }, // exclude internal notes internal: false, }).sort({ createdAt: 1 }); } + public static widgetsGetUnreadMessagesCount(conversationId: string) { + return Messages.countDocuments({ + conversationId, + userId: { $exists: true }, + internal: false, + isCustomerRead: { $ne: true }, + }); + } + /** * Mark sent messages as read */ @@ -129,7 +155,23 @@ export const loadClass = () => { { conversationId, userId: { $exists: true }, - isCustomerRead: { $exists: false }, + isCustomerRead: { $ne: true }, + }, + { $set: { isCustomerRead: true } }, + { multi: true }, + ); + } + + /** + * Force read previous unread engage messages ============ + */ + public static forceReadCustomerPreviousEngageMessages(customerId: string) { + return Messages.updateMany( + { + customerId, + engageData: { $exists: true }, + 'engageData.engageKind': { $ne: 'auto' }, + isCustomerRead: { $ne: true }, }, { $set: { isCustomerRead: true } }, { multi: true }, diff --git a/src/db/models/Conversations.ts b/src/db/models/Conversations.ts index 840ceb034..24ac37c5b 100644 --- a/src/db/models/Conversations.ts +++ b/src/db/models/Conversations.ts @@ -1,25 +1,20 @@ import { Model, model } from 'mongoose'; import { ConversationMessages, Users } from '.'; +import { cleanHtml, sendToWebhook } from '../../data/utils'; import { CONVERSATION_STATUSES } from './definitions/constants'; import { IMessageDocument } from './definitions/conversationMessages'; import { conversationSchema, IConversation, IConversationDocument } from './definitions/conversations'; -interface ISTATUSES { - NEW: 'new'; - OPEN: 'open'; - CLOSED: 'closed'; - ALL_LIST: ['new', 'open', 'closed']; -} - export interface IConversationModel extends Model { - getConversationStatuses(): ISTATUSES; + getConversation(_id: string): IConversationDocument; createConversation(doc: IConversation): Promise; + updateConversation(_id: string, doc): Promise; checkExistanceConversations(ids: string[]): any; reopen(_id: string): Promise; assignUserConversation(conversationIds: string[], assignedUserId?: string): Promise; - unassignUserConversation(conversationIds: string[]): Promise; + unassignUserConversation(conversationIds: string[]): Promise; changeCustomerStatus(status: string, customerId: string, integrationId: string): Promise; @@ -34,13 +29,25 @@ export interface IConversationModel extends Model { changeCustomer(newCustomerId: string, customerIds: string[]): Promise; - removeCustomerConversations(customerId: string): Promise; + removeCustomersConversations(customerId: string[]): Promise<{ n: number; ok: number }>; + widgetsUnreadMessagesQuery(conversations: IConversationDocument[]): any; + + resolveAllConversation(query: any, userId: string): Promise<{ n: number; nModified: number; ok: number }>; } export const loadClass = () => { class Conversation { - public static getConversationStatuses() { - return CONVERSATION_STATUSES; + /** + * Retreives conversation + */ + public static async getConversation(_id: string) { + const conversation = await Conversations.findOne({ _id }); + + if (!conversation) { + throw new Error('Conversation not found'); + } + + return conversation; } /** @@ -63,35 +70,46 @@ export const loadClass = () => { public static async createConversation(doc: IConversation) { const now = new Date(); - return Conversations.create({ - status: this.getConversationStatuses().NEW, + const result = await Conversations.create({ + status: CONVERSATION_STATUSES.NEW, ...doc, - createdAt: now, - updatedAt: now, + content: cleanHtml(doc.content), + createdAt: doc.createdAt || now, + updatedAt: doc.createdAt || now, number: (await Conversations.find().countDocuments()) + 1, messageCount: 0, }); + + await sendToWebhook('create', 'conversation', result); + + return result; + } + + /** + * Update a conversation + */ + public static async updateConversation(_id, doc) { + if (doc.content) { + doc.content = cleanHtml(doc.content); + } + + return Conversations.updateOne({ _id }, { $set: doc }); } /* * Reopens conversation */ public static async reopen(_id: string) { - await Conversations.updateOne( - { _id }, - { - $set: { - // reset read state - readUserIds: [], + await Conversations.updateConversation(_id, { + // reset read state + readUserIds: [], - // if closed, reopen - status: this.getConversationStatuses().OPEN, + // if closed, reopen + status: CONVERSATION_STATUSES.OPEN, - closedAt: null, - closedUserId: null, - }, - }, - ); + closedAt: null, + closedUserId: null, + }); return Conversations.findOne({ _id }); } @@ -159,7 +177,7 @@ export const loadClass = () => { let closedAt; let closedUserId; - if (status === this.getConversationStatuses().CLOSED) { + if (status === CONVERSATION_STATUSES.CLOSED) { closedAt = new Date(); closedUserId = userId; } @@ -185,12 +203,13 @@ export const loadClass = () => { // if current user is first one if (!readUserIds || readUserIds.length === 0) { - await Conversations.updateOne({ _id }, { $set: { readUserIds: [userId] } }); + await Conversations.updateConversation(_id, { readUserIds: [userId] }); } // if current user is not in read users list then add it if (!readUserIds.includes(userId)) { - await Conversations.updateOne({ _id }, { $push: { readUserIds: userId } }); + readUserIds.push(userId); + await Conversations.updateConversation(_id, { readUserIds }); } return Conversations.findOne({ _id }); @@ -202,35 +221,32 @@ export const loadClass = () => { public static async newOrOpenConversation() { return Conversations.find({ status: { - $in: [this.getConversationStatuses().NEW, this.getConversationStatuses().OPEN], + $in: [CONVERSATION_STATUSES.NEW, CONVERSATION_STATUSES.OPEN], }, + messageCount: { $gt: 1 }, }); } /** * Add participated users */ public static addManyParticipatedUsers(conversationId: string, userIds: string[]) { - if (conversationId && userIds) { - return Conversations.updateOne( - { _id: conversationId }, - { - $addToSet: { participatedUserIds: { $each: userIds } }, - }, - ); - } + return Conversations.updateOne( + { _id: conversationId }, + { + $addToSet: { participatedUserIds: { $each: userIds } }, + }, + ); } /** * Add participated user */ public static addParticipatedUsers(conversationId: string, userId: string) { - if (conversationId && userId) { - return Conversations.updateOne( - { _id: conversationId }, - { - $addToSet: { participatedUserIds: userId }, - }, - ); - } + return Conversations.updateOne( + { _id: conversationId }, + { + $addToSet: { participatedUserIds: userId }, + }, + ); } /** @@ -250,21 +266,42 @@ export const loadClass = () => { } /** - * Removes customer conversations + * Removes customers conversations */ - public static async removeCustomerConversations(customerId: string) { + public static async removeCustomersConversations(customerIds: string[]) { // Finding every conversation of customer const conversations = await Conversations.find({ - customerId, + customerId: { $in: customerIds }, }); // Removing conversations and conversation messages const conversationIds = conversations.map(conv => conv._id); + await ConversationMessages.deleteMany({ conversationId: { $in: conversationIds }, }); + await Conversations.deleteMany({ _id: { $in: conversationIds } }); } + + public static widgetsUnreadMessagesQuery(conversations: IConversationDocument[]) { + const unreadMessagesSelector = { userId: { $exists: true }, internal: false, isCustomerRead: { $ne: true } }; + + const conversationIds = conversations.map(c => c._id); + + return { conversationId: { $in: conversationIds }, ...unreadMessagesSelector }; + } + + /** + * Resolve all conversation + */ + public static resolveAllConversation(query: any, userId: string) { + const closedAt = new Date(); + const closedUserId = userId; + const status = CONVERSATION_STATUSES.CLOSED; + + return Conversations.updateMany(query, { $set: { status, closedAt, closedUserId } }, { multi: true }); + } } conversationSchema.loadClass(Conversation); diff --git a/src/db/models/Customers.ts b/src/db/models/Customers.ts index fafa26d3b..54e3933aa 100644 --- a/src/db/models/Customers.ts +++ b/src/db/models/Customers.ts @@ -1,26 +1,97 @@ import { Model, model } from 'mongoose'; -import { validateEmail } from '../../data/utils'; -import { ActivityLogs, Conversations, Deals, EngageMessages, Fields, InternalNotes, Tickets } from './'; -import { STATUSES } from './definitions/constants'; +import { validSearchText } from '../../data/utils'; +import { validateSingle } from '../../data/verifierUtils'; +import { ActivityLogs, Conformities, Conversations, EngageMessages, Fields, InternalNotes } from './'; +import { ICustomField } from './definitions/common'; import { customerSchema, ICustomer, ICustomerDocument } from './definitions/customers'; import { IUserDocument } from './definitions/users'; +interface IGetCustomerParams { + email?: string; + phone?: string; + code?: string; + integrationId?: string; + cachedCustomerId?: string; +} + interface ICustomerFieldsInput { primaryEmail?: string; primaryPhone?: string; + code?: string; +} + +interface ICreateMessengerCustomerParams { + doc: { + integrationId: string; + email?: string; + emailValidationStatus?: string; + phone?: string; + phoneValidationStatus?: string; + code?: string; + isUser?: boolean; + firstName?: string; + lastName?: string; + description?: string; + deviceToken?: string; + }; + customData?: any; +} + +export interface IUpdateMessengerCustomerParams { + _id: string; + doc: { + integrationId: string; + email?: string; + phone?: string; + code?: string; + isUser?: boolean; + deviceToken?: string; + }; + customData?: any; +} + +export interface IVisitorContactInfoParams { + customerId: string; + type: string; + value: string; +} + +export interface IBrowserInfo { + language?: string; + url?: string; + city?: string; + countryCode?: string; +} + +interface IPSS { + profileScore: string; + searchText: string; + state: string; } export interface ICustomerModel extends Model { checkDuplication(customerFields: ICustomerFieldsInput, idsToExclude?: string[] | string): never; + getCustomer(_id: string): Promise; + getCustomerName(customer: ICustomer): string; + createVisitor(): Promise; createCustomer(doc: ICustomer, user?: IUserDocument): Promise; updateCustomer(_id: string, doc: ICustomer): Promise; markCustomerAsActive(customerId: string): Promise; markCustomerAsNotActive(_id: string): Promise; - updateCompanies(_id: string, companyIds: string[]): Promise; - removeCustomer(customerId: string): void; - mergeCustomers(customerIds: string[], customerFields: ICustomer): Promise; + removeCustomers(customerIds: string[]): Promise<{ n: number; ok: number }>; + changeState(_id: string, value: string): Promise; + mergeCustomers(customerIds: string[], customerFields: ICustomer, user?: IUserDocument): Promise; bulkInsert(fieldNames: string[], fieldValues: string[][], user: IUserDocument): Promise; - updateProfileScore(customerId: string, save: boolean): never; + calcPSS(doc: any): IPSS; + updateVerificationStatus(customerIds: string[], type: string, status: string): Promise; + + // widgets === + getWidgetCustomer(doc: IGetCustomerParams): Promise; + createMessengerCustomer(param: ICreateMessengerCustomerParams): Promise; + updateMessengerCustomer(param: IUpdateMessengerCustomerParams): Promise; + updateSession(_id: string): Promise; + updateLocation(_id: string, browserInfo: IBrowserInfo): Promise; + saveVisitorContactInfo(doc: IVisitorContactInfoParams): Promise; } export const loadClass = () => { @@ -29,7 +100,7 @@ export const loadClass = () => { * Checking if customer has duplicated unique properties */ public static async checkDuplication(customerFields: ICustomerFieldsInput, idsToExclude?: string[] | string) { - const query: { status: {}; [key: string]: any } = { status: { $ne: STATUSES.DELETED } }; + const query: { status: {}; [key: string]: any } = { status: { $ne: 'deleted' } }; let previousEntry; // Adding exclude operator to the query @@ -80,12 +151,70 @@ export const loadClass = () => { throw new Error('Duplicated phone'); } } + + if (customerFields.code) { + // check duplication from code + previousEntry = await Customers.find({ + ...query, + code: customerFields.code, + }); + + if (previousEntry.length > 0) { + throw new Error('Duplicated code'); + } + } + } + + public static getCustomerName(customer: ICustomer) { + if (customer.firstName || customer.lastName) { + return (customer.firstName || '') + ' ' + (customer.lastName || ''); + } + + if (customer.primaryEmail || customer.primaryPhone) { + return customer.primaryEmail || customer.primaryPhone; + } + + const { visitorContactInfo } = customer; + + if (visitorContactInfo) { + return visitorContactInfo.phone || visitorContactInfo.email; + } + + return 'Unknown'; + } + + /** + * Retreives customer + */ + public static async getCustomer(_id: string) { + const customer = await Customers.findOne({ _id }); + + if (!customer) { + throw new Error('Customer not found'); + } + + return customer; + } + + /** + * Create a visitor + */ + public static async createVisitor(): Promise { + const customer = await Customers.create({ + state: 'visitor', + createdAt: new Date(), + modifiedAt: new Date(), + }); + + await ActivityLogs.createCocLog({ coc: customer, contentType: 'customer' }); + + return customer._id; } /** * Create a customer */ - public static async createCustomer(doc: ICustomer, user?: IUserDocument) { + public static async createCustomer(doc: ICustomer, user?: IUserDocument): Promise { // Checking duplicated fields of customer await Customers.checkDuplication(doc); @@ -93,27 +222,41 @@ export const loadClass = () => { doc.ownerId = user._id; } + if (doc.primaryEmail && !doc.emails) { + doc.emails = [doc.primaryEmail]; + } + + if (doc.primaryPhone && !doc.phones) { + doc.phones = [doc.primaryPhone]; + } + // clean custom field values - doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); - const isValid = await validateEmail(doc.primaryEmail); + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); - if (doc.primaryEmail && isValid) { - doc.hasValidEmail = true; + if (doc.integrationId) { + doc.relatedIntegrationIds = [doc.integrationId]; } + const pssDoc = await Customers.calcPSS(doc); + const customer = await Customers.create({ createdAt: new Date(), modifiedAt: new Date(), ...doc, + ...pssDoc, }); - // calculateProfileScore - await Customers.updateProfileScore(customer._id, true); + if (doc.primaryEmail && !doc.emailValidationStatus) { + validateSingle({ email: doc.primaryEmail }); + } - // create log - await ActivityLogs.createCustomerLog(customer); + if (doc.primaryPhone && !doc.phoneValidationStatus) { + validateSingle({ phone: doc.primaryPhone }); + } - return customer; + await ActivityLogs.createCocLog({ coc: customer, contentType: 'customer' }); + + return Customers.getCustomer(customer._id); } /* @@ -123,20 +266,32 @@ export const loadClass = () => { // Checking duplicated fields of customer await Customers.checkDuplication(doc, _id); + const oldCustomer = await Customers.getCustomer(_id); + if (doc.customFieldsData) { // clean custom field values - doc.customFieldsData = await Fields.cleanMulti(doc.customFieldsData || {}); + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); } if (doc.primaryEmail) { - const isValid = await validateEmail(doc.primaryEmail); - doc.hasValidEmail = isValid; + if (doc.primaryEmail !== oldCustomer.primaryEmail) { + doc.emailValidationStatus = 'unknown'; + + validateSingle({ email: doc.primaryEmail }); + } + } + + if (doc.primaryPhone) { + if (doc.primaryPhone !== oldCustomer.primaryPhone) { + doc.phoneValidationStatus = 'unknown'; + + validateSingle({ phone: doc.primaryPhone }); + } } - // calculateProfileScore - await Customers.updateProfileScore(_id, true); + const pssDoc = await Customers.calcPSS({ ...oldCustomer.toObject(), ...doc }); - await Customers.updateOne({ _id }, { $set: { ...doc, modifiedAt: new Date() } }); + await Customers.updateOne({ _id }, { $set: { ...doc, ...pssDoc, modifiedAt: new Date() } }); return Customers.findOne({ _id }); } @@ -145,7 +300,7 @@ export const loadClass = () => { * Mark customer as active */ public static async markCustomerAsActive(customerId: string) { - await Customers.updateOne({ _id: customerId }, { $set: { 'messengerData.isActive': true } }); + await Customers.updateOne({ _id: customerId }, { $set: { isOnline: true } }); return Customers.findOne({ _id: customerId }); } @@ -158,8 +313,8 @@ export const loadClass = () => { _id, { $set: { - 'messengerData.isActive': false, - 'messengerData.lastSeenAt': new Date(), + isOnline: false, + lastSeenAt: new Date(), }, }, { new: true }, @@ -169,80 +324,92 @@ export const loadClass = () => { } /** - * Update customer companies + * Calc customer profileScore, searchText and state */ - public static async updateCompanies(_id: string, companyIds: string[]) { - // updating companyIds field - await Customers.findByIdAndUpdate(_id, { $set: { companyIds } }); - - return Customers.findOne({ _id }); - } - - /** - * Update customer profile score - */ - public static async updateProfileScore(customerId: string, save: boolean) { - let score = 0; - + public static async calcPSS(customer: any) { const nullValues = ['', null]; - const customer = await Customers.findOne({ _id: customerId }); - if (!customer) { - return 0; - } + let possibleLead = false; + let score = 0; + let searchText = (customer.emails || []).join(' ').concat(' ', (customer.phones || []).join(' ')); if (!nullValues.includes(customer.firstName || '')) { score += 10; + possibleLead = true; + searchText = searchText.concat(' ', customer.firstName || ''); } if (!nullValues.includes(customer.lastName || '')) { score += 5; + possibleLead = true; + searchText = searchText.concat(' ', customer.lastName || ''); + } + + if (!nullValues.includes(customer.code || '')) { + score += 10; + possibleLead = true; + searchText = searchText.concat(' ', customer.code || ''); } if (!nullValues.includes(customer.primaryEmail || '')) { + possibleLead = true; score += 15; } if (!nullValues.includes(customer.primaryPhone || '')) { + possibleLead = true; score += 10; } if (customer.visitorContactInfo != null) { + possibleLead = true; score += 5; + + searchText = searchText.concat( + ' ', + customer.visitorContactInfo.email || '', + ' ', + customer.visitorContactInfo.phone || '', + ); } - if (!save) { - return { - updateOne: { - filter: { _id: customerId }, - update: { $set: { profileScore: score } }, - }, - }; + searchText = validSearchText([searchText]); + + let state = customer.state || 'visitor'; + + if (possibleLead && state !== 'customer') { + state = 'lead'; } - await Customers.updateOne({ _id: customerId }, { $set: { profileScore: score } }); + return { profileScore: score, searchText, state }; } /** - * Removes customer + * Remove customers */ - public static async removeCustomer(customerId: string) { + public static async removeCustomers(customerIds: string[]) { // Removing every modules that associated with customer - await Conversations.removeCustomerConversations(customerId); - await EngageMessages.removeCustomerEngages(customerId); - await InternalNotes.removeCustomerInternalNotes(customerId); + await Conversations.removeCustomersConversations(customerIds); + await EngageMessages.removeCustomersEngages(customerIds); + await InternalNotes.removeCustomersInternalNotes(customerIds); + + for (const customerId of customerIds) { + await Conformities.removeConformity({ mainType: 'customer', mainTypeId: customerId }); + } - return Customers.deleteOne({ _id: customerId }); + return Customers.deleteMany({ _id: { $in: customerIds } }); } /** * Merge customers */ - public static async mergeCustomers(customerIds: string[], customerFields: ICustomer) { + public static async mergeCustomers(customerIds: string[], customerFields: ICustomer, user?: IUserDocument) { // Checking duplicated fields of customer await Customers.checkDuplication(customerFields, customerIds); + let scopeBrandIds: string[] = []; let tagIds: string[] = []; - let companyIds: string[] = []; + let customFieldsData: ICustomField[] = []; + let state: any = ''; let emails: string[] = []; let phones: string[] = []; @@ -255,7 +422,6 @@ export const loadClass = () => { phones.push(customerFields.primaryPhone); } - // Merging customer tags and companies for (const customerId of customerIds) { const customerObj = await Customers.findOne({ _id: customerId }); @@ -263,55 +429,313 @@ export const loadClass = () => { // get last customer's integrationId customerFields.integrationId = customerObj.integrationId; + // merge custom fields data + customFieldsData = [...customFieldsData, ...(customerObj.customFieldsData || [])]; + + // Merging scopeBrandIds + scopeBrandIds = [...scopeBrandIds, ...(customerObj.scopeBrandIds || [])]; + const customerTags: string[] = customerObj.tagIds || []; - const customerCompanies: string[] = customerObj.companyIds || []; // Merging customer's tag and companies into 1 array tagIds = tagIds.concat(customerTags); - companyIds = companyIds.concat(customerCompanies); // Merging emails, phones emails = [...emails, ...(customerObj.emails || [])]; phones = [...phones, ...(customerObj.phones || [])]; - await Customers.findByIdAndUpdate(customerId, { $set: { status: STATUSES.DELETED } }); + // Merging customer`s state for new customer + state = customerObj.state; + + await Customers.findByIdAndUpdate(customerId, { $set: { status: 'deleted' } }); } } - // Removing Duplicated Tags from customer + // Removing Duplicates + scopeBrandIds = Array.from(new Set(scopeBrandIds)); tagIds = Array.from(new Set(tagIds)); - // Removing Duplicated Companies from customer - companyIds = Array.from(new Set(companyIds)); - // Removing Duplicated Emails from customer emails = Array.from(new Set(emails)); - - // Removing Duplicated Phones from customer phones = Array.from(new Set(phones)); // Creating customer with properties - const customer = await this.createCustomer({ - ...customerFields, - tagIds, - companyIds, - mergedIds: customerIds, - emails, - phones, - }); + const customer = await this.createCustomer( + { + ...customerFields, + scopeBrandIds, + customFieldsData, + tagIds, + mergedIds: customerIds, + emails, + phones, + state, + }, + user, + ); // Updating every modules associated with customers + await Conformities.changeConformity({ type: 'customer', newTypeId: customer._id, oldTypeIds: customerIds }); await Conversations.changeCustomer(customer._id, customerIds); await EngageMessages.changeCustomer(customer._id, customerIds); await InternalNotes.changeCustomer(customer._id, customerIds); - await Deals.changeCustomer(customer._id, customerIds); - await Tickets.changeCustomer(customer._id, customerIds); - // create log - await ActivityLogs.createCustomerLog(customer); + return customer; + } + + /* + * Get widget customer + */ + public static async getWidgetCustomer({ integrationId, email, phone, code, cachedCustomerId }: IGetCustomerParams) { + let customer: ICustomerDocument | null = null; + + if (email) { + customer = await Customers.findOne({ + $or: [{ emails: { $in: [email] } }, { primaryEmail: email }], + }); + } + + if (!customer && phone) { + customer = await Customers.findOne({ + $or: [{ phones: { $in: [phone] } }, { primaryPhone: phone }], + }); + } + + if (!customer && code) { + customer = await Customers.findOne({ code }); + } + + if (!customer && cachedCustomerId) { + customer = await Customers.findOne({ _id: cachedCustomerId }); + } + + if (customer) { + const ids = customer.relatedIntegrationIds; + + if (integrationId && ids && !ids.includes(integrationId)) { + ids.push(integrationId); + await Customers.updateOne({ _id: customer._id }, { $set: { relatedIntegrationIds: ids } }); + customer = await Customers.findOne({ _id: customer._id }); + } + } return customer; } + + public static customerFieldNames() { + const names: string[] = []; + + customerSchema.eachPath(name => { + names.push(name); + + const path = customerSchema.paths[name]; + + if (path.schema) { + path.schema.eachPath(subName => { + names.push(`${name}.${subName}`); + }); + } + }); + + return names; + } + + public static fixListFields(doc: any, customData = {}, customer?: ICustomerDocument) { + let emails: string[] = []; + let phones: string[] = []; + let deviceTokens: string[] = []; + + // extract basic fields from customData + for (const name of this.customerFieldNames()) { + if (customData[name]) { + doc[name] = customData[name]; + + delete customData[name]; + } + } + + if (customer) { + emails = customer.emails || []; + phones = customer.phones || []; + deviceTokens = customer.deviceTokens || []; + } + + if (doc.email) { + if (!emails.includes(doc.email)) { + emails.push(doc.email); + } + + doc.primaryEmail = doc.email; + + delete doc.email; + } + + if (doc.phone) { + if (!phones.includes(doc.phone)) { + phones.push(doc.phone); + } + + doc.primaryPhone = doc.phone; + + delete doc.phone; + } + + if (doc.deviceToken) { + if (!deviceTokens.includes(doc.deviceToken)) { + deviceTokens.push(doc.deviceToken); + } + + delete doc.deviceToken; + } + + doc.emails = emails; + doc.phones = phones; + doc.deviceTokens = deviceTokens; + + return doc; + } + + /* + * Create a new messenger customer + */ + public static async createMessengerCustomer({ doc, customData }: ICreateMessengerCustomerParams) { + this.fixListFields(doc, customData); + + return this.createCustomer({ + ...doc, + trackedData: Fields.generateTypedListFromMap(customData), + lastSeenAt: new Date(), + isOnline: true, + sessionCount: 1, + }); + } + + /* + * Update messenger customer + */ + public static async updateMessengerCustomer({ _id, doc, customData }: IUpdateMessengerCustomerParams) { + const customer = await Customers.getCustomer(_id); + + this.fixListFields(doc, customData, customer); + + const modifier = { + ...doc, + trackedData: Fields.generateTypedListFromMap(customData), + state: doc.isUser ? 'customer' : customer.state, + modifiedAt: new Date(), + }; + + await Customers.updateOne({ _id }, { $set: modifier }); + + const updateCustomer = await Customers.getCustomer(_id); + + const pssDoc = await Customers.calcPSS(updateCustomer); + + await Customers.updateOne({ _id }, { $set: pssDoc }); + + return Customers.findOne({ _id }); + } + + /* + * Update session data + */ + public static async updateSession(_id: string) { + const now = new Date(); + const customer = await Customers.getCustomer(_id); + + const query: any = { + $set: { + lastSeenAt: now, + isOnline: true, + }, + }; + + // Preventing session count to increase on page every refresh + // Close your web site tab and reopen it after 6 seconds then it will increase + // session count by 1 + if (customer.lastSeenAt && now.getTime() - customer.lastSeenAt.getTime() > 6 * 1000) { + // update session count + query.$inc = { sessionCount: 1 }; + } + + // update + await Customers.findByIdAndUpdate(_id, query); + + // updated customer + return Customers.findOne({ _id }); + } + + /* + * Change state + */ + public static async changeState(_id: string, value: string) { + await Customers.findByIdAndUpdate( + { _id }, + { + $set: { state: value }, + }, + ); + + return Customers.findOne({ _id }); + } + + /* + * Update customer's location info + */ + public static async updateLocation(_id: string, browserInfo: IBrowserInfo) { + await Customers.findByIdAndUpdate( + { _id }, + { + $set: { location: browserInfo }, + }, + ); + + return Customers.findOne({ _id }); + } + + /* + * If customer is a visitor then we will contact with this customer using + * this information later + */ + public static async saveVisitorContactInfo(args: IVisitorContactInfoParams) { + const { customerId, type, value } = args; + + if (type === 'email') { + await Customers.updateOne( + { _id: customerId }, + { + $set: { 'visitorContactInfo.email': value }, + $push: { emails: value }, + }, + ); + } + + if (type === 'phone') { + await Customers.updateOne( + { _id: customerId }, + { + $set: { 'visitorContactInfo.phone': value }, + $push: { phones: value }, + }, + ); + } + + const customer = await Customers.getCustomer(customerId); + + const pssDoc = await Customers.calcPSS(customer); + + await Customers.updateOne({ _id: customerId }, { $set: pssDoc }); + + return Customers.getCustomer(customerId); + } + + public static async updateVerificationStatus(customerIds: string, type: string, status: string) { + const set: any = type !== 'email' ? { phoneValidationStatus: status } : { emailValidationStatus: status }; + + await Customers.updateMany({ _id: { $in: customerIds } }, { $set: set }); + + return Customers.find({ _id: { $in: customerIds } }); + } } customerSchema.loadClass(Customer); diff --git a/src/db/models/Deals.ts b/src/db/models/Deals.ts index 9a206aea3..fb7fe4d65 100644 --- a/src/db/models/Deals.ts +++ b/src/db/models/Deals.ts @@ -1,18 +1,15 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; -import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; -import { IOrderInput } from './definitions/boards'; +import { destroyBoardItemRelations, fillSearchTextItem, watchItem } from './boardUtils'; +import { ACTIVITY_CONTENT_TYPES } from './definitions/constants'; import { dealSchema, IDeal, IDealDocument } from './definitions/deals'; export interface IDealModel extends Model { getDeal(_id: string): Promise; createDeal(doc: IDeal): Promise; updateDeal(_id: string, doc: IDeal): Promise; - updateOrder(stageId: string, orders: IOrderInput[]): Promise; - removeDeal(_id: string): void; watchDeal(_id: string, isAdd: boolean, userId: string): void; - changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; - changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; + removeDeals(_ids: string[]): Promise<{ n: number; ok: number }>; } export const loadDealClass = () => { @@ -31,18 +28,23 @@ export const loadDealClass = () => { * Create a deal */ public static async createDeal(doc: IDeal) { - const dealsCount = await Deals.find({ - stageId: doc.stageId, - }).countDocuments(); + if (doc.sourceConversationId) { + const convertedDeal = await Deals.findOne({ sourceConversationId: doc.sourceConversationId }); + + if (convertedDeal) { + throw new Error('Already converted a deal'); + } + } const deal = await Deals.create({ ...doc, - order: dealsCount, + createdAt: new Date(), modifiedAt: new Date(), + searchText: fillSearchTextItem(doc), }); // create log - await ActivityLogs.createDealLog(deal); + await ActivityLogs.createBoardItemLog({ item: deal, contentType: 'deal' }); return deal; } @@ -51,52 +53,29 @@ export const loadDealClass = () => { * Update Deal */ public static async updateDeal(_id: string, doc: IDeal) { - await Deals.updateOne({ _id }, { $set: doc }); - - return Deals.findOne({ _id }); - } - - /* - * Update given deals orders - */ - public static async updateOrder(stageId: string, orders: IOrderInput[]) { - return updateOrder(Deals, orders, stageId); - } - - /** - * Remove Deal - */ - public static async removeDeal(_id: string) { - const deal = await Deals.findOne({ _id }); + const searchText = fillSearchTextItem(doc, await Deals.getDeal(_id)); - if (!deal) { - throw new Error('Deal not found'); - } + await Deals.updateOne({ _id }, { $set: doc, searchText }); - return deal.remove(); + return Deals.findOne({ _id }); } /** * Watch deal */ - public static async watchDeal(_id: string, isAdd: boolean, userId: string) { + public static watchDeal(_id: string, isAdd: boolean, userId: string) { return watchItem(Deals, _id, isAdd, userId); } - /** - * Change customer - */ - public static async changeCustomer(newCustomerId: string, oldCustomerIds: string[]) { - return changeCustomer(Deals, newCustomerId, oldCustomerIds); - } + public static async removeDeals(_ids: string[]) { + // completely remove all related things + for (const _id of _ids) { + await destroyBoardItemRelations(_id, ACTIVITY_CONTENT_TYPES.DEAL); + } - /** - * Change company - */ - public static async changeCompany(newCompanyId: string, oldCompanyIds: string[]) { - return changeCompany(Deals, newCompanyId, oldCompanyIds); + return Deals.deleteMany({ _id: { $in: _ids } }); } - } + } // end Deal class dealSchema.loadClass(Deal); diff --git a/src/db/models/EmailDeliveries.ts b/src/db/models/EmailDeliveries.ts index 86573f0c6..8cf7f4243 100644 --- a/src/db/models/EmailDeliveries.ts +++ b/src/db/models/EmailDeliveries.ts @@ -1,9 +1,9 @@ import { Model, model } from 'mongoose'; -import { ActivityLogs } from '.'; import { emailDeliverySchema, IEmailDeliveries, IEmailDeliveriesDocument } from './definitions/emailDeliveries'; export interface IEmailDeliveryModel extends Model { createEmailDelivery(doc: IEmailDeliveries): Promise; + updateEmailDeliveryStatus(_id: string, status: string): Promise; } export const loadClass = () => { @@ -12,12 +12,13 @@ export const loadClass = () => { * Create an EmailDelivery document */ public static async createEmailDelivery(doc: IEmailDeliveries) { - const emailDelivery = await EmailDeliveries.create(doc); - - // create log - await ActivityLogs.createEmailDeliveryLog(emailDelivery); + return EmailDeliveries.create({ + ...doc, + }); + } - return emailDelivery; + public static async updateEmailDeliveryStatus(_id: string, status: string) { + return EmailDeliveries.updateOne({ _id }, { $set: { status } }); } } diff --git a/src/db/models/EmailTemplates.ts b/src/db/models/EmailTemplates.ts index 4343ec661..f9f04121a 100644 --- a/src/db/models/EmailTemplates.ts +++ b/src/db/models/EmailTemplates.ts @@ -2,6 +2,7 @@ import { Model, model } from 'mongoose'; import { emailTemplateSchema, IEmailTemplate, IEmailTemplateDocument } from './definitions/emailTemplates'; export interface IEmailTemplateModel extends Model { + getEmailTemplate(_id: string): IEmailTemplateDocument; updateEmailTemplate(_id: string, fields: IEmailTemplate): IEmailTemplateDocument; removeEmailTemplate(_id: string): void; } @@ -9,7 +10,20 @@ export interface IEmailTemplateModel extends Model { export const loadClass = () => { class EmailTemplate { /** - * Update email template + * Get email template + */ + public static async getEmailTemplate(_id: string) { + const emailTemplate = await EmailTemplates.findOne({ _id }); + + if (!emailTemplate) { + throw new Error('Email template not found'); + } + + return emailTemplate; + } + + /** + * Updates an email template */ public static async updateEmailTemplate(_id: string, fields: IEmailTemplate) { await EmailTemplates.updateOne({ _id }, { $set: fields }); diff --git a/src/db/models/Engages.ts b/src/db/models/Engages.ts index 4151fe637..bff8c0959 100644 --- a/src/db/models/Engages.ts +++ b/src/db/models/Engages.ts @@ -1,9 +1,38 @@ import { Model, model } from 'mongoose'; -import { Customers } from '.'; +import { ConversationMessages, Conversations, Users } from '.'; +import { replaceEditorAttributes } from '../../data/utils'; +import { getNumberOfVisits } from '../../events'; +import { IBrowserInfo } from './Customers'; +import { IBrandDocument } from './definitions/brands'; +import { IEngageData, IMessageDocument } from './definitions/conversationMessages'; import { ICustomerDocument } from './definitions/customers'; import { engageMessageSchema, IEngageMessage, IEngageMessageDocument } from './definitions/engages'; +import { IIntegrationDocument } from './definitions/integrations'; +import { IUserDocument } from './definitions/users'; + +interface ICheckRulesParams { + rules: IRule[]; + browserInfo: IBrowserInfo; + numberOfVisits?: number; +} + +/* + * Checks individual rule + */ +interface IRule { + value?: string; + kind: string; + condition: string; +} + +interface ICheckRuleParams { + rule: IRule; + browserInfo: IBrowserInfo; + numberOfVisits?: number; +} export interface IEngageMessageModel extends Model { + getEngageMessage(_id: string): IEngageMessageDocument; createEngageMessage(doc: IEngageMessage): Promise; updateEngageMessage(_id: string, doc: IEngageMessage): Promise; @@ -11,34 +40,48 @@ export interface IEngageMessageModel extends Model { engageMessageSetLive(_id: string): Promise; engageMessageSetPause(_id: string): Promise; removeEngageMessage(_id: string): void; - setCustomerIds(_id: string, customers: ICustomerDocument[]): Promise; - - addNewDeliveryReport(_id: string, mailMessageId: string, customerId: string): Promise; - - changeDeliveryReportStatus(headers: IHeaders, status: string): Promise; - + setCustomersCount(_id: string, type: string, count: number): Promise; changeCustomer(newCustomerId: string, customerIds: string[]): Promise; - - removeCustomerEngages(customerId: string): void; - updateStats(engageMessageId: string, stat: string): void; -} - -interface IHeaders { - engageMessageId: string; - customerId: string; - mailId: string; + removeCustomersEngages(customerIds: string[]): Promise<{ n: number; ok: number }>; + + checkRule(params: ICheckRuleParams): boolean; + checkRules(params: ICheckRulesParams): Promise; + createOrUpdateConversationAndMessages(args: { + customer: ICustomerDocument; + integration: IIntegrationDocument; + user: IUserDocument; + engageData: IEngageData; + replacedContent: string; + }): Promise; + createVisitorMessages(params: { + brand: IBrandDocument; + integration: IIntegrationDocument; + customer: ICustomerDocument; + browserInfo: any; + }): Promise; } export const loadClass = () => { class Message { + /** + * Get engage message + */ + public static async getEngageMessage(_id: string) { + const engageMessage = await EngageMessages.findOne({ _id }); + + if (!engageMessage) { + throw new Error('Engage message not found'); + } + + return engageMessage; + } + /** * Create engage message */ public static createEngageMessage(doc: IEngageMessage) { return EngageMessages.create({ ...doc, - deliveryReports: {}, - createdDate: new Date(), }); } @@ -93,60 +136,14 @@ export const loadClass = () => { } /** - * Save matched customer ids - */ - public static async setCustomerIds(_id: string, customers: ICustomerDocument[]) { - await EngageMessages.updateOne({ _id }, { $set: { customerIds: customers.map(customer => customer._id) } }); - - return EngageMessages.findOne({ _id }); - } - - /** - * Add new delivery report + * Save matched customers count */ - public static async addNewDeliveryReport(_id: string, mailMessageId: string, customerId: string) { - await EngageMessages.updateOne( - { _id }, - { - $set: { - [`deliveryReports.${mailMessageId}`]: { - customerId, - status: 'pending', - }, - }, - }, - ); + public static async setCustomersCount(_id: string, type: string, count: number) { + await EngageMessages.updateOne({ _id }, { $set: { [type]: count } }); return EngageMessages.findOne({ _id }); } - /** - * Change delivery report status - */ - public static async changeDeliveryReportStatus(headers: IHeaders, status: string) { - const { engageMessageId, mailId, customerId } = headers; - const customer = await Customers.findOne({ _id: customerId }); - - if (!customer) { - throw new Error('Change Delivery Report Status: Customer not found'); - } - - if (status === 'complaint' || status === 'bounce') { - await Customers.updateOne({ _id: customer._id }, { $set: { doNotDisturb: 'Yes' } }); - } - - await EngageMessages.updateOne( - { _id: engageMessageId }, - { - $set: { - [`deliveryReports.${mailId}.status`]: status, - }, - }, - ); - - return EngageMessages.findOne({ _id: engageMessageId }); - } - /** * Transfers customers' engage messages to another customer */ @@ -176,23 +173,246 @@ export const loadClass = () => { } /** - * Removes customer Engages + * Remove customers engages */ - public static async removeCustomerEngages(customerId: string) { + public static async removeCustomersEngages(customerIds: string[]) { // Removing customer from engage messages await EngageMessages.updateMany( - { messengerReceivedCustomerIds: { $in: [customerId] } }, - { $pull: { messengerReceivedCustomerIds: { $in: [customerId] } } }, + { messengerReceivedCustomerIds: { $in: customerIds } }, + { $pull: { messengerReceivedCustomerIds: { $in: customerIds } } }, ); - return EngageMessages.updateMany({ customerIds: customerId }, { $pull: { customerIds: customerId } }); + return EngageMessages.updateMany({ customerIds }, { $pull: { customerIds } }); } - /** - * Increase engage message stat by 1 + /* + * This function will be used in messagerConnect and it will create conversations + * when visitor messenger connect + */ + public static async createVisitorMessages(params: { + brand: IBrandDocument; + integration: IIntegrationDocument; + customer: ICustomerDocument; + browserInfo: any; + }) { + const { brand, integration, customer, browserInfo } = params; + + // force read previous unread engage messages ============ + await ConversationMessages.forceReadCustomerPreviousEngageMessages(customer._id); + + const messages = await EngageMessages.find({ + 'messenger.brandId': brand._id, + kind: 'visitorAuto', + method: 'messenger', + isLive: true, + }); + + const conversationMessages: IMessageDocument[] = []; + + for (const message of messages) { + const messenger = message.messenger ? message.messenger.toJSON() : {}; + + const user = await Users.findOne({ _id: message.fromUserId }); + + if (!user) { + continue; + } + + // check for rules === + const numberOfVisits = await getNumberOfVisits(customer._id, browserInfo.url); + + const isPassedAllRules = await this.checkRules({ + rules: messenger.rules, + browserInfo, + numberOfVisits, + }); + + // if given visitor is matched with given condition then create + // conversations + if (isPassedAllRules) { + // replace keys in content + const { replacedContent } = await replaceEditorAttributes({ + content: messenger.content, + customer, + user, + }); + + const conversationMessage = await this.createOrUpdateConversationAndMessages({ + customer, + integration, + user, + replacedContent: replacedContent || '', + engageData: { + ...messenger, + content: replacedContent, + engageKind: 'visitorAuto', + messageId: message._id, + fromUserId: message.fromUserId, + }, + }); + + if (conversationMessage) { + // collect created messages + conversationMessages.push(conversationMessage); + + // add given customer to customerIds list + await EngageMessages.updateOne({ _id: message._id }, { $push: { customerIds: customer._id } }); + } + } + } + + return conversationMessages; + } + + /* + * Creates or update conversation & message object using given info + */ + public static async createOrUpdateConversationAndMessages(args: { + customer: ICustomerDocument; + integration: IIntegrationDocument; + user: IUserDocument; + engageData: IEngageData; + replacedContent: string; + }) { + const { customer, integration, user, engageData, replacedContent } = args; + + const prevMessage: IMessageDocument | null = await ConversationMessages.findOne({ + customerId: customer._id, + 'engageData.messageId': engageData.messageId, + }); + + // if previously created conversation for this customer + if (prevMessage) { + const messages = await ConversationMessages.find({ + conversationId: prevMessage.conversationId, + }); + + // leave conversations with responses alone + if (messages.length > 1) { + return null; + } + + // mark as unread again && reset engageData + await ConversationMessages.updateOne({ _id: prevMessage._id }, { $set: { engageData, isCustomerRead: false } }); + + return null; + } + + // create conversation + const conversation = await Conversations.createConversation({ + userId: user._id, + customerId: customer._id, + integrationId: integration._id, + content: replacedContent, + }); + + // create message + return ConversationMessages.createMessage({ + engageData, + conversationId: conversation._id, + userId: user._id, + customerId: customer._id, + content: replacedContent, + }); + } + + /* + * This function determines whether or not current visitor's information + * satisfying given engage message's rules */ - public static async updateStats(engageMessageId: string, stat: string) { - return EngageMessages.updateOne({ _id: engageMessageId }, { $inc: { [`stats.${stat}`]: 1 } }); + public static async checkRules(params: ICheckRulesParams) { + const { rules, browserInfo, numberOfVisits } = params; + + let passedAllRules = true; + + rules.forEach(rule => { + // check individual rule + if (!this.checkRule({ rule, browserInfo, numberOfVisits })) { + passedAllRules = false; + return; + } + }); + + return passedAllRules; + } + + public static checkRule(params: ICheckRuleParams) { + const { rule, browserInfo, numberOfVisits } = params; + const { language, url, city, countryCode } = browserInfo; + const { value, kind, condition } = rule; + const ruleValue: any = value; + + let valueToTest: any; + + if (kind === 'browserLanguage') { + valueToTest = language; + } + + if (kind === 'currentPageUrl') { + valueToTest = url; + } + + if (kind === 'city') { + valueToTest = city; + } + + if (kind === 'country') { + valueToTest = countryCode; + } + + if (kind === 'numberOfVisits') { + valueToTest = numberOfVisits; + } + + // is + if (condition === 'is' && valueToTest !== ruleValue) { + return false; + } + + // isNot + if (condition === 'isNot' && valueToTest === ruleValue) { + return false; + } + + // isUnknown + if (condition === 'isUnknown' && valueToTest) { + return false; + } + + // hasAnyValue + if (condition === 'hasAnyValue' && !valueToTest) { + return false; + } + + // startsWith + if (condition === 'startsWith' && valueToTest && !valueToTest.startsWith(ruleValue)) { + return false; + } + + // endsWith + if (condition === 'endsWith' && valueToTest && !valueToTest.endsWith(ruleValue)) { + return false; + } + + // contains + if (condition === 'contains' && valueToTest && !valueToTest.includes(ruleValue)) { + return false; + } + + // greaterThan + if (condition === 'greaterThan' && valueToTest < parseInt(ruleValue, 10)) { + return false; + } + + if (condition === 'lessThan' && valueToTest > parseInt(ruleValue, 10)) { + return false; + } + + if (condition === 'doesNotContain' && valueToTest.includes(ruleValue)) { + return false; + } + + return true; } } diff --git a/src/db/models/Fields.ts b/src/db/models/Fields.ts index d7b3d3633..b7617bd76 100644 --- a/src/db/models/Fields.ts +++ b/src/db/models/Fields.ts @@ -20,6 +20,26 @@ export interface IOrderInput { order: number; } +export interface ITypedListItem { + field: string; + value: any; + stringValue?: string; + numberValue?: number; + dateValue?: Date; +} + +export const isValidDate = value => { + if ( + (value && validator.isISO8601(value.toString())) || + /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z)/.test(value.toString()) || + value instanceof Date + ) { + return true; + } + + return false; +}; + export interface IFieldModel extends Model { checkIsDefinedByErxes(_id: string): never; createField(doc: IField): Promise; @@ -27,8 +47,10 @@ export interface IFieldModel extends Model { removeField(_id: string): void; updateOrder(orders: IOrderInput[]): Promise; clean(_id: string, _value: string | Date | number): string | Date | number; - cleanMulti(data: { [key: string]: string }): any; - + cleanMulti(data: { [key: string]: any }): any; + generateTypedListFromMap(data: { [key: string]: any }): ITypedListItem[]; + generateTypedItem(field: string, value: string): ITypedListItem; + prepareCustomFieldsData(customFieldsData?: Array<{ field: string; value: any }>): Promise; updateFieldsVisible(_id: string, isVisible: boolean, lastUpdatedUserId: string): Promise; } @@ -160,7 +182,7 @@ export const loadFieldClass = () => { }; // required - if (field.isRequired && !value) { + if (field.isRequired && (!value || !value.toString().trim())) { throwError('required'); } @@ -177,7 +199,7 @@ export const loadFieldClass = () => { // date if (validation === 'date') { - if (!validator.isISO8601(value)) { + if (!isValidDate(value)) { throwError('Invalid date'); } @@ -191,9 +213,8 @@ export const loadFieldClass = () => { /* * Validates multiple fields, fixes values if necessary */ - public static async cleanMulti(data: { [key: string]: string }) { + public static async cleanMulti(data: { [key: string]: any }) { const ids = Object.keys(data); - const fixedValues = {}; // validate individual fields @@ -204,6 +225,48 @@ export const loadFieldClass = () => { return fixedValues; } + public static generateTypedItem(field: string, value: string): ITypedListItem { + let stringValue; + let numberValue; + let dateValue; + + if (value) { + stringValue = value.toString(); + + // number + if (validator.isFloat(value.toString())) { + numberValue = value; + stringValue = null; + } + + if (isValidDate(value)) { + dateValue = value; + stringValue = null; + } + } + + return { field, value, stringValue, numberValue, dateValue }; + } + + public static generateTypedListFromMap(data: { [key: string]: any }): ITypedListItem[] { + const ids = Object.keys(data || {}); + return ids.map(_id => this.generateTypedItem(_id, data[_id])); + } + + public static async prepareCustomFieldsData( + customFieldsData?: Array<{ field: string; value: any }>, + ): Promise { + const result: ITypedListItem[] = []; + + for (const customFieldData of customFieldsData || []) { + await Fields.clean(customFieldData.field, customFieldData.value); + + result.push(Fields.generateTypedItem(customFieldData.field, customFieldData.value)); + } + + return result; + } + /** * Update single field's visible */ diff --git a/src/db/models/Forms.ts b/src/db/models/Forms.ts index 60fd781e8..4c4d1662d 100644 --- a/src/db/models/Forms.ts +++ b/src/db/models/Forms.ts @@ -1,21 +1,54 @@ import * as Random from 'meteor-random'; import { Model, model } from 'mongoose'; +import * as validator from 'validator'; import { FIELD_CONTENT_TYPES } from '../../data/constants'; import { Fields } from './'; -import { formSchema, IForm, IFormDocument } from './definitions/forms'; +import { + formSchema, + formSubmissionSchema, + IForm, + IFormDocument, + IFormSubmission, + IFormSubmissionDocument, +} from './definitions/forms'; + +interface ISubmission { + _id: string; + value: any; + type?: string; + validation?: string; +} + +interface IError { + fieldId: string; + code: string; + text: string; +} export interface IFormModel extends Model { + getForm(_id: string): Promise; generateCode(): string; - createForm(doc: IForm, createdUserId?: string): Promise; + createForm(doc: IForm, createdUserId: string): Promise; - updateForm(_id, { title, description, buttonText, themeColor, callout, rules }: IForm): Promise; + updateForm(_id, { title, description, buttonText }: IForm): Promise; removeForm(_id: string): void; duplicate(_id: string): Promise; + + validate(formId: string, submissions: ISubmission[]): Promise; } -export const loadClass = () => { +export const loadFormClass = () => { class Form { + public static async getForm(_id: string) { + const form = await Forms.findOne({ _id }); + + if (!form) { + throw new Error('Form not found'); + } + + return form; + } /** * Generates a random and unique 6 letter code */ @@ -25,7 +58,7 @@ export const loadClass = () => { do { code = Random.id().substr(0, 6); - foundForm = (await Forms.findOne({ code })) ? true : false; + foundForm = Boolean(await Forms.findOne({ code })); } while (foundForm); return code; @@ -34,29 +67,17 @@ export const loadClass = () => { /** * Creates a form document */ - public static async createForm(doc: IForm, createdUserId?: string) { - if (!createdUserId) { - throw new Error('createdUser must be supplied'); - } - + public static async createForm(doc: IForm, createdUserId: string) { doc.code = await this.generateCode(); - return Forms.create({ - ...doc, - createdDate: new Date(), - createdUserId, - }); + return Forms.create({ ...doc, createdDate: new Date(), createdUserId }); } /** * Updates a form document */ - public static async updateForm(_id: string, { title, description, buttonText, themeColor, callout, rules }: IForm) { - await Forms.updateOne( - { _id }, - { $set: { title, description, buttonText, themeColor, callout, rules } }, - { runValidators: true }, - ); + public static async updateForm(_id: string, doc: IForm) { + await Forms.updateOne({ _id }, { $set: doc }, { runValidators: true }); return Forms.findOne({ _id }); } @@ -75,18 +96,11 @@ export const loadClass = () => { * Duplicates form and form fields of the form */ public static async duplicate(_id: string) { - const form = await Forms.findOne({ _id }); - - if (!form) { - throw new Error('Form not found'); - } + const form = await Forms.getForm(_id); // duplicate form =================== const newForm = await this.createForm( - { - title: `${form.title} duplicated`, - description: form.description, - }, + { title: `${form.title} duplicated`, description: form.description, type: form.type }, form.createdUserId, ); @@ -109,6 +123,57 @@ export const loadClass = () => { return newForm; } + + public static async validate(formId: string, submissions: ISubmission[]) { + const fields = await Fields.find({ contentTypeId: formId }); + const errors: Array<{ fieldId: string; code: string; text: string }> = []; + + for (const field of fields) { + // find submission object by _id + const submission = submissions.find(sub => sub._id === field._id); + + if (!submission) { + continue; + } + + const value = submission.value || ''; + + const type = field.type; + const validation = field.validation; + + // required + if (field.isRequired && !value) { + errors.push({ fieldId: field._id, code: 'required', text: 'Required' }); + } + + if (value) { + // email + if ((type === 'email' || validation === 'email') && !validator.isEmail(value)) { + errors.push({ fieldId: field._id, code: 'invalidEmail', text: 'Invalid email' }); + } + + // phone + if ( + (type === 'phone' || validation === 'phone') && + !/^\d{8,}$/.test(value.replace(/[\s()+\-\.]|ext/gi, '')) + ) { + errors.push({ fieldId: field._id, code: 'invalidPhone', text: 'Invalid phone' }); + } + + // number + if (validation === 'number' && !validator.isNumeric(value.toString())) { + errors.push({ fieldId: field._id, code: 'invalidNumber', text: 'Invalid number' }); + } + + // date + if (validation === 'date' && !validator.isISO8601(value)) { + errors.push({ fieldId: field._id, code: 'invalidDate', text: 'Invalid Date' }); + } + } + } + + return errors; + } } formSchema.loadClass(Form); @@ -116,9 +181,32 @@ export const loadClass = () => { return formSchema; }; -loadClass(); +export interface IFormSubmissionModel extends Model { + createFormSubmission(doc: IFormSubmission): Promise; +} + +export const loadFormSubmissionClass = () => { + class FormSubmission { + /** + * Creates a form document + */ + public static async createFormSubmission(doc: IFormSubmission) { + return FormSubmissions.create(doc); + } + } + + formSubmissionSchema.loadClass(FormSubmission); + + return formSubmissionSchema; +}; + +loadFormClass(); +loadFormSubmissionClass(); // tslint:disable-next-line const Forms = model('forms', formSchema); -export default Forms; +// tslint:disable-next-line +const FormSubmissions = model('form_submissions', formSubmissionSchema); + +export { Forms, FormSubmissions }; diff --git a/src/db/models/GrowthHacks.ts b/src/db/models/GrowthHacks.ts new file mode 100644 index 000000000..20065888d --- /dev/null +++ b/src/db/models/GrowthHacks.ts @@ -0,0 +1,98 @@ +import { Model, model } from 'mongoose'; +import { ActivityLogs } from '.'; +import { fillSearchTextItem, watchItem } from './boardUtils'; +import { growthHackSchema, IGrowthHack, IGrowthHackDocument } from './definitions/growthHacks'; + +export interface IGrowthHackModel extends Model { + getGrowthHack(_id: string): Promise; + createGrowthHack(doc: IGrowthHack): Promise; + updateGrowthHack(_id: string, doc: IGrowthHack): Promise; + watchGrowthHack(_id: string, isAdd: boolean, userId: string): void; + voteGrowthHack(_id: string, isVote: boolean, userId: string): Promise; +} + +export const loadGrowthHackClass = () => { + class GrowthHack { + public static async getGrowthHack(_id: string) { + const growthHack = await GrowthHacks.findOne({ _id }); + + if (!growthHack) { + throw new Error('Growth hack not found'); + } + + return growthHack; + } + + /** + * Create a growth hack + */ + public static async createGrowthHack(doc: IGrowthHack) { + const growthHack = await GrowthHacks.create({ + ...doc, + createdAt: new Date(), + modifiedAt: new Date(), + searchText: fillSearchTextItem(doc), + }); + + // create log + await ActivityLogs.createBoardItemLog({ item: growthHack, contentType: 'growtHack' }); + + return growthHack; + } + + /** + * Update growth hack + */ + public static async updateGrowthHack(_id: string, doc: IGrowthHack) { + const searchText = fillSearchTextItem(doc, await GrowthHacks.getGrowthHack(_id)); + + await GrowthHacks.updateOne({ _id }, { $set: doc, searchText }); + + return GrowthHacks.findOne({ _id }); + } + + /** + * Watch growth hack + */ + public static watchGrowthHack(_id: string, isAdd: boolean, userId: string) { + return watchItem(GrowthHacks, _id, isAdd, userId); + } + + /** + * Vote growth hack + */ + public static async voteGrowthHack(_id: string, isVote: boolean, userId: string) { + const growthHack = await GrowthHack.getGrowthHack(_id); + + let votedUserIds = growthHack.votedUserIds || []; + let voteCount = growthHack.voteCount || 0; + + if (isVote) { + votedUserIds.push(userId); + + voteCount++; + } else { + votedUserIds = votedUserIds.filter(id => id !== userId); + + voteCount--; + } + + const doc = { votedUserIds, voteCount }; + + await GrowthHacks.updateOne({ _id }, { $set: doc }); + + return GrowthHacks.findOne({ _id }); + } + } + + growthHackSchema.loadClass(GrowthHack); + + return growthHackSchema; +}; + +loadGrowthHackClass(); + +// tslint:disable-next-line +const GrowthHacks = model('growth_hacks', growthHackSchema); + +export default GrowthHacks; diff --git a/src/db/models/ImportHistory.ts b/src/db/models/ImportHistory.ts index edb746819..ef899db33 100644 --- a/src/db/models/ImportHistory.ts +++ b/src/db/models/ImportHistory.ts @@ -3,12 +3,26 @@ import { IImportHistory, IImportHistoryDocument, importHistorySchema } from './d import { IUserDocument } from './definitions/users'; export interface IImportHistoryModel extends Model { + getImportHistory(_id: string): Promise; createHistory(doc: IImportHistory, user: IUserDocument): Promise; removeHistory(_id: string): Promise; } export const loadClass = () => { class ImportHistory { + /* + * Get a import history + */ + public static async getImportHistory(_id: string) { + const importHistory = await ImportHistories.findOne({ _id }); + + if (!importHistory) { + throw new Error('Import history not found'); + } + + return importHistory; + } + /* * Create new history */ diff --git a/src/db/models/Integrations.ts b/src/db/models/Integrations.ts index d3598772c..97c73a9b7 100644 --- a/src/db/models/Integrations.ts +++ b/src/db/models/Integrations.ts @@ -1,20 +1,23 @@ -import { Model, model } from 'mongoose'; +import * as momentTz from 'moment-timezone'; +import { Model, model, Query } from 'mongoose'; import 'mongoose-type-email'; -import { ConversationMessages, Conversations, Customers, Forms } from '.'; +import { Brands, ConversationMessages, Conversations, Customers, Forms } from '.'; import { KIND_CHOICES } from './definitions/constants'; import { - IFormData, IIntegration, IIntegrationDocument, + ILeadData, IMessengerData, integrationSchema, IUiOptions, } from './definitions/integrations'; export interface IMessengerIntegration { + kind: string; name: string; brandId: string; languageCode: string; + channelIds?: string[]; } export interface IExternalIntegrationParams { @@ -22,56 +25,131 @@ export interface IExternalIntegrationParams { name: string; brandId: string; accountId: string; + channelIds?: string[]; } +interface IIntegrationBasicInfo { + name: string; + brandId: string; +} + +/** + * Extracts hour & minute from time string formatted as "HH:mm am|pm". + * Time string is defined as constant in modules/settings/integrations/constants. + */ +const getHourAndMinute = (timeString: string) => { + const normalized = timeString.toLowerCase().trim(); + const colon = timeString.indexOf(':'); + let hour = parseInt(normalized.substring(0, colon), 10); + const minute = parseInt(normalized.substring(colon + 1, colon + 3), 10); + + if (normalized.indexOf('pm') !== -1) { + hour += 12; + } + + return { hour, minute }; +}; + +export const isTimeInBetween = (timezone: string, date: Date, startTime: string, closeTime: string): boolean => { + // date of given timezone + const now = momentTz(date).tz(timezone); + + const start = getHourAndMinute(startTime); + const startDate: any = momentTz(now); + + startDate.hours(start.hour); + startDate.minutes(start.minute); + + const end = getHourAndMinute(closeTime); + const closeDate: any = momentTz(now); + + closeDate.hours(end.hour); + closeDate.minutes(end.minute); + + return now.isBetween(startDate, closeDate); +}; + export interface IIntegrationModel extends Model { - generateFormDoc(mainDoc: IIntegration, formData: IFormData): IIntegration; - createIntegration(doc: IIntegration): Promise; - createMessengerIntegration(doc: IIntegration): Promise; + getIntegration(_id: string): IIntegrationDocument; + findIntegrations(query: any, options?: any): Query; + findAllIntegrations(query: any, options?: any): Query; + createIntegration(doc: IIntegration, userId: string): Promise; + createMessengerIntegration(doc: IIntegration, userId: string): Promise; updateMessengerIntegration(_id: string, doc: IIntegration): Promise; saveMessengerAppearanceData(_id: string, doc: IUiOptions): Promise; saveMessengerConfigs(_id: string, messengerData: IMessengerData): Promise; - createFormIntegration(doc: IIntegration): Promise; - updateFormIntegration(_id: string, doc: IIntegration): Promise; - createExternalIntegration(doc: IExternalIntegrationParams): Promise; + createLeadIntegration(doc: IIntegration, userId: string): Promise; + updateLeadIntegration(_id: string, doc: IIntegration): Promise; + createExternalIntegration(doc: IExternalIntegrationParams, userId: string): Promise; removeIntegration(_id: string): void; + updateBasicInfo(_id: string, doc: IIntegrationBasicInfo): Promise; + + getWidgetIntegration(brandCode: string, kind: string, brandObject?: boolean): any; + increaseViewCount(formId: string, get?: boolean): Promise; + increaseContactsGathered(formId: string, get?: boolean): Promise; + isOnline(integration: IIntegrationDocument, now?: Date): boolean; } export const loadClass = () => { class Integration { /** - * Generate form integration data based on the given form data (formData) - * and integration data (mainDoc) + * Retreives integration */ - public static generateFormDoc(mainDoc: IIntegration, formData: IFormData) { - return { - ...mainDoc, - kind: KIND_CHOICES.FORM, - formData, - }; + public static async getIntegration(_id: string) { + const integration = await Integrations.findOne({ _id }); + + if (!integration) { + throw new Error('Integration not found'); + } + + return integration; + } + + /** + * Find integrations + */ + public static findIntegrations(query, options) { + return Integrations.find({ ...query, isActive: { $ne: false } }, options); + } + + public static findAllIntegrations(query: any, options: any) { + return Integrations.find({ ...query }, options); } /** * Create an integration, intended as a private method */ - public static createIntegration(doc: IIntegration) { - return Integrations.create(doc); + public static createIntegration(doc: IIntegration, userId: string) { + return Integrations.create({ ...doc, isActive: true, createdUserId: userId }); } /** * Create a messenger kind integration */ - public static createMessengerIntegration(doc: IMessengerIntegration) { - return this.createIntegration({ - ...doc, - kind: KIND_CHOICES.MESSENGER, - }); + public static async createMessengerIntegration(doc: IMessengerIntegration, userId: string) { + const integration = await Integrations.findOne({ kind: KIND_CHOICES.MESSENGER, brandId: doc.brandId }); + + if (integration) { + throw new Error('Duplicated messenger for single brand'); + } + + return this.createIntegration({ ...doc, kind: KIND_CHOICES.MESSENGER }, userId); } /** * Update messenger integration document */ public static async updateMessengerIntegration(_id: string, doc: IMessengerIntegration) { + const integration = await Integrations.findOne({ + _id: { $ne: _id }, + kind: KIND_CHOICES.MESSENGER, + brandId: doc.brandId, + }); + + if (integration) { + throw new Error('Duplicated messenger for single brand'); + } + await Integrations.updateOne({ _id }, { $set: doc }, { runValidators: true }); return Integrations.findOne({ _id }); @@ -80,10 +158,10 @@ export const loadClass = () => { /** * Save messenger appearance data */ - public static async saveMessengerAppearanceData(_id: string, { color, wallpaper, logo }: IUiOptions) { + public static async saveMessengerAppearanceData(_id: string, { color, wallpaper, logo, textColor }: IUiOptions) { await Integrations.updateOne( { _id }, - { $set: { uiOptions: { color, wallpaper, logo } } }, + { $set: { uiOptions: { color, wallpaper, logo, textColor } } }, { runValdatiors: true }, ); @@ -99,30 +177,44 @@ export const loadClass = () => { } /** - * Create a form kind integration + * Create a lead kind integration */ - public static createFormIntegration({ formData = {}, ...mainDoc }: IIntegration) { - const doc = this.generateFormDoc({ ...mainDoc }, formData); + public static createLeadIntegration({ leadData = {}, ...mainDoc }: IIntegration, userId: string) { + const doc = { ...mainDoc, kind: KIND_CHOICES.LEAD, leadData }; - if (Object.keys(formData || {}).length === 0) { - throw new Error('formData must be supplied'); + if (Object.keys(leadData).length === 0) { + throw new Error('leadData must be supplied'); } - return Integrations.createIntegration(doc); + return Integrations.createIntegration(doc, userId); } /** * Create external integrations like facebook, twitter integration */ - public static createExternalIntegration(doc: IExternalIntegrationParams): Promise { - return Integrations.createIntegration(doc); + public static createExternalIntegration( + doc: IExternalIntegrationParams, + userId: string, + ): Promise { + return Integrations.createIntegration(doc, userId); } /** - * Update form integration + * Update lead integration */ - public static async updateFormIntegration(_id: string, { formData = {}, ...mainDoc }: IIntegration) { - const doc = this.generateFormDoc(mainDoc, formData); + public static async updateLeadIntegration(_id: string, { leadData = {}, ...mainDoc }: IIntegration) { + const prevEntry = await Integrations.getIntegration(_id); + const prevLeadData: ILeadData = prevEntry.leadData || {}; + + const doc = { + ...mainDoc, + kind: KIND_CHOICES.LEAD, + leadData: { + ...leadData, + viewCount: prevLeadData.viewCount, + contactsGathered: prevLeadData.contactsGathered, + }, + }; await Integrations.updateOne({ _id }, { $set: doc }, { runValidators: true }); @@ -143,26 +235,132 @@ export const loadClass = () => { const conversations = await Conversations.find({ integrationId: _id }, { _id: true }); const conversationIds = conversations.map(conv => conv._id); - await ConversationMessages.deleteMany({ - conversationId: { $in: conversationIds }, - }); + await ConversationMessages.deleteMany({ conversationId: { $in: conversationIds } }); + await Conversations.deleteMany({ integrationId: _id }); // Remove customers ================== const customers = await Customers.find({ integrationId: _id }); const customerIds = customers.map(cus => cus._id); - for (const customerId of customerIds) { - await Customers.removeCustomer(customerId); - } + await Customers.removeCustomers(customerIds); - // Remove form & fields + // Remove form if (integration.formId) { await Forms.removeForm(integration.formId); } return Integrations.deleteMany({ _id }); } + + public static async updateBasicInfo(_id: string, doc: IIntegrationBasicInfo) { + const integration = await Integrations.findOne({ _id }); + + if (!integration) { + throw new Error('Integration not found'); + } + + await Integrations.updateOne({ _id }, { $set: doc }); + + return Integrations.findOne({ _id }); + } + + public static async getWidgetIntegration(brandCode: string, kind: string, brandObject = false) { + const brand = await Brands.findOne({ code: brandCode }); + + if (!brand) { + throw new Error('Brand not found'); + } + + const integration = await Integrations.findOne({ brandId: brand._id, kind }); + + if (brandObject) { + return { integration, brand }; + } + + return integration; + } + + public static async increaseViewCount(formId: string, get = false) { + const response = await Integrations.updateOne( + { formId, leadData: { $exists: true } }, + { $inc: { 'leadData.viewCount': 1 } }, + ); + return get ? Integrations.findOne({ formId }) : response; + } + + /* + * Increase form submitted count + */ + public static async increaseContactsGathered(formId: string, get = false) { + const response = await Integrations.updateOne( + { formId, leadData: { $exists: true } }, + { $inc: { 'leadData.contactsGathered': 1 } }, + ); + return get ? Integrations.findOne({ formId }) : response; + } + + public static isOnline(integration: IIntegrationDocument, now = new Date()) { + const daysAsString = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; + + const isWeekday = (d: string): boolean => { + return ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'].includes(d); + }; + + const isWeekend = (d: string): boolean => { + return ['saturday', 'sunday'].includes(d); + }; + + if (!integration.messengerData) { + return false; + } + + const { messengerData } = integration; + const { availabilityMethod, onlineHours = [], timezone } = messengerData; + const timezoneString = timezone || ''; + + /* + * Manual: We can determine state from isOnline field value when method is manual + */ + if (availabilityMethod === 'manual') { + return messengerData.isOnline; + } + + /* + * Auto + */ + const day = daysAsString[now.getDay()]; + + // check by everyday config + const everydayConf = onlineHours.find(c => c.day === 'everyday'); + + if (everydayConf) { + return isTimeInBetween(timezoneString, now, everydayConf.from || '', everydayConf.to || ''); + } + + // check by weekdays config + const weekdaysConf = onlineHours.find(c => c.day === 'weekdays'); + + if (weekdaysConf && isWeekday(day)) { + return isTimeInBetween(timezoneString, now, weekdaysConf.from || '', weekdaysConf.to || ''); + } + + // check by weekends config + const weekendsConf = onlineHours.find(c => c.day === 'weekends'); + + if (weekendsConf && isWeekend(day)) { + return isTimeInBetween(timezoneString, now, weekendsConf.from || '', weekendsConf.to || ''); + } + + // check by regular day config + const dayConf = onlineHours.find(c => c.day === day); + + if (dayConf) { + return isTimeInBetween(timezoneString, now, dayConf.from || '', dayConf.to || ''); + } + + return false; + } } integrationSchema.loadClass(Integration); diff --git a/src/db/models/InternalNotes.ts b/src/db/models/InternalNotes.ts index a89cdcc46..612755e45 100644 --- a/src/db/models/InternalNotes.ts +++ b/src/db/models/InternalNotes.ts @@ -1,10 +1,10 @@ import { Model, model } from 'mongoose'; -import { ActivityLogs } from '.'; import { ACTIVITY_CONTENT_TYPES } from './definitions/constants'; import { IInternalNote, IInternalNoteDocument, internalNoteSchema } from './definitions/internalNotes'; import { IUserDocument } from './definitions/users'; export interface IInternalNoteModel extends Model { + getInternalNote(_id: string): Promise; createInternalNote( { contentType, contentTypeId, ...fields }: IInternalNote, user: IUserDocument, @@ -16,14 +16,23 @@ export interface IInternalNoteModel extends Model { changeCustomer(newCustomerId: string, customerIds: string[]): Promise; - removeCustomerInternalNotes(customerId: string): void; - removeCompanyInternalNotes(companyId: string): void; + removeCustomersInternalNotes(customerIds: string[]): Promise<{ n: number; ok: number }>; + removeCompaniesInternalNotes(companyIds: string[]): void; changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; } export const loadClass = () => { class InternalNote { + public static async getInternalNote(_id: string) { + const internalNote = await InternalNotes.findOne({ _id }); + + if (!internalNote) { + throw new Error('Internal note not found'); + } + + return internalNote; + } /* * Create new internalNote */ @@ -35,13 +44,10 @@ export const loadClass = () => { contentType, contentTypeId, createdUserId: user._id, - createdDate: new Date(), + createdAt: Date.now(), ...fields, }); - // create log - await ActivityLogs.createInternalNoteLog(internalNote); - return internalNote; } @@ -88,24 +94,24 @@ export const loadClass = () => { } /** - * Removing customers' internal notes + * Remove customers' internal notes */ - public static async removeCustomerInternalNotes(customerId: string) { + public static async removeCustomersInternalNotes(customerIds: string) { // Removing every internal notes of customer return InternalNotes.deleteMany({ contentType: ACTIVITY_CONTENT_TYPES.CUSTOMER, - contentTypeId: customerId, + contentTypeId: { $in: customerIds }, }); } /** - * Removing companies' internal notes + * Remove companies' internal notes */ - public static async removeCompanyInternalNotes(companyId: string) { + public static async removeCompaniesInternalNotes(companyIds: string[]) { // Removing every internal notes of company return InternalNotes.deleteMany({ contentType: ACTIVITY_CONTENT_TYPES.COMPANY, - contentTypeId: companyId, + contentTypeId: { $in: companyIds }, }); } diff --git a/src/db/models/KnowledgeBase.ts b/src/db/models/KnowledgeBase.ts index f61132a4d..45465e301 100644 --- a/src/db/models/KnowledgeBase.ts +++ b/src/db/models/KnowledgeBase.ts @@ -18,13 +18,25 @@ export interface IArticleCreate extends IArticle { } export interface IArticleModel extends Model { + getArticle(_id: string): Promise; createDoc({ categoryIds, ...docFields }: IArticleCreate, userId?: string): Promise; updateDoc(_id: string, { categoryIds, ...docFields }: IArticleCreate, userId?: string): Promise; removeDoc(_id: string): void; + incReactionCount(articleId: string, reactionChoice): void; } export const loadArticleClass = () => { class Article { + public static async getArticle(_id: string) { + const article = await KnowledgeBaseArticles.findOne({ _id }); + + if (!article) { + throw new Error('Knowledge base article not found'); + } + + return article; + } + /** * Create KnowledgeBaseArticle document */ @@ -79,11 +91,7 @@ export const loadArticleClass = () => { }, ); - const article = await KnowledgeBaseArticles.findOne({ _id }); - - if (!article) { - throw new Error('Article not found'); - } + const article = await KnowledgeBaseArticles.getArticle(_id); // add new article id to categories's articleIds field if ((categoryIds || []).length > 0) { @@ -114,6 +122,23 @@ export const loadArticleClass = () => { public static removeDoc(_id: string) { return KnowledgeBaseArticles.deleteOne({ _id }); } + + /* + * Increase form view count + */ + public static async incReactionCount(articleId: string, reactionChoice: string) { + const article = await KnowledgeBaseArticles.findOne({ _id: articleId }); + + if (!article) { + throw new Error('Article not found'); + } + + const reactionCounts = article.reactionCounts || {}; + + reactionCounts[reactionChoice] = (reactionCounts[reactionChoice] || 0) + 1; + + await KnowledgeBaseArticles.updateOne({ _id: articleId }, { $set: { reactionCounts } }); + } } articleSchema.loadClass(Article); @@ -127,6 +152,7 @@ export interface ICategoryCreate extends ICategory { } export interface ICategoryModel extends Model { + getCategory(_id: string): Promise; createDoc({ topicIds, ...docFields }: ICategoryCreate, userId?: string): Promise; updateDoc(_id: string, { topicIds, ...docFields }: ICategoryCreate, userId?: string): Promise; removeDoc(categoryId: string): void; @@ -134,6 +160,16 @@ export interface ICategoryModel extends Model { export const loadCategoryClass = () => { class Category { + public static async getCategory(_id: string) { + const category = await KnowledgeBaseCategories.findOne({ _id }); + + if (!category) { + throw new Error('Knowledge base category not found'); + } + + return category; + } + /** * Create KnowledgeBaseCategory document */ @@ -186,11 +222,7 @@ export const loadCategoryClass = () => { }, ); - const category = await KnowledgeBaseCategories.findOne({ _id }); - - if (!category) { - throw new Error('Category not found'); - } + const category = await KnowledgeBaseCategories.getCategory(_id); if ((topicIds || []).length > 0) { const topics = await KnowledgeBaseTopics.find({ _id: { $in: topicIds } }); @@ -222,9 +254,7 @@ export const loadCategoryClass = () => { throw new Error('Category not found'); } - if (category.articleIds) { - await KnowledgeBaseArticles.deleteMany({ _id: { $in: category.articleIds } }); - } + await KnowledgeBaseArticles.deleteMany({ _id: { $in: category.articleIds || [] } }); return KnowledgeBaseCategories.deleteOne({ _id }); } @@ -236,6 +266,7 @@ export const loadCategoryClass = () => { }; export interface ITopicModel extends Model { + getTopic(_id: string): Promise; createDoc(docFields: ITopic, userId?: string): Promise; updateDoc(_id: string, docFields: ITopic, userId?: string): Promise; removeDoc(_id: string): void; @@ -243,6 +274,15 @@ export interface ITopicModel extends Model { export const loadTopicClass = () => { class Topic { + public static async getTopic(_id: string) { + const topic = await KnowledgeBaseTopics.findOne({ _id }); + + if (!topic) { + throw new Error('Knowledge base topic not found'); + } + + return topic; + } /** * Create KnowledgeBaseTopic document */ diff --git a/src/db/models/MessengerApps.ts b/src/db/models/MessengerApps.ts index 80c65670a..662dd8514 100644 --- a/src/db/models/MessengerApps.ts +++ b/src/db/models/MessengerApps.ts @@ -2,14 +2,32 @@ import { Model, model } from 'mongoose'; import { IMessengerApp, IMessengerAppDocument, messengerAppSchema } from './definitions/messengerApps'; export interface IMessengerAppModel extends Model { + getApp(_id: string): Promise; createApp(doc: IMessengerApp): Promise; + updateApp(_id: string, doc: IMessengerApp): Promise; } export const loadClass = () => { class MessengerApp { + public static async getApp(_id: string) { + const messengerApp = await MessengerApps.findOne({ _id }); + + if (!messengerApp) { + throw new Error('Messenger app not found'); + } + + return messengerApp; + } + public static async createApp(doc: IMessengerApp) { return MessengerApps.create(doc); } + + public static async updateApp(_id: string, doc: IMessengerApp) { + await MessengerApps.updateOne({ _id }, { $set: doc }, { runValidators: true }); + + return MessengerApps.findOne({ _id }); + } } messengerAppSchema.loadClass(MessengerApp); diff --git a/src/db/models/Notifications.ts b/src/db/models/Notifications.ts index 75e9ba555..8e29248b5 100644 --- a/src/db/models/Notifications.ts +++ b/src/db/models/Notifications.ts @@ -12,6 +12,7 @@ export interface INotificationModel extends Model { markAsRead(ids: string[], userId?: string): void; createNotification(doc: INotification, createdUser?: IUserDocument | string): Promise; updateNotification(_id: string, doc: INotification): Promise; + checkIfRead(userId: string, contentTypeId: string): Promise; removeNotification(_id: string): void; } @@ -23,17 +24,26 @@ export const loadNotificationClass = () => { public static markAsRead(ids: string[], userId: string) { let selector: any = { receiver: userId }; - if (ids) { + if (ids && ids.length > 0) { selector = { _id: { $in: ids } }; } return Notifications.updateMany(selector, { $set: { isRead: true } }, { multi: true }); } + /** + * Check if user has read notification + */ + public static async checkIfRead(userId, contentTypeId) { + const notification = await Notifications.findOne({ isRead: false, receiver: userId, contentTypeId }); + + return notification ? false : true; + } + /** * Create a notification */ - public static async createNotification(doc: INotification, createdUser?: IUserDocument | string) { + public static async createNotification(doc: INotification, createdUserId: string) { // if receiver is configured to get this notification const config = await NotificationConfigurations.findOne({ user: doc.receiver, @@ -45,7 +55,7 @@ export const loadNotificationClass = () => { throw new Error('Configuration does not exist'); } - return Notifications.create({ ...doc, createdUser }); + return Notifications.create({ ...doc, createdUser: createdUserId }); } /** diff --git a/src/db/models/Permissions.ts b/src/db/models/Permissions.ts index bdc1bedf1..b61f79b6e 100644 --- a/src/db/models/Permissions.ts +++ b/src/db/models/Permissions.ts @@ -14,13 +14,15 @@ import { export interface IPermissionModel extends Model { createPermission(doc: IPermissionParams): Promise; removePermission(ids: string[]): Promise; + getPermission(id: string): Promise; } export interface IUserGroupModel extends Model { - generateDocs(groupId: string, memberIds?: string[]): Array<{ userId: string; groupId: string }>; + getGroup(_id: string): Promise; createGroup(doc: IUserGroup, memberIds?: string[]): Promise; updateGroup(_id: string, doc: IUserGroup, memberIds?: string[]): Promise; removeGroup(_id: string): Promise; + copyGroup(sourceGroupId: string, memberIds?: string[]): Promise; } export const permissionLoadClass = () => { @@ -33,10 +35,6 @@ export const permissionLoadClass = () => { public static async createPermission(doc: IPermissionParams) { const permissions: IPermissionDocument[] = []; - if (!doc.actions) { - throw new Error('Actions not found'); - } - for (const action of doc.actions) { if (!actionsMap[action]) { throw new Error('Invalid data'); @@ -105,6 +103,16 @@ export const permissionLoadClass = () => { return Permissions.deleteMany({ _id: { $in: ids } }); } + + public static async getPermission(id: string) { + const permission = await Permissions.findOne({ _id: id }); + + if (!permission) { + throw new Error('Permission not found'); + } + + return permission; + } } permissionSchema.loadClass(Permission); @@ -114,6 +122,16 @@ export const permissionLoadClass = () => { export const userGroupLoadClass = () => { class UserGroup { + public static async getGroup(_id: string) { + const userGroup = await UsersGroups.findOne({ _id }); + + if (!userGroup) { + throw new Error('User group not found'); + } + + return userGroup; + } + /** * Create a group */ @@ -132,7 +150,7 @@ export const userGroupLoadClass = () => { // remove groupId from old members await Users.updateMany({ groupIds: { $in: [_id] } }, { $pull: { groupIds: { $in: [_id] } } }); - await UsersGroups.update({ _id }, { $set: doc }); + await UsersGroups.updateOne({ _id }, { $set: doc }); // add groupId to new members await Users.updateMany({ _id: { $in: memberIds || [] } }, { $push: { groupIds: _id } }); @@ -152,8 +170,40 @@ export const userGroupLoadClass = () => { throw new Error(`Group not found with id ${_id}`); } + await Users.updateMany({ groupIds: { $in: [_id] } }, { $pull: { groupIds: { $in: [_id] } } }); + + await Permissions.remove({ groupId: groupObj._id }); + return groupObj.remove(); } + + public static async copyGroup(sourceGroupId: string, memberIds?: string[]) { + const sourceGroup = await UsersGroups.getGroup(sourceGroupId); + + const nameCount = await UsersGroups.countDocuments({ name: new RegExp(`${sourceGroup.name}`, 'i') }); + + const clone = await UsersGroups.createGroup( + { + name: `${sourceGroup.name}-copied-${nameCount}`, + description: `${sourceGroup.description}-copied`, + }, + memberIds, + ); + + const permissions = await Permissions.find({ groupId: sourceGroupId }); + + for (const perm of permissions) { + await Permissions.create({ + groupId: clone._id, + action: perm.action, + module: perm.module, + requiredActions: perm.requiredActions, + allowed: perm.allowed, + }); + } + + return clone; + } } userGroupSchema.loadClass(UserGroup); diff --git a/src/db/models/PipelineLabels.ts b/src/db/models/PipelineLabels.ts new file mode 100644 index 000000000..f761e3070 --- /dev/null +++ b/src/db/models/PipelineLabels.ts @@ -0,0 +1,151 @@ +import { Model, model } from 'mongoose'; +import { Pipelines } from '.'; +import { getCollection } from './boardUtils'; +import { IPipelineLabel, IPipelineLabelDocument, pipelineLabelSchema } from './definitions/pipelineLabels'; + +interface IFilter extends IPipelineLabel { + _id?: any; +} + +interface ILabelObjectParams { + labelIds: string[]; + targetId: string; + collection: any; +} + +export interface IPipelineLabelModel extends Model { + getPipelineLabel(_id: string): Promise; + createPipelineLabel(doc: IPipelineLabel): Promise; + updatePipelineLabel(_id: string, doc: IPipelineLabel): Promise; + removePipelineLabel(_id: string): void; + labelsLabel(pipelineId: string, targetId: string, labelIds: string[]): void; + validateUniqueness(filter: IFilter, _id?: string): Promise; + labelObject(params: ILabelObjectParams): void; +} + +export const loadPipelineLabelClass = () => { + class PipelineLabel { + public static async getPipelineLabel(_id: string) { + const pipelineLabel = await PipelineLabels.findOne({ _id }); + + if (!pipelineLabel) { + throw new Error('Label not found'); + } + + return pipelineLabel; + } + /* + * Validates label uniquness + */ + public static async validateUniqueness(filter: IFilter, _id?: string): Promise { + if (_id) { + filter._id = { $ne: _id }; + } + + if (await PipelineLabels.findOne(filter)) { + return false; + } + + return true; + } + + /* + * Common helper for objects like deal, task, ticket and growth hack etc ... + */ + + public static async labelObject({ labelIds, targetId, collection }: ILabelObjectParams) { + const prevLabelsCount = await PipelineLabels.find({ + _id: { $in: labelIds }, + }).countDocuments(); + + if (prevLabelsCount !== labelIds.length) { + throw new Error('Label not found'); + } + + await collection.updateMany({ _id: targetId }, { $set: { labelIds } }, { multi: true }); + } + + /** + * Create a pipeline label + */ + public static async createPipelineLabel(doc: IPipelineLabel) { + const filter: IFilter = { + name: doc.name, + pipelineId: doc.pipelineId, + colorCode: doc.colorCode, + }; + + const isUnique = await PipelineLabels.validateUniqueness(filter); + + if (!isUnique) { + throw new Error('Label duplicated'); + } + + return PipelineLabels.create(doc); + } + + /** + * Update pipeline label + */ + public static async updatePipelineLabel(_id: string, doc: IPipelineLabel) { + const isUnique = await PipelineLabels.validateUniqueness({ ...doc }, _id); + + if (!isUnique) { + throw new Error('Label duplicated'); + } + + await PipelineLabels.updateOne({ _id }, { $set: doc }); + + return PipelineLabels.findOne({ _id }); + } + + /** + * Remove pipeline label + */ + public static async removePipelineLabel(_id: string) { + const pipelineLabel = await PipelineLabels.findOne({ _id }); + + if (!pipelineLabel) { + throw new Error('Label not found'); + } + + const pipeline = await Pipelines.getPipeline(pipelineLabel.pipelineId); + + const collection = getCollection(pipeline.type); + + // delete labelId from collection that used labelId + await collection.updateMany( + { labelIds: { $in: [pipelineLabel._id] } }, + { $pull: { labelIds: pipelineLabel._id } }, + ); + + return PipelineLabels.deleteOne({ _id }); + } + + /** + * Attach a label + */ + public static async labelsLabel(pipelineId: string, targetId: string, labelIds: string[]) { + const pipeline = await Pipelines.getPipeline(pipelineId); + + const collection = getCollection(pipeline.type); + + await PipelineLabels.labelObject({ + labelIds, + targetId, + collection, + }); + } + } + + pipelineLabelSchema.loadClass(PipelineLabel); + + return pipelineLabelSchema; +}; + +loadPipelineLabelClass(); + +// tslint:disable-next-line +const PipelineLabels = model('pipeline_labels', pipelineLabelSchema); + +export default PipelineLabels; diff --git a/src/db/models/PipelineTemplates.ts b/src/db/models/PipelineTemplates.ts new file mode 100644 index 000000000..2629a4762 --- /dev/null +++ b/src/db/models/PipelineTemplates.ts @@ -0,0 +1,138 @@ +import { Model, model } from 'mongoose'; +import { Forms } from '.'; +import { + IPipelineTemplateDocument, + IPipelineTemplateStage, + pipelineTemplateSchema, +} from './definitions/pipelineTemplates'; + +interface IDoc { + name: string; + description?: string; + type: string; +} + +export const getDuplicatedStages = async ({ + templateId, + pipelineId, + type, +}: { + templateId: string; + pipelineId?: string; + type?: string; +}) => { + const template = await PipelineTemplates.getPipelineTemplate(templateId); + + const stages: any[] = []; + + for (const stage of template.stages) { + const duplicated = await Forms.duplicate(stage.formId); + + stages.push({ + _id: Math.random().toString(), + name: stage.name, + pipelineId, + type, + formId: duplicated._id, + }); + } + + return stages; +}; + +export interface IPipelineTemplateModel extends Model { + getPipelineTemplate(_id: string): Promise; + createPipelineTemplate(doc: IDoc, stages: IPipelineTemplateStage[]): Promise; + updatePipelineTemplate(_id: string, doc: IDoc, stages: IPipelineTemplateStage[]): Promise; + removePipelineTemplate(_id: string): void; + duplicatePipelineTemplate(_id: string): Promise; +} + +export const loadPipelineTemplateClass = () => { + class PipelineTemplate { + /* + * Get a pipeline template + */ + public static async getPipelineTemplate(_id: string) { + const pipelineTemplate = await PipelineTemplates.findOne({ _id }); + + if (!pipelineTemplate) { + throw new Error('Pipeline template not found'); + } + + return pipelineTemplate; + } + + /** + * Create a pipeline template + */ + public static async createPipelineTemplate(doc: IDoc, stages: IPipelineTemplateStage[]) { + const orderedStages = stages.map((stage, index) => ({ ...stage, index })); + + return PipelineTemplates.create({ ...doc, stages: orderedStages }); + } + + /** + * Update pipeline template + */ + public static async updatePipelineTemplate(_id: string, doc: IDoc, stages: IPipelineTemplateStage[]) { + const orderedStages = stages.map((stage, index) => ({ ...stage, index })); + + await PipelineTemplates.updateOne({ _id }, { $set: { ...doc, stages: orderedStages } }); + + return PipelineTemplates.findOne({ _id }); + } + + /** + * Duplicate pipeline template + */ + public static async duplicatePipelineTemplate(_id: string) { + const pipelineTemplate = await PipelineTemplates.findOne({ _id }).lean(); + + if (!pipelineTemplate) { + throw new Error('Pipeline template not found'); + } + + const duplicated: IDoc = { + name: `${pipelineTemplate.name} duplicated`, + description: pipelineTemplate.description || '', + type: pipelineTemplate.type, + }; + + const stages: any[] = await getDuplicatedStages({ templateId: pipelineTemplate._id }); + + return PipelineTemplates.createPipelineTemplate(duplicated, stages); + } + + /** + * Remove pipeline template + */ + public static async removePipelineTemplate(_id: string) { + const pipelineTemplate = await PipelineTemplates.findOne({ _id }); + + if (!pipelineTemplate) { + throw new Error('Pipeline template not found'); + } + + for (const stage of pipelineTemplate.stages) { + await Forms.removeForm(stage.formId); + } + + return PipelineTemplates.deleteOne({ _id }); + } + } + + pipelineTemplateSchema.loadClass(PipelineTemplate); + + return pipelineTemplateSchema; +}; + +loadPipelineTemplateClass(); + +// tslint:disable-next-line +const PipelineTemplates = model( + 'pipeline_templates', + pipelineTemplateSchema, +); + +export default PipelineTemplates; diff --git a/src/db/models/Products.ts b/src/db/models/Products.ts index aa069f19e..59dce1b5c 100644 --- a/src/db/models/Products.ts +++ b/src/db/models/Products.ts @@ -1,19 +1,49 @@ import { Model, model } from 'mongoose'; -import { Deals } from '.'; -import { IProduct, IProductDocument, productSchema } from './definitions/deals'; +import { Deals, Fields } from '.'; +import { + IProduct, + IProductCategory, + IProductCategoryDocument, + IProductDocument, + productCategorySchema, + productSchema, +} from './definitions/deals'; export interface IProductModel extends Model { + getProduct(selector: any): Promise; createProduct(doc: IProduct): Promise; updateProduct(_id: string, doc: IProduct): Promise; - removeProduct(_id: string): void; + removeProducts(_ids: string[]): Promise<{ n: number; ok: number }>; } -export const loadClass = () => { +export const loadProductClass = () => { class Product { + /** + * + * Get Product Cagegory + */ + + public static async getProduct(selector: any) { + const product = await Products.findOne(selector); + + if (!product) { + throw new Error('Product not found'); + } + + return product; + } + /** * Create a product */ public static async createProduct(doc: IProduct) { + if (doc.categoryCode) { + const category = await ProductCategories.getProductCatogery({ code: doc.categoryCode }); + doc.categoryId = category._id; + } + + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); + return Products.create(doc); } @@ -21,41 +51,150 @@ export const loadClass = () => { * Update Product */ public static async updateProduct(_id: string, doc: IProduct) { + if (doc.customFieldsData) { + // clean custom field values + doc.customFieldsData = await Fields.prepareCustomFieldsData(doc.customFieldsData); + } + await Products.updateOne({ _id }, { $set: doc }); return Products.findOne({ _id }); } /** - * Remove Product + * Remove products */ - public static async removeProduct(_id: string) { - const product = await Products.findOne({ _id }); + public static async removeProducts(_ids: string[]) { + const deals = await Deals.find( + { + 'productsData.productId': { $in: _ids }, + }, + { name: 1 }, + ).lean(); - if (!product) { - throw new Error('Product not found'); + if (deals.length > 0) { + const names = deals.map(deal => deal.name); + + throw new Error(`Can not remove products. Following deals are used ${names.join(',')}`); } - const count = await Deals.find({ - 'productsData.productId': { $in: [_id] }, - }).countDocuments(); + return Products.deleteMany({ _id: { $in: _ids } }); + } + } + + productSchema.loadClass(Product); + + return productSchema; +}; + +export interface IProductCategoryModel extends Model { + getProductCatogery(selector: any): Promise; + createProductCategory(doc: IProductCategory): Promise; + updateProductCategory(_id: string, doc: IProductCategory): Promise; + removeProductCategory(_id: string): void; +} + +export const loadProductCategoryClass = () => { + class ProductCategory { + /** + * + * Get Product Cagegory + */ + + public static async getProductCatogery(selector: any) { + const productCategory = await ProductCategories.findOne(selector); + + if (!productCategory) { + throw new Error('Product & service category not found'); + } + + return productCategory; + } + + /** + * Create a product categorys + */ + public static async createProductCategory(doc: IProductCategory) { + const parentCategory = await ProductCategories.findOne({ _id: doc.parentId }).lean(); + + // Generatingg order + doc.order = await this.generateOrder(parentCategory, doc); + + return ProductCategories.create(doc); + } + + /** + * Update Product category + */ + public static async updateProductCategory(_id: string, doc: IProductCategory) { + const parentCategory = await ProductCategories.findOne({ _id: doc.parentId }).lean(); + + if (parentCategory && parentCategory.parentId === _id) { + throw new Error('Cannot change category'); + } + + // Generatingg order + doc.order = await this.generateOrder(parentCategory, doc); + + const productCategory = await ProductCategories.getProductCatogery({ _id }); + + const childCategories = await ProductCategories.find({ + $and: [{ order: { $regex: new RegExp(productCategory.order, 'i') } }, { _id: { $ne: _id } }], + }); + + await ProductCategories.updateOne({ _id }, { $set: doc }); + + // updating child categories order + childCategories.forEach(async category => { + let order = category.order; + + order = order.replace(productCategory.order, doc.order); + + await ProductCategories.updateOne({ _id: category._id }, { $set: { order } }); + }); + + return ProductCategories.findOne({ _id }); + } + + /** + * Remove Product category + */ + public static async removeProductCategory(_id: string) { + await ProductCategories.getProductCatogery({ _id }); + + let count = await Products.countDocuments({ categoryId: _id }); + count += await ProductCategories.countDocuments({ parentId: _id }); if (count > 0) { - throw new Error("Can't remove a product"); + throw new Error("Can't remove a product category"); } - return Products.deleteOne({ _id }); + return ProductCategories.deleteOne({ _id }); + } + + /** + * Generating order + */ + public static async generateOrder(parentCategory: IProductCategory, doc: IProductCategory) { + const order = parentCategory ? `${parentCategory.order}/${doc.name}${doc.code}` : `${doc.name}${doc.code}`; + + return order; } } - productSchema.loadClass(Product); + productCategorySchema.loadClass(ProductCategory); - return productSchema; + return productCategorySchema; }; -loadClass(); +loadProductClass(); +loadProductCategoryClass(); // tslint:disable-next-line -const Products = model('products', productSchema); +export const Products = model('products', productSchema); -export default Products; +// tslint:disable-next-line +export const ProductCategories = model( + 'product_categories', + productCategorySchema, +); diff --git a/src/db/models/ResponseTemplates.ts b/src/db/models/ResponseTemplates.ts index 6c6d8b389..0fbb5ede5 100644 --- a/src/db/models/ResponseTemplates.ts +++ b/src/db/models/ResponseTemplates.ts @@ -2,12 +2,25 @@ import { Model, model } from 'mongoose'; import { IResponseTemplate, IResponseTemplateDocument, responseTemplateSchema } from './definitions/responseTemplates'; export interface IResponseTemplateModel extends Model { + getResponseTemplate(_id: string): Promise; updateResponseTemplate(_id: string, fields: IResponseTemplate): Promise; removeResponseTemplate(_id: string): void; } export const loadClass = () => { class ResponseTemplate { + /* + * Get a Pipeline template + */ + public static async getResponseTemplate(_id: string) { + const responseTemplate = await ResponseTemplates.findOne({ _id }); + + if (!responseTemplate) { + throw new Error('Response template not found'); + } + + return responseTemplate; + } /** * Update response template */ diff --git a/src/db/models/Robot.ts b/src/db/models/Robot.ts new file mode 100644 index 000000000..a31e6d3ec --- /dev/null +++ b/src/db/models/Robot.ts @@ -0,0 +1,202 @@ +import { Model, model } from 'mongoose'; +import { Companies, Customers } from '.'; +import { + IOnboardingHistoryDocument, + IRobotEntryDocument, + onboardingHistorySchema, + robotEntrySchema, +} from './definitions/robot'; +import { IUserDocument } from './definitions/users'; + +// entries ========================== +export interface IRobotEntryModel extends Model { + createEntry(data): Promise; + markAsNotified(_id: string): Promise; + updateOrCreate(action: string, data): Promise; +} + +export const loadClass = () => { + class RobotEntry { + public static async updateOrCreate(action: string, data): Promise { + return RobotEntries.findOneAndUpdate({ action, data }, { isNotified: false }, { new: true, upsert: true }); + } + + public static async markAsNotified(_id: string): Promise { + return RobotEntries.updateOne({ _id }, { $set: { isNotified: true } }); + } + + public static async createEntry(data): Promise { + if (data.action === 'mergeCustomers') { + const customerIds = data.customerIds; + const randomCustomer = await Customers.findOne({ _id: { $in: customerIds } }).lean(); + + if (randomCustomer) { + delete randomCustomer._id; + await Customers.mergeCustomers(customerIds, randomCustomer); + return RobotEntries.create({ action: 'mergeCustomers', data: { customerIds } }); + } + } + + if (data.action === 'fillCompanyInfo') { + const results = data.results; + + const parent = await RobotEntries.create({ action: 'fillCompanyInfo', data: { count: results.length } }); + + for (const result of results) { + const { _id, modifier } = result; + + await Companies.updateOne({ _id }, { $set: modifier }); + return RobotEntries.create({ action: 'fillCompanyInfo', parentId: parent._id, data: { _id, modifier } }); + } + + return parent; + } + + if (data.action === 'customerScoring') { + const { scoreMap } = data; + + if (!scoreMap || scoreMap.length === 0) { + return undefined; + } + + const modifier = scoreMap.map(entry => ({ + updateOne: { + filter: { + _id: entry._id, + }, + update: { + $set: { profileScore: entry.score }, + }, + }, + })); + + await Customers.bulkWrite(modifier); + + return RobotEntries.create({ action: 'customerScoring', data: { scoreMap } }); + } + + if (data.action === 'channelsWithoutIntegration') { + return RobotEntries.updateOrCreate('channelsWithoutIntegration', { channelIds: data.channelIds }); + } + + if (data.action === 'channelsWithoutMembers') { + return RobotEntries.updateOrCreate('channelsWithoutMembers', { channelIds: data.channelIds }); + } + + if (data.action === 'brandsWithoutIntegration') { + return RobotEntries.updateOrCreate('brandsWithoutIntegration', { brandIds: data.brandIds }); + } + + if (data.action === 'featureSuggestion') { + return RobotEntries.updateOrCreate('featureSuggestion', { message: data.message }); + } + } + } + + robotEntrySchema.loadClass(RobotEntry); + + return robotEntrySchema; +}; + +// onboarding ========================== +interface IGetOrCreateDoc { + type: string; + user: IUserDocument; +} + +interface IGetOrCreateResponse { + status: string; + entry: IOnboardingHistoryDocument; +} + +interface IStepsCompletenessResponse { + [key: string]: boolean; +} + +export interface IOnboardingHistoryModel extends Model { + getOrCreate(doc: IGetOrCreateDoc): Promise; + stepsCompletness(steps: string[], user: IUserDocument): IStepsCompletenessResponse; + completeShowStep(step: string, userId: string): void; + forceComplete(userId: string): void; + userStatus(userId: string): string; +} + +export const loadOnboardingHistoryClass = () => { + class OnboardingHistory { + public static async getOrCreate({ type, user }: IGetOrCreateDoc): Promise { + const prevEntry = await OnboardingHistories.findOne({ userId: user._id }); + + if (!prevEntry) { + const entry = await OnboardingHistories.create({ userId: user._id, completedSteps: [type] }); + return { status: 'created', entry }; + } + + if (prevEntry.completedSteps.includes(type)) { + return { status: 'prev', entry: prevEntry }; + } + + const updatedEntry = await OnboardingHistories.updateOne( + { userId: user._id }, + { $push: { completedSteps: type } }, + ); + + return { status: 'created', entry: updatedEntry }; + } + + public static async stepsCompletness(steps: string[], user: IUserDocument): Promise { + const result: IStepsCompletenessResponse = {}; + + for (const step of steps) { + const selector = { userId: user._id, completedSteps: { $in: [step] } }; + result[step] = (await OnboardingHistories.find(selector).countDocuments()) > 0; + } + + return result; + } + + public static async forceComplete(userId: string): Promise { + const entry = await OnboardingHistories.findOne({ userId }); + + if (!entry) { + return OnboardingHistories.create({ userId, isCompleted: true }); + } + + return OnboardingHistories.updateOne({ userId }, { $set: { isCompleted: true } }); + } + + public static async completeShowStep(step: string, userId: string): Promise { + return OnboardingHistories.updateOne({ userId }, { $push: { completedSteps: step } }, { upsert: true }); + } + + public static async userStatus(userId: string): Promise { + const entries = await OnboardingHistories.find({ userId }); + const completed = entries.find(item => item.isCompleted); + + if (completed) { + return 'completed'; + } + + if (entries.length > 0) { + return 'inComplete'; + } + + return 'initial'; + } + } + + onboardingHistorySchema.loadClass(OnboardingHistory); + + return onboardingHistorySchema; +}; + +loadClass(); +loadOnboardingHistoryClass(); + +// tslint:disable-next-line +export const RobotEntries = model('robot_entries', robotEntrySchema); + +// tslint:disable-next-line +export const OnboardingHistories = model( + 'onboarding_histories', + onboardingHistorySchema, +); diff --git a/src/db/models/Scripts.ts b/src/db/models/Scripts.ts index 51f435c36..a2d1ab813 100644 --- a/src/db/models/Scripts.ts +++ b/src/db/models/Scripts.ts @@ -3,6 +3,7 @@ import { Brands, Forms, Integrations } from '.'; import { IScript, IScriptDocument, scriptSchema } from './definitions/scripts'; export interface IScriptModel extends Model { + getScript(_id: string): Promise; createScript(fields: IScript): Promise; updateScript(_id: string, fields: IScript): Promise; removeScript(_id: string): void; @@ -12,43 +13,48 @@ type LeadMaps = Array<{ formCode?: string; brandCode?: string }>; export const loadClass = () => { class Script { + /* + * Get a script + */ + public static async getScript(_id: string) { + const script = await Scripts.findOne({ _id }); + + if (!script) { + throw new Error('Script not found'); + } + + return script; + } + public static async calculateAutoFields(fields: IScript) { const autoFields: { messengerBrandCode?: string; leadMaps?: LeadMaps } = {}; // generate brandCode if (fields.messengerId) { - const messengerIntegration = await Integrations.findOne({ _id: fields.messengerId }); + const messengerIntegration = await Integrations.getIntegration(fields.messengerId); - if (messengerIntegration) { - const brand = await Brands.findOne({ _id: messengerIntegration.brandId }); + const brand = await Brands.getBrand(messengerIntegration.brandId || ''); - if (brand) { - autoFields.messengerBrandCode = brand.code; - } - } + autoFields.messengerBrandCode = brand.code; } - // Generate formCode, brandCode combinations + // Generate leadCode, brandCode combinations if (fields.leadIds) { - const integrations = await Integrations.find({ _id: { $in: fields.leadIds } }); + const integrations = await Integrations.findIntegrations({ _id: { $in: fields.leadIds } }); const maps: LeadMaps = []; - if (integrations) { - for (const integration of integrations) { - const brand = await Brands.findOne({ _id: integration.brandId }); - const form = await Forms.findOne({ _id: integration.formId }); + for (const integration of integrations) { + const brand = await Brands.getBrand(integration.brandId || ''); + const form = await Forms.getForm(integration.formId || ''); - if (brand && form) { - maps.push({ - formCode: form.code, - brandCode: brand.code, - }); - } - } - - autoFields.leadMaps = maps; + maps.push({ + formCode: form.code, + brandCode: brand.code, + }); } + + autoFields.leadMaps = maps; } return autoFields; diff --git a/src/db/models/Segments.ts b/src/db/models/Segments.ts index 5ea071d85..9a6161571 100644 --- a/src/db/models/Segments.ts +++ b/src/db/models/Segments.ts @@ -2,6 +2,7 @@ import { Model, model } from 'mongoose'; import { ISegment, ISegmentDocument, segmentSchema } from './definitions/segments'; export interface ISegmentModel extends Model { + getSegment(_id: string): Promise; createSegment(doc: ISegment): Promise; updateSegment(_id: string, doc: ISegment): Promise; removeSegment(_id: string): void; @@ -9,6 +10,19 @@ export interface ISegmentModel extends Model { export const loadClass = () => { class Segment { + /* + * Get a segment + */ + public static async getSegment(_id: string) { + const segment = await Segments.findOne({ _id }); + + if (!segment) { + throw new Error('Segment not found'); + } + + return segment; + } + /** * Create a segment * @param {Object} segmentObj object diff --git a/src/db/models/Tags.ts b/src/db/models/Tags.ts index 12d136218..bc6a31b26 100644 --- a/src/db/models/Tags.ts +++ b/src/db/models/Tags.ts @@ -1,6 +1,6 @@ import { Model, model } from 'mongoose'; import * as _ from 'underscore'; -import { Companies, Conversations, Customers, EngageMessages, Integrations } from '.'; +import { Companies, Conversations, Customers, EngageMessages, Integrations, Products } from '.'; import { ITag, ITagDocument, tagSchema } from './definitions/tags'; interface ITagObjectParams { @@ -11,6 +11,7 @@ interface ITagObjectParams { } export interface ITagModel extends Model { + getTag(_id: string): Promise; createTag(doc: ITag): Promise; updateTag(_id: string, doc: ITag): Promise; removeTag(ids: string[]): void; @@ -21,6 +22,18 @@ export interface ITagModel extends Model { export const loadClass = () => { class Tag { + /* + * Get a tag + */ + public static async getTag(_id: string) { + const tag = await Tags.findOne({ _id }); + + if (!tag) { + throw new Error('Tag not found'); + } + + return tag; + } /* * Validates tag uniquness */ @@ -131,7 +144,8 @@ export const loadClass = () => { count += await Conversations.find({ tagIds: { $in: ids } }).countDocuments(); count += await EngageMessages.find({ tagIds: { $in: ids } }).countDocuments(); count += await Companies.find({ tagIds: { $in: ids } }).countDocuments(); - count += await Integrations.find({ tagIds: { $in: ids } }).countDocuments(); + count += await Integrations.findIntegrations({ tagIds: { $in: ids } }).countDocuments(); + count += await Products.find({ tagIds: { $in: ids } }).countDocuments(); if (count > 0) { throw new Error("Can't remove a tag with tagged object(s)"); @@ -159,6 +173,9 @@ export const loadClass = () => { case 'integration': collection = Integrations; break; + case 'product': + collection = Products; + break; } await Tags.tagObject({ diff --git a/src/db/models/Tasks.ts b/src/db/models/Tasks.ts index b619b3e55..5cdbecaf4 100644 --- a/src/db/models/Tasks.ts +++ b/src/db/models/Tasks.ts @@ -1,37 +1,55 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; -import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; -import { IOrderInput } from './definitions/boards'; -import { ITask, ITaskDocument, taskSchema } from './definitions/tasks'; +import { destroyBoardItemRelations, fillSearchTextItem, watchItem } from './boardUtils'; +import { IItemCommonFields as ITask } from './definitions/boards'; +import { ACTIVITY_CONTENT_TYPES } from './definitions/constants'; +import { ITaskDocument, taskSchema } from './definitions/tasks'; export interface ITaskModel extends Model { createTask(doc: ITask): Promise; + getTask(_id: string): Promise; updateTask(_id: string, doc: ITask): Promise; - updateOrder(stageId: string, orders: IOrderInput[]): Promise; - removeTask(_id: string): void; watchTask(_id: string, isAdd: boolean, userId: string): void; - changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; - changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; + removeTasks(_ids: string[]): Promise<{ n: number; ok: number }>; + updateTimeTracking(_id: string, status: string, timeSpent: number, startDate: string): Promise; } export const loadTaskClass = () => { class Task { + /** + * Retreives Task + */ + public static async getTask(_id: string) { + const task = await Tasks.findOne({ _id }); + + if (!task) { + throw new Error('Task not found'); + } + + return task; + } + /** * Create a Task */ public static async createTask(doc: ITask) { - const tasksCount = await Tasks.find({ - stageId: doc.stageId, - }).countDocuments(); + if (doc.sourceConversationId) { + const convertedTask = await Tasks.findOne({ sourceConversationId: doc.sourceConversationId }); + + if (convertedTask) { + throw new Error('Already converted a task'); + } + } const task = await Tasks.create({ ...doc, - order: tasksCount, + createdAt: new Date(), modifiedAt: new Date(), + searchText: fillSearchTextItem(doc), }); // create log - await ActivityLogs.createTaskLog(task); + await ActivityLogs.createBoardItemLog({ item: task, contentType: 'task' }); return task; } @@ -40,29 +58,11 @@ export const loadTaskClass = () => { * Update Task */ public static async updateTask(_id: string, doc: ITask) { - await Tasks.updateOne({ _id }, { $set: doc }); + const searchText = fillSearchTextItem(doc, await Tasks.getTask(_id)); - return Tasks.findOne({ _id }); - } + await Tasks.updateOne({ _id }, { $set: doc, searchText }); - /* - * Update given Tasks orders - */ - public static async updateOrder(stageId: string, orders: IOrderInput[]) { - return updateOrder(Tasks, orders, stageId); - } - - /** - * Remove Task - */ - public static async removeTask(_id: string) { - const task = await Tasks.findOne({ _id }); - - if (!task) { - throw new Error('Task not found'); - } - - return task.remove(); + return Tasks.findOne({ _id }); } /** @@ -72,18 +72,25 @@ export const loadTaskClass = () => { return watchItem(Tasks, _id, isAdd, userId); } - /** - * Change customer - */ - public static async changeCustomer(newCustomerId: string, oldCustomerIds: string[]) { - return changeCustomer(Tasks, newCustomerId, oldCustomerIds); + public static async removeTasks(_ids: string[]) { + // completely remove all related things + for (const _id of _ids) { + await destroyBoardItemRelations(_id, ACTIVITY_CONTENT_TYPES.TASK); + } + + return Tasks.deleteMany({ _id: { $in: _ids } }); } - /** - * Change company - */ - public static async changeCompany(newCompanyId: string, oldCompanyIds: string[]) { - return changeCompany(Tasks, newCompanyId, oldCompanyIds); + public static async updateTimeTracking(_id: string, status: string, timeSpent: number, startDate?: string) { + const doc: { status: string; timeSpent: number; startDate?: string } = { status, timeSpent }; + + if (startDate) { + doc.startDate = startDate; + } + + await Tasks.updateOne({ _id }, { $set: { timeTrack: doc } }); + + return Tasks.findOne({ _id }).lean(); } } diff --git a/src/db/models/Tickets.ts b/src/db/models/Tickets.ts index b1b9a0fdc..da86bbd7b 100644 --- a/src/db/models/Tickets.ts +++ b/src/db/models/Tickets.ts @@ -1,37 +1,53 @@ import { Model, model } from 'mongoose'; import { ActivityLogs } from '.'; -import { changeCompany, changeCustomer, updateOrder, watchItem } from './boardUtils'; -import { IOrderInput } from './definitions/boards'; +import { destroyBoardItemRelations, fillSearchTextItem, watchItem } from './boardUtils'; +import { ACTIVITY_CONTENT_TYPES } from './definitions/constants'; import { ITicket, ITicketDocument, ticketSchema } from './definitions/tickets'; export interface ITicketModel extends Model { createTicket(doc: ITicket): Promise; + getTicket(_id: string): Promise; updateTicket(_id: string, doc: ITicket): Promise; - updateOrder(stageId: string, orders: IOrderInput[]): Promise; - removeTicket(_id: string): void; watchTicket(_id: string, isAdd: boolean, userId: string): void; - changeCustomer(newCustomerId: string, oldCustomerIds: string[]): Promise; - changeCompany(newCompanyId: string, oldCompanyIds: string[]): Promise; + removeTickets(_ids: string[]): Promise<{ n: number; ok: number }>; } export const loadTicketClass = () => { class Ticket { + /** + * Retreives Ticket + */ + public static async getTicket(_id: string) { + const ticket = await Tickets.findOne({ _id }); + + if (!ticket) { + throw new Error('Ticket not found'); + } + + return ticket; + } + /** * Create a Ticket */ public static async createTicket(doc: ITicket) { - const ticketsCount = await Tickets.find({ - stageId: doc.stageId, - }).countDocuments(); + if (doc.sourceConversationId) { + const convertedTicket = await Tickets.findOne({ sourceConversationId: doc.sourceConversationId }); + + if (convertedTicket) { + throw new Error('Already converted a ticket'); + } + } const ticket = await Tickets.create({ ...doc, - order: ticketsCount, + createdAt: new Date(), modifiedAt: new Date(), + searchText: fillSearchTextItem(doc), }); // create log - await ActivityLogs.createTicketLog(ticket); + await ActivityLogs.createBoardItemLog({ item: ticket, contentType: 'ticket' }); return ticket; } @@ -40,29 +56,11 @@ export const loadTicketClass = () => { * Update Ticket */ public static async updateTicket(_id: string, doc: ITicket) { - await Tickets.updateOne({ _id }, { $set: doc }); + const searchText = fillSearchTextItem(doc, await Tickets.getTicket(_id)); - return Tickets.findOne({ _id }); - } + await Tickets.updateOne({ _id }, { $set: doc, searchText }); - /* - * Update given tickets orders - */ - public static async updateOrder(stageId: string, orders: IOrderInput[]) { - return updateOrder(Tickets, orders, stageId); - } - - /** - * Remove Ticket - */ - public static async removeTicket(_id: string) { - const ticket = await Tickets.findOne({ _id }); - - if (!ticket) { - throw new Error('Ticket not found'); - } - - return ticket.remove(); + return Tickets.findOne({ _id }); } /** @@ -72,18 +70,13 @@ export const loadTicketClass = () => { return watchItem(Tickets, _id, isAdd, userId); } - /** - * Change customer - */ - public static async changeCustomer(newCustomerId: string, oldCustomerIds: string[]) { - return changeCustomer(Tickets, newCustomerId, oldCustomerIds); - } + public static async removeTickets(_ids: string[]) { + // completely remove all related things + for (const _id of _ids) { + await destroyBoardItemRelations(_id, ACTIVITY_CONTENT_TYPES.TICKET); + } - /** - * Change company - */ - public static async changeCompany(newCompanyId: string, oldCompanyIds: string[]) { - return changeCompany(Tickets, newCompanyId, oldCompanyIds); + return Tickets.deleteMany({ _id: { $in: _ids } }); } } diff --git a/src/db/models/Users.ts b/src/db/models/Users.ts index bf2a4e56d..5c149525d 100644 --- a/src/db/models/Users.ts +++ b/src/db/models/Users.ts @@ -4,7 +4,8 @@ import * as jwt from 'jsonwebtoken'; import { Model, model } from 'mongoose'; import * as sha256 from 'sha256'; import { UsersGroups } from '.'; -import { IDetail, IEmailSignature, ILink, IUser, IUserDocument, userSchema } from './definitions/users'; +import { ILink } from './definitions/common'; +import { IDetail, IEmailSignature, IUser, IUserDocument, userSchema } from './definitions/users'; const SALT_WORK_FACTOR = 10; @@ -18,9 +19,12 @@ interface IEditProfile { interface IUpdateUser extends IEditProfile { password?: string; groupIds?: string[]; + brandIds?: string[]; } export interface IUserModel extends Model { + getUser(_id: string): Promise; + checkPassword(password: string): void; checkDuplication({ email, idsToExclude, @@ -35,12 +39,11 @@ export interface IUserModel extends Model { createUser(doc: IUser): Promise; updateUser(_id: string, doc: IUpdateUser): Promise; editProfile(_id: string, doc: IEditProfile): Promise; - updateOnBoardSeen({ _id }: { _id: string }): Promise; configEmailSignatures(_id: string, signatures: IEmailSignature[]): Promise; configGetNotificationByEmail(_id: string, isAllowed: boolean): Promise; setUserActiveOrInactive(_id: string): Promise; - generatePassword(password: string): string; - createUserWithConfirmation({ email, groupId }: { email: string; groupId: string }): string; + generatePassword(password: string): Promise; + invite({ email, password, groupId }: { email: string; password: string; groupId: string }): string; resendInvitation({ email }: { email: string }): string; confirmInvitation({ token, @@ -57,6 +60,7 @@ export interface IUserModel extends Model { }): Promise; comparePassword(password: string, userPassword: string): boolean; resetPassword({ token, newPassword }: { token: string; newPassword: string }): Promise; + resetMemberPassword({ _id, newPassword }: { _id: string; newPassword: string }): Promise; changePassword({ _id, currentPassword, @@ -82,24 +86,33 @@ export interface IUserModel extends Model { export const loadClass = () => { class User { + public static async getUser(_id: string) { + const user = await Users.findOne({ _id }); + + if (!user) { + throw new Error('User not found'); + } + + return user; + } + + public static checkPassword(password: string) { + if (!password.match(/^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$/)) { + throw new Error( + 'Must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters', + ); + } + } /** * Checking if user has duplicated properties */ - public static async checkDuplication({ - email, - idsToExclude, - emails, - }: { - email?: string; - idsToExclude?: string | string[]; - emails?: string[]; - }) { + public static async checkDuplication({ email, idsToExclude }: { email?: string; idsToExclude?: string }) { const query: { [key: string]: any } = {}; let previousEntry; // Adding exclude operator to the query if (idsToExclude) { - query._id = idsToExclude instanceof Array ? { $nin: idsToExclude } : { $ne: idsToExclude }; + query._id = { $ne: idsToExclude }; } // Checking if user has email @@ -111,24 +124,16 @@ export const loadClass = () => { throw new Error('Duplicated email'); } } - - if (emails) { - previousEntry = await Users.find({ email: { $in: [emails] } }); - - if (previousEntry.length > 0) { - throw new Error('Duplicated emails'); - } - } } public static getSecret() { - return 'dfjklsafjjekjtejifjidfjsfd'; + return process.env.JWT_TOKEN_SECRET || ''; } /** * Create new user */ - public static async createUser({ username, email, password, details, links, groupIds }: IUser) { + public static async createUser({ username, email, password, details, links, groupIds, isOwner = false }: IUser) { // empty string password validation if (password === '') { throw new Error('Password can not be empty'); @@ -137,7 +142,10 @@ export const loadClass = () => { // Checking duplicated email await Users.checkDuplication({ email }); + this.checkPassword(password); + return Users.create({ + isOwner, username, email, details, @@ -152,14 +160,19 @@ export const loadClass = () => { /** * Update user information */ - public static async updateUser(_id: string, { username, email, password, details, links, groupIds }: IUpdateUser) { - const doc = { username, email, password, details, links, groupIds }; + public static async updateUser( + _id: string, + { username, email, password, details, links, groupIds, brandIds }: IUpdateUser, + ) { + const doc = { username, email, password, details, links, groupIds, brandIds }; // Checking duplicated email await this.checkDuplication({ email, idsToExclude: _id }); // change password if (password) { + this.checkPassword(password); + doc.password = await this.generatePassword(password); // if there is no password specified then leave password field alone @@ -185,7 +198,7 @@ export const loadClass = () => { /** * Create new user with invitation token */ - public static async createUserWithConfirmation({ email, groupId }: { email: string; groupId: string }) { + public static async invite({ email, password, groupId }: { email: string; password: string; groupId: string }) { // Checking duplicated email await Users.checkDuplication({ email }); @@ -195,9 +208,14 @@ export const loadClass = () => { const { token, expires } = await User.generateToken(); + this.checkPassword(password); + await Users.create({ email, groupIds: [groupId], + isActive: true, + // hash password + password: await this.generatePassword(password), registrationToken: token, registrationTokenExpires: expires, }); @@ -232,21 +250,6 @@ export const loadClass = () => { return token; } - /** - * User has seen on board set up - */ - public static async updateOnBoardSeen({ _id }: { _id: string }) { - const user = await Users.findOne({ _id }); - - if (!user) { - throw new Error('User not found'); - } - - await Users.updateOne({ _id }, { $set: { hasSeenOnBoard: true } }); - - return user; - } - /** * Confirms user by invitation */ @@ -282,6 +285,8 @@ export const loadClass = () => { throw new Error('Password does not match'); } + this.checkPassword(password); + await Users.updateOne( { _id: user._id }, { @@ -393,6 +398,8 @@ export const loadClass = () => { throw new Error('Password is required.'); } + this.checkPassword(newPassword); + // set new password await Users.findByIdAndUpdate( { _id: user._id }, @@ -406,6 +413,23 @@ export const loadClass = () => { return Users.findOne({ _id: user._id }); } + /** + * Reset member's password by given _id & newPassword + */ + public static async resetMemberPassword({ _id, newPassword }: { _id: string; newPassword: string }) { + const user = await Users.getUser(_id); + + if (!newPassword) { + throw new Error('Password is required.'); + } + + this.checkPassword(newPassword); + + await Users.updateOne({ _id }, { $set: { password: await this.generatePassword(newPassword) } }); + + return Users.findOne({ _id: user._id }); + } + /* * Change user password */ @@ -423,11 +447,9 @@ export const loadClass = () => { throw new Error('Password can not be empty'); } - const user = await Users.findOne({ _id }); + this.checkPassword(newPassword); - if (!user) { - throw new Error('User not found'); - } + const user = await Users.getUser(_id); // check current password ============ const valid = await this.comparePassword(currentPassword, user.password); @@ -484,6 +506,7 @@ export const loadClass = () => { details: _user.details, isOwner: _user.isOwner, groupIds: _user.groupIds, + brandIds: _user.brandIds, username: _user.username, }; @@ -500,24 +523,19 @@ export const loadClass = () => { * Renews tokens */ public static async refreshTokens(refreshToken: string) { - let _id = null; + let _id = ''; try { // validate refresh token const { user } = jwt.verify(refreshToken, this.getSecret()); _id = user._id; - // if refresh token is expired then force to login } catch (e) { return {}; } - const dbUser = await Users.findOne({ _id }); - - if (!dbUser) { - throw new Error('User not found'); - } + const dbUser = await Users.getUser(_id); // recreate tokens const [newToken, newRefreshToken] = await this.createTokens(dbUser, this.getSecret()); @@ -542,7 +560,11 @@ export const loadClass = () => { deviceToken?: string; }) { const user = await Users.findOne({ - $or: [{ email: { $regex: new RegExp(email, 'i') } }, { username: { $regex: new RegExp(email, 'i') } }], + $or: [ + { email: { $regex: new RegExp(`^${email}$`, 'i') } }, + { username: { $regex: new RegExp(`^${email}$`, 'i') } }, + ], + isActive: true, }); if (!user || !user.password) { diff --git a/src/db/models/Webhook.ts b/src/db/models/Webhook.ts new file mode 100644 index 000000000..946ad2cba --- /dev/null +++ b/src/db/models/Webhook.ts @@ -0,0 +1,80 @@ +import { Model, model } from 'mongoose'; +import { getUniqueValue } from '../factories'; +import { WEBHOOK_STATUS } from './definitions/constants'; +import { IWebhook, IWebhookDocument, webhookSchema } from './definitions/webhook'; + +export interface IWebhookModel extends Model { + getWebHook(_id: string): Promise; + getWebHooks(): Promise; + createWebhook(doc: IWebhook): Promise; + updateWebhook(_id: string, doc: IWebhook): Promise; + updateStatus(_id: string, status: string): Promise; + removeWebhooks(_id: string): void; +} + +export const loadClass = () => { + class Webhook { + /* + * Get a Webhook + */ + public static async getWebHook(_id: string) { + const webhook = await Webhooks.findOne({ _id }); + + if (!webhook) { + throw new Error('Webhook not found'); + } + + return webhook; + } + + public static async getWebHooks() { + return Webhooks.find({}); + } + + /** + * Create webhook + */ + public static async createWebhook(doc: IWebhook) { + if (!doc.url.includes('https')) { + throw new Error('Url is not valid. Enter valid url with ssl cerfiticate'); + } + + const modifiedDoc: any = { ...doc }; + modifiedDoc.token = await getUniqueValue(Webhooks, 'token'); + modifiedDoc.status = WEBHOOK_STATUS.UNAVAILABLE; + + return Webhooks.create(modifiedDoc); + } + + public static async updateWebhook(_id: string, doc: IWebhook) { + if (!doc.url.includes('https')) { + throw new Error('Url is not valid. Enter valid url with ssl cerfiticate'); + } + + await Webhooks.updateOne({ _id }, { $set: doc }, { runValidators: true }); + + return Webhooks.findOne({ _id }); + } + + public static async removeWebhooks(_id) { + return Webhooks.deleteOne({ _id }); + } + + public static async updateStatus(_id: string, status: string) { + await Webhooks.updateOne({ _id }, { $set: { status } }, { runValidators: true }); + + return Webhooks.findOne({ _id }); + } + } + + webhookSchema.loadClass(Webhook); + + return webhookSchema; +}; + +loadClass(); + +// tslint:disable-next-line +const Webhooks = model('webhooks', webhookSchema); + +export default Webhooks; diff --git a/src/db/models/boardUtils.ts b/src/db/models/boardUtils.ts index 8bd0c0ad4..8db5a5cd8 100644 --- a/src/db/models/boardUtils.ts +++ b/src/db/models/boardUtils.ts @@ -1,6 +1,105 @@ -import { IOrderInput } from './definitions/boards'; +import { + ActivityLogs, + Checklists, + Companies, + Conformities, + Customers, + Deals, + GrowthHacks, + InternalNotes, + Tasks, + Tickets, +} from '.'; +import { validSearchText } from '../../data/utils'; +import { IItemCommonFields, IOrderInput } from './definitions/boards'; +import { ICompanyDocument } from './definitions/companies'; +import { BOARD_STATUSES, BOARD_TYPES } from './definitions/constants'; +import { ICustomerDocument } from './definitions/customers'; + +interface ISetOrderParam { + collection: any; + stageId: string; + aboveItemId?: string; +} + +const randomBetween = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +const orderHeler = (aboveOrder, belowOrder) => { + // empty stage + if (!aboveOrder && !belowOrder) { + return 100; + } + + // end of stage + if (!belowOrder) { + return aboveOrder + 10; + } + + // begin of stage + if (!aboveOrder) { + return randomBetween(0, belowOrder); + } + + // between items on stage + return randomBetween(aboveOrder, belowOrder); +}; + +export const getNewOrder = async ({ collection, stageId, aboveItemId }: ISetOrderParam) => { + const aboveItem = await collection.findOne({ _id: aboveItemId }); + + const aboveOrder = aboveItem?.order || 0; + + const belowItems = await collection + .find({ stageId, order: { $gt: aboveOrder }, status: { $ne: BOARD_STATUSES.ARCHIVED } }) + .sort({ order: 1 }) + .limit(1); + + const belowOrder = belowItems[0]?.order; + + const order = orderHeler(aboveOrder, belowOrder); + + // if duplicated order, then in stages items bulkUpdate 100, 110, 120, 130 + if ([aboveOrder, belowOrder].includes(order)) { + const bulkOps: Array<{ + updateOne: { + filter: { _id: string }; + update: { order: number }; + }; + }> = []; + + let ord = 100; + + const allItems = await collection + .find( + { + stageId, + status: { $ne: BOARD_STATUSES.ARCHIVED }, + }, + { _id: 1, order: 1 }, + ) + .sort({ order: 1 }); + + for (const item of allItems) { + bulkOps.push({ + updateOne: { + filter: { _id: item._id }, + update: { order: ord }, + }, + }); + + ord = ord + 10; + } -export const updateOrder = async (collection: any, orders: IOrderInput[], stageId?: string) => { + await collection.bulkWrite(bulkOps); + return getNewOrder({ collection, stageId, aboveItemId }); + } + + return order; +}; + +export const updateOrder = async (collection: any, orders: IOrderInput[]) => { if (orders.length === 0) { return []; } @@ -9,18 +108,14 @@ export const updateOrder = async (collection: any, orders: IOrderInput[], stageI const bulkOps: Array<{ updateOne: { filter: { _id: string }; - update: { stageId?: string; order: number }; + update: { order: number }; }; }> = []; for (const { _id, order } of orders) { ids.push(_id); - const selector: { order: number; stageId?: string } = { order }; - - if (stageId) { - selector.stageId = stageId; - } + const selector: { order: number } = { order }; bulkOps.push({ updateOne: { @@ -30,38 +125,11 @@ export const updateOrder = async (collection: any, orders: IOrderInput[], stageI }); } - if (bulkOps) { - await collection.bulkWrite(bulkOps); - } + await collection.bulkWrite(bulkOps); return collection.find({ _id: { $in: ids } }).sort({ order: 1 }); }; -export const changeCustomer = async (collection: any, newCustomerId: string, oldCustomerIds: string[]) => { - if (oldCustomerIds) { - await collection.updateMany( - { customerIds: { $in: oldCustomerIds } }, - { $addToSet: { customerIds: newCustomerId } }, - ); - await collection.updateMany( - { customerIds: { $in: oldCustomerIds } }, - { $pullAll: { customerIds: oldCustomerIds } }, - ); - } - - return collection.find({ customerIds: { $in: oldCustomerIds } }); -}; - -export const changeCompany = async (collection: any, newCompanyId: string, oldCompanyIds: string[]) => { - if (oldCompanyIds) { - await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $addToSet: { companyIds: newCompanyId } }); - - await collection.updateMany({ companyIds: { $in: oldCompanyIds } }, { $pullAll: { companyIds: oldCompanyIds } }); - } - - return collection.find({ customerIds: { $in: oldCompanyIds } }); -}; - export const watchItem = async (collection: any, _id: string, isAdd: boolean, userId: string) => { const item = await collection.findOne({ _id }); @@ -79,3 +147,73 @@ export const watchItem = async (collection: any, _id: string, isAdd: boolean, us return collection.findOne({ _id }); }; + +export const fillSearchTextItem = (doc: IItemCommonFields, item?: IItemCommonFields) => { + const document = item || { name: '', description: '' }; + Object.assign(document, doc); + + return validSearchText([document.name || '', document.description || '']); +}; + +export const getCollection = (type: string) => { + let collection; + + switch (type) { + case BOARD_TYPES.DEAL: { + collection = Deals; + + break; + } + case BOARD_TYPES.GROWTH_HACK: { + collection = GrowthHacks; + + break; + } + case BOARD_TYPES.TASK: { + collection = Tasks; + + break; + } + case BOARD_TYPES.TICKET: { + collection = Tickets; + + break; + } + } + + return collection; +}; + +export const getItem = async (type: string, _id: string) => { + const item = await getCollection(type).findOne({ _id }); + + if (!item) { + throw new Error(`${type} not found`); + } + + return item; +}; + +export const getCompanies = async (mainType: string, mainTypeId: string): Promise => { + const conformities = await Conformities.find({ mainType, mainTypeId, relType: 'company' }); + + const companyIds = conformities.map(c => c.relTypeId); + + return Companies.find({ _id: { $in: companyIds } }); +}; + +export const getCustomers = async (mainType: string, mainTypeId: string): Promise => { + const conformities = await Conformities.find({ mainType, mainTypeId, relType: 'customer' }); + + const customerIds = conformities.map(c => c.relTypeId); + + return Customers.find({ _id: { $in: customerIds } }); +}; + +// Removes all board item related things +export const destroyBoardItemRelations = async (contentTypeId: string, contentType: string) => { + await ActivityLogs.removeActivityLog(contentTypeId); + await Checklists.removeChecklists(contentType, contentTypeId); + await Conformities.removeConformity({ mainType: contentType, mainTypeId: contentTypeId }); + await InternalNotes.remove({ contentType, contentTypeId }); +}; diff --git a/src/db/models/definitions/activityLogs.ts b/src/db/models/definitions/activityLogs.ts index 28e513ae2..7c6a3083c 100644 --- a/src/db/models/definitions/activityLogs.ts +++ b/src/db/models/definitions/activityLogs.ts @@ -1,140 +1,33 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -import { ACTIVITY_ACTIONS, ACTIVITY_CONTENT_TYPES, ACTIVITY_PERFORMER_TYPES, ACTIVITY_TYPES } from './constants'; +import { field } from './utils'; -export interface IActionPerformer { - type: string; - id: string; -} - -interface IActionPerformerDocument extends IActionPerformer, Document { - id: string; -} - -export interface IActivity { - type: string; +export interface IActivityLogInput { action: string; - content?: string; - id?: string; -} - -interface IActivityDocument extends IActivity, Document { - id?: string; -} - -export interface IContentType { - id: string; - type: string; -} - -interface IContentTypeDocument extends IContentType, Document { - id: string; -} - -export interface IActivityLogDocument extends Document { - _id: string; - activity: IActivityDocument; - performedBy?: IActionPerformerDocument; - contentType: IContentTypeDocument; - createdAt: Date; + content?: any; + contentType: string; + contentId: string; + createdBy: string; } - export interface IActivityLog { + action: string; + content?: any; contentType: string; contentId: string; - activityType: string; - limit: number; + createdBy: string; } -// Mongoose schemas =========== - -/* Performer of the action: - *system* cron job, user - ex: Sales manager that has registered a new customer - Sales manager is the action performer */ -const actionPerformerSchema = new Schema( - { - type: field({ - type: String, - enum: ACTIVITY_PERFORMER_TYPES.ALL, - default: ACTIVITY_PERFORMER_TYPES.SYSTEM, - required: true, - }), - id: field({ - type: String, - }), - }, - { _id: false }, -); - -/* - The action that is being performed - ex1: A user writes an internal note - in this case: type is InternalNote - action is create (write) - id is the InternalNote id - ex2: Sales manager registers a new customer - in this case: type is customer - action is create (register) - id is Customer id - customer and activity contentTypes are the same in this case - ex3: Cronjob runs and a customer is found to be suitable for a particular segment - action is create: a new segment user - type is segment - id is Segment id - ex4: An internalNote concerning a customer was updated - action is update - type is InternalNote - id is InternalNote id - */ -const activitySchema = new Schema( - { - type: field({ - type: String, - required: true, - enum: ACTIVITY_TYPES.ALL, - }), - action: field({ - type: String, - required: true, - enum: ACTIVITY_ACTIONS.ALL, - }), - content: field({ - type: String, - optional: true, - }), - id: field({ - type: String, - }), - }, - { _id: false }, -); - -/* the customer that is related to a given ActivityLog - can be both Company or Customer documents */ -const contentTypeSchema = new Schema( - { - id: field({ - type: String, - required: true, - }), - type: field({ - type: String, - enum: ACTIVITY_CONTENT_TYPES.ALL, - required: true, - }), - }, - { _id: false }, -); +export interface IActivityLogDocument extends IActivityLog, Document { + _id: string; + createdAt: Date; +} export const activityLogSchema = new Schema({ _id: field({ pkey: true }), - activity: { type: activitySchema }, - performedBy: { type: actionPerformerSchema, optional: true }, - contentType: { type: contentTypeSchema }, - // TODO: remove - coc: { type: contentTypeSchema, optional: true }, - + contentId: field({ type: String, index: true }), + contentType: field({ type: String, index: true }), + action: field({ type: String, index: true }), + content: Schema.Types.Mixed, + createdBy: field({ type: String }), createdAt: field({ type: Date, required: true, diff --git a/src/db/models/definitions/boards.ts b/src/db/models/definitions/boards.ts index be56d8428..3f6a7b473 100644 --- a/src/db/models/definitions/boards.ts +++ b/src/db/models/definitions/boards.ts @@ -1,6 +1,13 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -import { BOARD_TYPES, PIPELINE_VISIBLITIES, PROBABILITY } from './constants'; +import { + BOARD_STATUSES, + BOARD_TYPES, + HACK_SCORING_TYPES, + PIPELINE_VISIBLITIES, + PROBABILITY, + TIME_TRACK_TYPES, +} from './constants'; +import { field, schemaWrapper } from './utils'; interface ICommonFields { userId?: string; @@ -11,6 +18,7 @@ interface ICommonFields { export interface IItemCommonFields { name?: string; + // TODO migrate after remove 2row companyIds?: string[]; customerIds?: string[]; closeDate?: Date; @@ -18,18 +26,40 @@ export interface IItemCommonFields { assignedUserIds?: string[]; watchedUserIds?: string[]; notifiedUserIds?: string[]; - stageId?: string; + labelIds?: string[]; + attachments?: any[]; + stageId: string; initialStageId?: string; modifiedAt?: Date; modifiedBy?: string; userId?: string; createdAt?: Date; order?: number; + searchText?: string; + priority?: string; + sourceConversationId?: string; + status?: string; + timeTrack?: { + status: string; + timeSpent: number; + startDate?: string; + }; +} + +export interface IItemCommonFieldsDocument extends IItemCommonFields, Document { + _id: string; +} + +export interface IItemDragCommonFields { + proccessId: string; + itemId: string; + aboveItemId?: string; + destinationStageId: string; + sourceStageId: string; } export interface IBoard extends ICommonFields { name?: string; - isDefault?: boolean; } export interface IBoardDocument extends IBoard, Document { @@ -38,11 +68,19 @@ export interface IBoardDocument extends IBoard, Document { export interface IPipeline extends ICommonFields { name?: string; - boardId?: string; + boardId: string; visibility?: string; memberIds?: string[]; bgColor?: string; watchedUserIds?: string[]; + + startDate?: Date; + endDate?: Date; + metric?: string; + hackScoringType?: string; + templateId?: string; + isCheckUser?: boolean; + excludeCheckUserIds?: string[]; } export interface IPipelineDocument extends IPipeline, Document { @@ -52,89 +90,149 @@ export interface IPipelineDocument extends IPipeline, Document { export interface IStage extends ICommonFields { name?: string; probability?: string; - pipelineId?: string; + pipelineId: string; + formId?: string; + status?: string; } export interface IStageDocument extends IStage, Document { _id: string; } +// Not mongoose document, just stage shaped plain object +export type IPipelineStage = IStage & { _id: string }; + export interface IOrderInput { _id: string; order: number; } +export const attachmentSchema = new Schema( + { + name: field({ type: String, label: 'Name' }), + url: field({ type: String, label: 'Url' }), + type: field({ type: String, label: 'Type' }), + size: field({ type: Number, optional: true, label: 'Size' }), + }, + { _id: false }, +); + // Mongoose schemas ======================= const commonFieldsSchema = { - userId: field({ type: String }), + userId: field({ type: String, label: 'Created by' }), createdAt: field({ type: Date, default: new Date(), + label: 'Created at', }), - order: field({ type: Number }), + order: field({ type: Number, label: 'Order' }), type: field({ type: String, enum: BOARD_TYPES.ALL, required: true, + label: 'Type', }), }; +const timeTrackSchema = new Schema( + { + startDate: field({ type: String }), + timeSpent: field({ type: Number }), + status: field({ + type: String, + enum: TIME_TRACK_TYPES.ALL, + default: TIME_TRACK_TYPES.STOPPED, + }), + }, + { _id: false }, +); + export const commonItemFieldsSchema = { _id: field({ pkey: true }), - userId: field({ type: String }), - createdAt: field({ - type: Date, - default: new Date(), - }), - order: field({ type: Number }), - name: field({ type: String }), - companyIds: field({ type: [String] }), - customerIds: field({ type: [String] }), - closeDate: field({ type: Date }), - description: field({ type: String, optional: true }), - assignedUserIds: field({ type: [String] }), - watchedUserIds: field({ type: [String] }), - stageId: field({ type: String, optional: true }), - initialStageId: field({ type: String, optional: true }), + userId: field({ type: String, label: 'Created by' }), + createdAt: field({ type: Date, label: 'Created at' }), + order: field({ type: Number, label: 'Order' }), + name: field({ type: String, label: 'Name' }), + closeDate: field({ type: Date, label: 'Close date' }), + reminderMinute: field({ type: Number, label: 'Reminder minute' }), + isComplete: field({ type: Boolean, default: false, label: 'Is complete' }), + description: field({ type: String, optional: true, label: 'Description' }), + assignedUserIds: field({ type: [String], label: 'Assigned users' }), + watchedUserIds: field({ type: [String], label: 'Watched users' }), + labelIds: field({ type: [String], label: 'Labels' }), + attachments: field({ type: [attachmentSchema], label: 'Attachments' }), + stageId: field({ type: String, label: 'Stage', index: true }), + initialStageId: field({ type: String, optional: true, label: 'Initial stage' }), modifiedAt: field({ type: Date, default: new Date(), + label: 'Modified at', + }), + modifiedBy: field({ type: String, label: 'Modified by' }), + searchText: field({ type: String, optional: true, index: true }), + priority: field({ type: String, optional: true, label: 'Priority' }), + sourceConversationId: field({ type: String, optional: true }), + timeTrack: field({ + type: timeTrackSchema, + }), + status: field({ + type: String, + enum: BOARD_STATUSES.ALL, + default: BOARD_STATUSES.ACTIVE, + index: true, }), - modifiedBy: field({ type: String }), }; -export const boardSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - isDefault: field({ - type: Boolean, - default: false, +export const boardSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + ...commonFieldsSchema, }), - ...commonFieldsSchema, -}); +); export const pipelineSchema = new Schema({ _id: field({ pkey: true }), - name: field({ type: String }), - boardId: field({ type: String }), + name: field({ type: String, label: 'Name' }), + boardId: field({ type: String, label: 'Board' }), visibility: field({ type: String, enum: PIPELINE_VISIBLITIES.ALL, default: PIPELINE_VISIBLITIES.PUBLIC, + label: 'Visibility', + }), + watchedUserIds: field({ type: [String], label: 'Watched users' }), + memberIds: field({ type: [String], label: 'Members' }), + bgColor: field({ type: String, label: 'Background color' }), + // Growth hack + startDate: field({ type: Date, optional: true, label: 'Start date' }), + endDate: field({ type: Date, optional: true, label: 'End date' }), + metric: field({ type: String, optional: true, label: 'Metric' }), + hackScoringType: field({ + type: String, + enum: HACK_SCORING_TYPES.ALL, + label: 'Hacking scoring type', }), - watchedUserIds: field({ type: [String] }), - memberIds: field({ type: [String] }), - bgColor: field({ type: String }), + templateId: field({ type: String, optional: true, label: 'Template' }), + isCheckUser: field({ type: Boolean, optional: true, label: 'Show only the users created or assigned cards' }), + excludeCheckUserIds: field({ type: [String], optional: true, label: 'Users elligible to see all cards' }), ...commonFieldsSchema, }); export const stageSchema = new Schema({ _id: field({ pkey: true }), - name: field({ type: String }), + name: field({ type: String, label: 'Name' }), probability: field({ type: String, enum: PROBABILITY.ALL, + label: 'Probability', }), // Win probability - pipelineId: field({ type: String }), + pipelineId: field({ type: String, label: 'Pipeline' }), + formId: field({ type: String, label: 'Form' }), + status: field({ + type: String, + enum: BOARD_STATUSES.ALL, + default: BOARD_STATUSES.ACTIVE, + }), ...commonFieldsSchema, }); diff --git a/src/db/models/definitions/brands.ts b/src/db/models/definitions/brands.ts index 5cbff8fb2..361638857 100644 --- a/src/db/models/definitions/brands.ts +++ b/src/db/models/definitions/brands.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IBrandEmailConfig { type?: string; @@ -23,20 +23,21 @@ export interface IBrandDocument extends IBrand, Document { } // Mongoose schemas =========== -const brandEmailConfigSchema = new Schema({ +export const brandEmailConfigSchema = new Schema({ type: field({ type: String, enum: ['simple', 'custom'], + label: 'Type', }), - template: field({ type: String }), + template: field({ type: String, label: 'Template' }), }); export const brandSchema = new Schema({ _id: field({ pkey: true }), - code: field({ type: String }), - name: field({ type: String }), - description: field({ type: String, optional: true }), - userId: field({ type: String }), - createdAt: field({ type: Date }), - emailConfig: field({ type: brandEmailConfigSchema }), + code: field({ type: String, label: 'Code' }), + name: field({ type: String, label: 'Name' }), + description: field({ type: String, optional: true, label: 'Description' }), + userId: field({ type: String, label: 'Created by' }), + createdAt: field({ type: Date, label: 'Created at' }), + emailConfig: field({ type: brandEmailConfigSchema, label: 'Email config' }), }); diff --git a/src/db/models/definitions/channels.ts b/src/db/models/definitions/channels.ts index ea0be7108..c0399efa7 100644 --- a/src/db/models/definitions/channels.ts +++ b/src/db/models/definitions/channels.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IChannel { name?: string; @@ -18,21 +18,24 @@ export interface IChannelDocument extends IChannel, Document { export const channelSchema = new Schema({ _id: field({ pkey: true }), - createdAt: field({ type: Date }), - name: field({ type: String }), + createdAt: field({ type: Date, label: 'Created at' }), + name: field({ type: String, label: 'Name' }), description: field({ type: String, optional: true, + label: 'Description', }), - integrationIds: field({ type: [String] }), - memberIds: field({ type: [String] }), - userId: field({ type: String }), + integrationIds: field({ type: [String], label: 'Integrations' }), + memberIds: field({ type: [String], label: 'Members' }), + userId: field({ type: String, label: 'Created by' }), conversationCount: field({ type: Number, default: 0, + label: 'Conversation count', }), openConversationCount: field({ type: Number, default: 0, + label: 'Open conversation count', }), }); diff --git a/src/db/models/definitions/checklists.ts b/src/db/models/definitions/checklists.ts new file mode 100644 index 000000000..6c9847f28 --- /dev/null +++ b/src/db/models/definitions/checklists.ts @@ -0,0 +1,53 @@ +import { Document, Schema } from 'mongoose'; +import { ACTIVITY_CONTENT_TYPES } from './constants'; +import { field } from './utils'; + +export interface IChecklist { + contentType: string; + contentTypeId: string; + title: string; +} + +export interface IChecklistDocument extends IChecklist, Document { + _id: string; + createdUserId: string; + createdDate: Date; +} + +export interface IChecklistItem { + checklistId: string; + content: string; + isChecked: boolean; +} + +export interface IChecklistItemDocument extends IChecklistItem, Document { + _id: string; + createdUserId: string; + createdDate: Date; +} + +// Mongoose schemas ======================= + +export const checklistSchema = new Schema({ + _id: field({ pkey: true }), + contentType: field({ + type: String, + enum: ACTIVITY_CONTENT_TYPES.ALL, + label: 'Content type', + }), + order: field({ type: Number }), + contentTypeId: field({ type: String, label: 'Content type item' }), + title: field({ type: String, label: 'Title' }), + createdUserId: field({ type: String, label: 'Created by' }), + createdDate: field({ type: Date, label: 'Created at' }), +}); + +export const checklistItemSchema = new Schema({ + _id: field({ pkey: true }), + checklistId: field({ type: String, label: 'Check list' }), + content: field({ type: String, label: 'Content' }), + isChecked: field({ type: Boolean, label: 'Is checked' }), + createdUserId: field({ type: String, label: 'Created by' }), + createdDate: field({ type: Date, label: 'Created at' }), + order: field({ type: Number }), +}); diff --git a/src/db/models/definitions/common.ts b/src/db/models/definitions/common.ts index a72199020..457c3eeb7 100644 --- a/src/db/models/definitions/common.ts +++ b/src/db/models/definitions/common.ts @@ -1,30 +1,56 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -export interface IRule extends Document { - _id: string; +import { field } from './utils'; +export interface IRule { kind: string; text: string; condition: string; value: string; } +export interface ILink { + [key: string]: string; +} + +export interface IRuleDocument extends IRule, Document { + _id: string; +} + // schema for form's rules const ruleSchema = new Schema( { _id: field({ type: String }), // browserLanguage, currentUrl, etc ... - kind: field({ type: String }), + kind: field({ type: String, label: 'Kind' }), // Browser language, Current url etc ... - text: field({ type: String }), + text: field({ type: String, label: 'Text' }), // is, isNot, startsWith - condition: field({ type: String }), + condition: field({ type: String, label: 'Condition' }), - value: field({ type: String }), + value: field({ type: String, label: 'Value' }), }, { _id: false }, ); -export { ruleSchema }; +const customFieldSchema = new Schema( + { + field: field({ type: String }), + value: field({ type: Schema.Types.Mixed }), + stringValue: field({ type: String, optional: true }), + numberValue: field({ type: Number, optional: true }), + dateValue: field({ type: Date, optional: true }), + }, + { _id: false }, +); + +export interface ICustomField { + field: string; + value: any; + stringValue?: string; + numberValue?: number; + dateValue?: Date; +} + +export { ruleSchema, customFieldSchema }; diff --git a/src/db/models/definitions/companies.ts b/src/db/models/definitions/companies.ts index ab536e697..23ce78ad3 100644 --- a/src/db/models/definitions/companies.ts +++ b/src/db/models/definitions/companies.ts @@ -1,27 +1,12 @@ import { Document, Schema } from 'mongoose'; -import { - COMPANY_BUSINESS_TYPES, - COMPANY_INDUSTRY_TYPES, - COMPANY_LEAD_STATUS_TYPES, - COMPANY_LIFECYCLE_STATE_TYPES, - STATUSES, -} from './constants'; - -import { field } from '../utils'; - -export interface ILink { - linkedIn?: string; - twitter?: string; - facebook?: string; - github?: string; - youtube?: string; - website?: string; -} +import { customFieldSchema, ICustomField, ILink } from './common'; +import { COMPANY_INDUSTRY_TYPES, COMPANY_SELECT_OPTIONS } from './constants'; -interface ILinkDocument extends ILink, Document {} +import { field, schemaWrapper } from './utils'; export interface ICompany { + scopeBrandIds?: string[]; primaryName?: string; avatar?: string; names?: string[]; @@ -39,146 +24,138 @@ export interface ICompany { phones?: string[]; mergedIds?: string[]; - leadStatus?: string; status?: string; - lifecycleState?: string; businessType?: string; description?: string; employees?: number; doNotDisturb?: string; links?: ILink; tagIds?: string[]; - customFieldsData?: any; + customFieldsData?: ICustomField[]; website?: string; + code?: string; } export interface ICompanyDocument extends ICompany, Document { _id: string; - links?: ILinkDocument; status?: string; createdAt: Date; modifiedAt: Date; + searchText: string; } -const linkSchema = new Schema( - { - linkedIn: field({ type: String, optional: true, label: 'LinkedIn' }), - twitter: field({ type: String, optional: true, label: 'Twitter' }), - facebook: field({ type: String, optional: true, label: 'Facebook' }), - github: field({ type: String, optional: true, label: 'Github' }), - youtube: field({ type: String, optional: true, label: 'Youtube' }), - website: field({ type: String, optional: true, label: 'Website' }), - }, - { _id: false }, -); - -export const companySchema = new Schema({ - _id: field({ pkey: true }), - - createdAt: field({ type: Date, label: 'Created at' }), - modifiedAt: field({ type: Date, label: 'Modified at' }), - - primaryName: field({ - type: String, - label: 'Name', - }), - - names: field({ - type: [String], - optional: true, - }), - - avatar: field({ - type: String, - optional: true, - }), - - size: field({ - type: Number, - label: 'Size', - optional: true, - }), - - industry: field({ - type: String, - enum: COMPANY_INDUSTRY_TYPES, - label: 'Industry', - optional: true, - }), - - website: field({ - type: String, - label: 'Website', - optional: true, - }), - - plan: field({ - type: String, - label: 'Plan', - optional: true, - }), - - parentCompanyId: field({ - type: String, - optional: true, - label: 'Parent Company', +const getEnum = (fieldName: string): string[] => { + return COMPANY_SELECT_OPTIONS[fieldName].map(option => option.value); +}; + +export const companySchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + + createdAt: field({ type: Date, label: 'Created at', esType: 'date' }), + modifiedAt: field({ type: Date, label: 'Modified at', esType: 'date' }), + + primaryName: field({ + type: String, + label: 'Name', + optional: true, + }), + + names: field({ + type: [String], + optional: true, + label: 'Names', + }), + + avatar: field({ + type: String, + optional: true, + label: 'Avatar', + }), + + size: field({ + type: Number, + label: 'Size', + optional: true, + esType: 'number', + }), + + industry: field({ + type: String, + enum: COMPANY_INDUSTRY_TYPES, + label: 'Industry', + optional: true, + esType: 'keyword', + }), + + website: field({ + type: String, + label: 'Website', + optional: true, + }), + + plan: field({ + type: String, + label: 'Plan', + optional: true, + }), + + parentCompanyId: field({ + type: String, + optional: true, + label: 'Parent Company', + }), + + primaryEmail: field({ type: String, optional: true, label: 'Primary email', esType: 'email' }), + emails: field({ type: [String], optional: true, label: 'Emails' }), + + primaryPhone: field({ type: String, optional: true, label: 'Primary phone' }), + phones: field({ type: [String], optional: true, label: 'Phones' }), + + ownerId: field({ type: String, optional: true, label: 'Owner' }), + + status: field({ + type: String, + enum: getEnum('STATUSES'), + default: 'Active', + optional: true, + label: 'Status', + esType: 'keyword', + selectOptions: COMPANY_SELECT_OPTIONS.STATUSES, + }), + + businessType: field({ + type: String, + enum: getEnum('BUSINESS_TYPES'), + optional: true, + label: 'Business Type', + esType: 'keyword', + selectOptions: COMPANY_SELECT_OPTIONS.BUSINESS_TYPES, + }), + + description: field({ type: String, optional: true, label: 'Description' }), + employees: field({ type: Number, optional: true, label: 'Employees' }), + doNotDisturb: field({ + type: String, + optional: true, + default: 'No', + enum: getEnum('DO_NOT_DISTURB'), + label: 'Do not disturb', + selectOptions: COMPANY_SELECT_OPTIONS.DO_NOT_DISTURB, + }), + links: field({ type: Object, default: {}, label: 'Links' }), + + tagIds: field({ + type: [String], + optional: true, + label: 'Tags', + }), + + // Merged company ids + mergedIds: field({ type: [String], optional: true, label: 'Merged companies' }), + + customFieldsData: field({ type: [customFieldSchema], optional: true, label: 'Custom fields data' }), + searchText: field({ type: String, optional: true, index: true }), + code: field({ type: String, label: 'Code', optional: true }), }), - - primaryEmail: field({ type: String, optional: true, label: 'Email' }), - emails: field({ type: [String], optional: true }), - - primaryPhone: field({ type: String, optional: true, label: 'Phone' }), - phones: field({ type: [String], optional: true }), - - ownerId: field({ type: String, optional: true, label: 'Owner' }), - - leadStatus: field({ - type: String, - enum: COMPANY_LEAD_STATUS_TYPES, - optional: true, - label: 'Lead Status', - }), - - status: field({ - type: String, - enum: STATUSES.ALL, - default: STATUSES.ACTIVE, - optional: true, - label: 'Status', - }), - - lifecycleState: field({ - type: String, - enum: COMPANY_LIFECYCLE_STATE_TYPES, - optional: true, - label: 'Lifecycle State', - }), - - businessType: field({ - type: String, - enum: COMPANY_BUSINESS_TYPES, - optional: true, - label: 'Business Type', - }), - - description: field({ type: String, optional: true }), - employees: field({ type: Number, optional: true, label: 'Employees' }), - doNotDisturb: field({ - type: String, - optional: true, - label: 'Do not disturb', - }), - links: field({ type: linkSchema, default: {} }), - - tagIds: field({ - type: [String], - optional: true, - }), - - // Merged company ids - mergedIds: field({ type: [String], optional: true }), - - customFieldsData: field({ - type: Object, - }), -}); +); diff --git a/src/db/models/definitions/configs.ts b/src/db/models/definitions/configs.ts index cc80820a1..b4488ee58 100644 --- a/src/db/models/definitions/configs.ts +++ b/src/db/models/definitions/configs.ts @@ -1,9 +1,9 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IConfig { code: string; - value: string[]; + value: any; } export interface IConfigDocument extends IConfig, Document { @@ -14,6 +14,6 @@ export interface IConfigDocument extends IConfig, Document { export const configSchema = new Schema({ _id: field({ pkey: true }), - code: field({ type: String }), - value: field({ type: [String] }), + code: field({ type: String, unique: true }), + value: field({ type: Object }), }); diff --git a/src/db/models/definitions/conformities.ts b/src/db/models/definitions/conformities.ts new file mode 100644 index 000000000..075c56176 --- /dev/null +++ b/src/db/models/definitions/conformities.ts @@ -0,0 +1,64 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IConformity { + mainType: string; + mainTypeId: string; + relType: string; + relTypeId: string; +} + +export interface IConformityAdd { + mainType: string; + mainTypeId: string; + relType: string; + relTypeId: string; +} + +export interface IConformityEdit { + mainType: string; + mainTypeId: string; + relType: string; + relTypeIds: string[]; +} + +export interface IConformitySaved { + mainType: string; + mainTypeId: string; + relTypes: string[]; +} + +export interface IConformityRelated { + mainType: string; + mainTypeId: string; + relType: string; +} + +export interface IConformityChange { + type: string; + newTypeId: string; + oldTypeIds: string[]; +} + +export interface IConformityFilter { + mainType: string; + mainTypeIds: string[]; + relType: string; +} + +export interface IConformityRemove { + mainType: string; + mainTypeId: string; +} + +export interface IConformityDocument extends IConformity, Document { + _id: string; +} + +export const conformitySchema = new Schema({ + _id: field({ pkey: true }), + mainType: field({ type: String, index: true }), + mainTypeId: field({ type: String, index: true }), + relType: field({ type: String, index: true }), + relTypeId: field({ type: String, index: true }), +}); diff --git a/src/db/models/definitions/constants.ts b/src/db/models/definitions/constants.ts index f551ea9ae..3e1a52494 100644 --- a/src/db/models/definitions/constants.ts +++ b/src/db/models/definitions/constants.ts @@ -5,13 +5,20 @@ export const CONVERSATION_STATUSES = { ALL: ['new', 'open', 'closed'], }; +export const CONVERSATION_OPERATOR_STATUS = { + BOT: 'bot', + OPERATOR: 'operator', + ALL: ['bot', 'operator'], +}; + export const TAG_TYPES = { CONVERSATION: 'conversation', CUSTOMER: 'customer', ENGAGE_MESSAGE: 'engageMessage', COMPANY: 'company', INTEGRATION: 'integration', - ALL: ['conversation', 'customer', 'engageMessage', 'company', 'integration'], + PRODUCT: 'product', + ALL: ['conversation', 'customer', 'engageMessage', 'company', 'integration', 'product'], }; export const MESSENGER_KINDS = { @@ -31,10 +38,18 @@ export const SENT_AS_CHOICES = { export const METHODS = { MESSENGER: 'messenger', EMAIL: 'email', - ALL: ['messenger', 'email'], + SMS: 'sms', + ALL: ['messenger', 'email', 'sms'], +}; + +export const ENGAGE_KINDS = { + AUTO: 'auto', + MANUAL: 'manual', + VISITOR_AUTO: 'visitorAuto', + ALL: ['auto', 'manual', 'visitorAuto'], }; -export const FORM_LOAD_TYPES = { +export const LEAD_LOAD_TYPES = { SHOUTBOX: 'shoutbox', POPUP: 'popup', EMBEDDED: 'embedded', @@ -44,7 +59,7 @@ export const FORM_LOAD_TYPES = { ALL: ['', 'shoutbox', 'popup', 'embedded', 'dropdown', 'slideInLeft', 'slideInRight'], }; -export const FORM_SUCCESS_ACTIONS = { +export const LEAD_SUCCESS_ACTIONS = { EMAIL: 'email', REDIRECT: 'redirect', ONPAGE: 'onPage', @@ -53,9 +68,72 @@ export const FORM_SUCCESS_ACTIONS = { export const KIND_CHOICES = { MESSENGER: 'messenger', - FORM: 'form', - FACEBOOK: 'facebook', - ALL: ['messenger', 'form', 'facebook'], + LEAD: 'lead', + FACEBOOK_MESSENGER: 'facebook-messenger', + FACEBOOK_POST: 'facebook-post', + GMAIL: 'gmail', + NYLAS_GMAIL: 'nylas-gmail', + NYLAS_IMAP: 'nylas-imap', + NYLAS_OFFICE365: 'nylas-office365', + NYLAS_EXCHANGE: 'nylas-exchange', + NYLAS_OUTLOOK: 'nylas-outlook', + NYLAS_YAHOO: 'nylas-yahoo', + CALLPRO: 'callpro', + TWITTER_DM: 'twitter-dm', + CHATFUEL: 'chatfuel', + SMOOCH_VIBER: 'smooch-viber', + SMOOCH_LINE: 'smooch-line', + SMOOCH_TELEGRAM: 'smooch-telegram', + SMOOCH_TWILIO: 'smooch-twilio', + WHATSAPP: 'whatsapp', + TELNYX: 'telnyx', + WEBHOOK: 'webhook', + ALL: [ + 'messenger', + 'lead', + 'facebook-messenger', + 'facebook-post', + 'gmail', + 'callpro', + 'chatfuel', + 'nylas-gmail', + 'nylas-imap', + 'nylas-office365', + 'nylas-outlook', + 'nylas-exchange', + 'nylas-yahoo', + 'twitter-dm', + 'smooch-viber', + 'smooch-line', + 'smooch-telegram', + 'smooch-twilio', + 'whatsapp', + 'telnyx', + 'webhook', + ], +}; + +export const INTEGRATION_NAMES_MAP = { + messenger: 'Web messenger', + lead: 'Lead', + 'facebook-messenger': 'Facebook messenger', + 'facebook-post': 'Facebook post', + gmail: 'Gmail', + callpro: 'Call pro', + chatfuel: 'Chatfuel', + 'nylas-gmail': 'Gmail', + 'nylas-imap': 'Imap', + 'nylas-exchange': 'exchange', + 'nylas-office365': 'Office 365', + 'nylas-outlook': 'Outook', + 'nylas-yahoo': 'Yahoo', + 'twitter-dm': 'Twitter dm', + 'smooch-viber': 'Viber', + 'smooch-line': 'Line', + 'smooch-telegram': 'Telegram', + 'smooch-twilio': 'Twilio SMS', + whatsapp: 'WhatsApp', + webhook: 'Webhook', }; // messenger data availability constants @@ -72,8 +150,10 @@ export const ACTIVITY_CONTENT_TYPES = { DEAL: 'deal', TICKET: 'ticket', TASK: 'task', + PRODUCT: 'product', + GROWTH_HACK: 'growthHack', - ALL: ['customer', 'company', 'user', 'deal', 'ticket', 'task'], + ALL: ['customer', 'company', 'user', 'deal', 'ticket', 'task', 'product', 'growthHack'], }; export const PUBLISH_STATUSES = { @@ -86,6 +166,7 @@ export const ACTIVITY_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', INTERNAL_NOTE: 'internal_note', + CHECKLIST: 'checklist', CONVERSATION: 'conversation', SEGMENT: 'segment', DEAL: 'deal', @@ -93,8 +174,22 @@ export const ACTIVITY_TYPES = { TICKET: 'ticket', TASK: 'task', BRAND: 'brand', + GROWTH_HACK: 'growthHack', - ALL: ['customer', 'company', 'internal_note', 'conversation', 'segment', 'deal', 'email', 'ticket', 'task', 'brand'], + ALL: [ + 'customer', + 'company', + 'internal_note', + 'checklist', + 'conversation', + 'segment', + 'deal', + 'email', + 'ticket', + 'task', + 'brand', + 'growthHack', + ], }; export const ACTIVITY_ACTIONS = { @@ -103,8 +198,11 @@ export const ACTIVITY_ACTIONS = { DELETE: 'delete', MERGE: 'merge', SEND: 'send', + MOVED: 'moved', + CONVERT: 'convert', + ASSIGNEE: 'assignee', - ALL: ['create', 'update', 'delete', 'merge', 'send'], + ALL: ['create', 'update', 'delete', 'merge', 'send', 'moved', 'convert', 'assignee'], }; export const ACTIVITY_PERFORMER_TYPES = { @@ -128,24 +226,20 @@ export const PIPELINE_VISIBLITIES = { ALL: ['public', 'private'], }; +export const HACK_SCORING_TYPES = { + RICE: 'rice', + ICE: 'ice', + PIE: 'pie', + ALL: ['rice', 'ice', 'pie'], +}; + export const FIELDS_GROUPS_CONTENT_TYPES = { CUSTOMER: 'customer', COMPANY: 'company', - ALL: ['customer', 'company'], + PRODUCT: 'product', + ALL: ['customer', 'company', 'product'], }; -export const CUSTOMER_LEAD_STATUS_TYPES = [ - '', - 'new', - 'open', - 'inProgress', - 'openDeal', - 'unqualified', - 'attemptedToContact', - 'connected', - 'badTiming', -]; - export const CUSTOMER_LIFECYCLE_STATE_TYPES = [ '', 'subscriber', @@ -158,18 +252,6 @@ export const CUSTOMER_LIFECYCLE_STATE_TYPES = [ 'other', ]; -export const COMPANY_LEAD_STATUS_TYPES = [ - '', - 'new', - 'open', - 'inProgress', - 'openDeal', - 'unqualified', - 'attemptedToContact', - 'connected', - 'badTiming', -]; - export const COMPANY_LIFECYCLE_STATE_TYPES = [ '', 'subscriber', @@ -196,63 +278,94 @@ export const COMPANY_BUSINESS_TYPES = [ 'Other', ]; -export const COMPANY_INDUSTRY_TYPES = [ +export const DEFAULT_COMPANY_INDUSTRY_TYPES = [ '', - 'Advertising/Public Relations', - 'Aerospace, Defense Contractors', - 'Agriculture', + 'Aerospace & Defense', + 'Air Freight & Logistics', 'Airlines', - 'Alcoholic Beverages', - 'Alternative Energy Production & Services', - 'Architectural Services', - 'Attorneys/Law Firms', - 'Automotive', + 'Auto Components', + 'Automobiles', 'Banks', - 'Bars & Restaurants', - 'Books, Magazines & Newspapers', - 'Builders/General Contractors', - 'Business Services', - 'Car Manufacturers', - 'Coal Mining', - 'Colleges, Universities & Schools', - 'Commercial TV & Radio Stations', - 'Computer Software', - 'Construction', - 'Dairy', - 'Doctors & Other Health Professionals', - 'Education', - 'Energy & Natural Resources', - 'Finance, Insurance & Real Estate', - 'Food & Beverage', - 'Foundations, Philanthropists & Non-Profits', - 'Health', - 'Hotels, Motels & Tourism', + 'Beverages', + 'Biotechnology', + 'Building Products', + 'Capital Markets', + 'Chemicals', + 'Commercial Services & Supplies', + 'Communications Equipment', + 'Construction & Engineering', + 'Construction Materials', + 'Consumer Finance', + 'Containers & Packaging', + 'Distributors', + 'Diversified Consumer Services', + 'Diversified Financial Services', + 'Diversified Telecommunication Services', + 'Electric Utilities', + 'Electrical Equipment', + 'Electronic Equipment, Instruments & Components', + 'Energy Equipment & Services', + 'Entertainment', + 'Equity Real Estate Investment Trusts (REITs)', + 'Food & Staples Retailing', + 'Food Products', + 'Gas Utilities', + 'Health Care Equipment & Supplies', + 'Health Care Providers & Services', + 'Health Care Technology', + 'Hotels, Restaurants & Leisure', + 'Household Durables', + 'Household Products', + 'Independent Power and Renewable Electricity Producers', + 'Industrial Conglomerates', 'Insurance', - 'Internet', - 'Lawyers / Law Firms', - 'Meat processing & products', - 'Medical Supplies', - 'Mining', - 'Mortgage Bankers & Brokers', - 'Music Production', - 'Natural Gas Pipelines', - 'Nursing Homes/Hospitals', - 'Phone Companies', - 'Postal Unions', - 'Printing & Publishing', - 'Private Equity & Investment Firms', - 'Publishing & Printing', - 'Real Estate', - 'Retail Sales', - 'Schools/Education', - 'Sports, Professional', - 'Telecom Services & Equipment', - 'Textiles', + 'Interactive Media & Services', + 'Internet & Direct Marketing Retail', + 'IT Services', + 'Leisure Products', + 'Life Sciences Tools & Services', + 'Machinery', + 'Marine', + 'Media', + 'Metals & Mining', + 'Mortgage Real Estate Investment Trusts (REITs)', + 'Multi-Utilities', + 'Multiline Retail', + 'Oil, Gas & Consumable Fuels', + 'Paper & Forest Products', + 'Personal Products', + 'Pharmaceuticals', + 'Professional Services', + 'Real Estate Management & Development', + 'Road & Rail', + 'Semiconductors & Semiconductor Equipment', + 'Software', + 'Specialty Retail', + 'Technology Hardware, Storage & Peripherals', + 'Textiles, Apparel & luxury goods', + 'Thrifts & Mortgage Finance', 'Tobacco', + 'Trading Companies & Distributors', + 'Transportation Infrastructure', + 'Water Utilities', + 'Wireless Telecommunication Services', 'Transportation', - 'TV / Movies / Music', + 'Mining', + 'Finance', + 'Group company', + 'Government', + 'Utility', + 'Education', + 'Manufacturing', + 'Communication', + 'Retail', + 'Health', + 'Construction', + 'Management', ]; +export const COMPANY_INDUSTRY_TYPES = [...DEFAULT_COMPANY_INDUSTRY_TYPES]; + export const PROBABILITY = { TEN: '10%', TWENTY: '20%', @@ -265,20 +378,38 @@ export const PROBABILITY = { NINETY: '90%', WON: 'Won', LOST: 'Lost', - ALL: ['10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'Won', 'Lost'], + DONE: 'Done', + RESOLVED: 'Resolved', + ALL: ['10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', 'Won', 'Lost', 'Done', 'Resolved'], }; -export const STATUSES = { - ACTIVE: 'Active', - DELETED: 'Deleted', - ALL: ['Active', 'Deleted'], +export const BOARD_STATUSES = { + ACTIVE: 'active', + ARCHIVED: 'archived', + ALL: ['active', 'archived'], +}; + +export const TIME_TRACK_TYPES = { + STARTED: 'started', + STOPPED: 'stopped', + PAUSED: 'paused', + COMPLETED: 'completed', + ALL: ['started', 'stopped', 'paused', 'completed'], }; export const BOARD_TYPES = { DEAL: 'deal', TICKET: 'ticket', TASK: 'task', - ALL: ['deal', 'ticket', 'task'], + GROWTH_HACK: 'growthHack', + ALL: ['deal', 'ticket', 'task', 'growthHack'], +}; + +export const MESSAGE_TYPES = { + VIDEO_CALL: 'videoCall', + VIDEO_CALL_REQUEST: 'videoCallRequest', + TEXT: 'text', + ALL: ['videoCall', 'videoCallRequest', 'text'], }; // module constants @@ -293,6 +424,12 @@ export const NOTIFICATION_TYPES = { DEAL_CHANGE: 'dealChange', DEAL_DUE_DATE: 'dealDueDate', DEAL_DELETE: 'dealDelete', + GROWTHHACK_ADD: 'growthHackAdd', + GROWTHHACK_REMOVE_ASSIGN: 'growthHackRemoveAssign', + GROWTHHACK_EDIT: 'growthHackEdit', + GROWTHHACK_CHANGE: 'growthHackChange', + GROWTHHACK_DUE_DATE: 'growthHackDueDate', + GROWTHHACK_DELETE: 'growthHackDelete', TICKET_ADD: 'ticketAdd', TICKET_REMOVE_ASSIGN: 'ticketRemoveAssign', TICKET_EDIT: 'ticketEdit', @@ -305,6 +442,8 @@ export const NOTIFICATION_TYPES = { TASK_CHANGE: 'taskChange', TASK_DUE_DATE: 'taskDueDate', TASK_DELETE: 'taskDelete', + CUSTOMER_MENTION: 'customerMention', + COMPANY_MENTION: 'companyMention', ALL: [ 'channelMembersChange', 'conversationAddMessage', @@ -316,6 +455,12 @@ export const NOTIFICATION_TYPES = { 'dealChange', 'dealDueDate', 'dealDelete', + 'growthHackAdd', + 'growthHackRemoveAssign', + 'growthHackEdit', + 'growthHackChange', + 'growthHackDueDate', + 'growthHackDelete', 'ticketAdd', 'ticketRemoveAssign', 'ticketEdit', @@ -328,5 +473,196 @@ export const NOTIFICATION_TYPES = { 'taskChange', 'taskDueDate', 'taskDelete', + 'customerMention', + 'companyMention', + ], +}; + +export const FORM_TYPES = { + LEAD: 'lead', + GROWTH_HACK: 'growthHack', + ALL: ['lead', 'growthHack'], +}; + +export const NOTIFICATION_CONTENT_TYPES = { + TASK: 'task', + DEAL: 'deal', + COMPANY: 'company', + CUSTOMER: 'customer', + TICKET: 'ticket', + CHANNEL: 'channel', + CONVERSATION: 'conversation', + ALL: ['task', 'deal', 'company', 'customer', 'ticket', 'channel', 'conversation'], +}; + +const STATUSES = [ + { label: 'Active', value: 'Active' }, + { label: 'Deleted', value: 'Deleted' }, +]; + +export const COMPANY_SELECT_OPTIONS = { + BUSINESS_TYPES: [ + { label: 'Competitor', value: 'Competitor' }, + { label: 'Customer', value: 'Customer' }, + { label: 'Investor', value: 'Investor' }, + { label: 'Partner', value: 'Partner' }, + { label: 'Press', value: 'Press' }, + { label: 'Prospect', value: 'Prospect' }, + { label: 'Reseller', value: 'Reseller' }, + { label: 'Other', value: 'Other' }, + { label: 'Unknown', value: '' }, + ], + STATUSES, + DO_NOT_DISTURB: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' }, + { label: 'Unknown', value: '' }, ], }; + +export const DEFAULT_SOCIAL_LINKS = [ + { label: 'Facebook', value: 'facebook' }, + { label: 'Twitter', value: 'twitter' }, + { label: 'Youtube', value: 'youtube' }, + { label: 'Website', value: 'website' }, +]; + +export const SOCIAL_LINKS = [ + ...DEFAULT_SOCIAL_LINKS, + { label: 'Academia.edu', value: 'academia.edu' }, + { label: 'Chess.com', value: 'chess.com' }, + { label: 'Crunchyroll', value: 'crunchyroll' }, + { label: 'DeviantArt', value: 'deviantArt' }, + { label: 'Discord', value: 'discord' }, + { label: 'Douban', value: 'douban' }, + { label: 'eToro', value: 'eToro' }, + { label: 'Flickr', value: 'flickr' }, + { label: 'Gapo', value: 'gapo' }, + { label: 'Goodreads', value: 'goodreads' }, + { label: 'GitHub', value: 'gitHub' }, + { label: 'Instagram', value: 'instagram' }, + { label: 'KakaoStory', value: 'kakaoStory' }, + { label: 'KizlarSoruyor', value: 'kizlarSoruyor' }, + { label: 'Last.fm', value: 'last.fm' }, + { label: 'LinkedIn', value: 'linkedIn' }, + { label: 'LiveJournal', value: 'liveJournal' }, + { label: 'Pinterest', value: 'pinterest' }, + { label: 'Pixnet', value: 'pixnet' }, + { label: 'Plurk', value: 'plurk' }, + { label: 'Quora', value: 'quora' }, + { label: 'Reddit', value: 'reddit' }, + { label: 'Renren', value: 'renren' }, + { label: 'Sina Weibo', value: 'sinaWeibo' }, + { label: 'SoundCloud', value: 'soundCloud' }, + { label: 'Spotify', value: 'spotify' }, + { label: 'Steam', value: 'steam' }, + { label: 'Tagged', value: 'tagged' }, + { label: 'Taringa', value: 'taringa' }, + { label: 'Tumblr', value: 'tumblr' }, + { label: 'VK', value: 'vk' }, + { label: 'Voat', value: 'voat' }, + { label: 'Wattpad', value: 'wattpad' }, + { label: 'XING', value: 'xing' }, + { label: 'Yammer', value: 'yammer' }, + { label: 'Yelp', value: 'yelp' }, +]; + +export const DEFAULT_SEX_CHOICES = [ + { label: 'Not known', value: 0 }, + { label: 'Male', value: 1 }, + { label: 'Female', value: 2 }, + { label: 'Not applicable', value: 9 }, +]; + +export const CUSTOMER_SELECT_OPTIONS = { + SEX: [ + ...DEFAULT_SEX_CHOICES, + { label: 'co/co', value: 10 }, + { label: 'en/en', value: 11 }, + { label: 'ey/em', value: 12 }, + { label: 'he/him', value: 13 }, + { label: 'he/them', value: 14 }, + { label: 'she/her', value: 15 }, + { label: 'she/them', value: 16 }, + { label: 'they/them', value: 17 }, + { label: 'xie/hir', value: 18 }, + { label: 'yo/yo', value: 19 }, + { label: 'ze/zir', value: 20 }, + { label: 've/vis', value: 21 }, + { label: 'xe/xem', value: 22 }, + ], + EMAIL_VALIDATION_STATUSES: [ + { label: 'Valid', value: 'valid' }, + { label: 'Invalid', value: 'invalid' }, + { label: 'Accept all unverifiable', value: 'accept_all_unverifiable' }, + { label: 'Unverifiable', value: 'unverifiable' }, + { label: 'Unknown', value: 'unknown' }, + { label: 'Disposable', value: 'disposable' }, + { label: 'Catch all', value: 'catchall' }, + { label: 'Bad syntax', value: 'badsyntax' }, + ], + PHONE_VALIDATION_STATUSES: [ + { label: 'Valid', value: 'valid' }, + { label: 'Invalid', value: 'invalid' }, + { label: 'Unknown', value: 'unknown' }, + { label: 'Can receive sms', value: 'receives_sms' }, + { label: 'Unverifiable', value: 'unverifiable' }, + ], + LEAD_STATUS_TYPES: [ + { label: 'New', value: 'new' }, + { label: 'Contacted', value: 'attemptedToContact' }, + { label: 'Working', value: 'inProgress' }, + { label: 'Bad Timing', value: 'badTiming' }, + { label: 'Unqualified', value: 'unqualified' }, + { label: 'Unknown', value: '' }, + ], + STATUSES, + DO_NOT_DISTURB: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' }, + { label: 'Unknown', value: '' }, + ], + HAS_AUTHORITY: [ + { label: 'Yes', value: 'Yes' }, + { label: 'No', value: 'No' }, + { label: 'Unknown', value: '' }, + ], + STATE: [ + { label: 'Visitor', value: 'visitor' }, + { label: 'Lead', value: 'lead' }, + { label: 'Customer', value: 'customer' }, + ], +}; + +export const DEFAULT_CONSTANT_VALUES = { + sex_choices: DEFAULT_SEX_CHOICES, + company_industry_types: DEFAULT_COMPANY_INDUSTRY_TYPES.map(v => ({ label: v, value: v })), + social_links: DEFAULT_SOCIAL_LINKS, +}; + +export const SEGMENT_STRING_OPERATORS = ['e', 'dne', 'c', 'dnc']; +export const SEGMENT_BOOLEAN_OPERATORS = ['is', 'ins', 'it', 'if']; +export const SEGMENT_NUMBER_OPERATORS = ['numbere', 'numberdne', 'numberigt', 'numberilt']; +export const SEGMENT_DATE_OPERATORS = ['dateigt', 'dateilt', 'wobm', 'woam', 'wobd', 'woad', 'drlt', 'drgt']; + +export const WEBHOOK_ACTIONS = [ + { label: 'Customer created', action: 'create', type: 'customer' }, + { label: 'Customer updated', action: 'update', type: 'customer' }, + { label: 'Customer deleted', action: 'delete', type: 'customer' }, + { label: 'Company created', action: 'create', type: 'company' }, + { label: 'Company updated', action: 'update', type: 'company' }, + { label: 'Company deleted', action: 'delete', type: 'company' }, + { label: 'Knowledge Base created', action: 'create', type: 'knowledgeBaseArticle' }, + { label: 'Knowledge Base updated', action: 'update', type: 'knowledgeBaseArticle' }, + { label: 'Knowledge Base deleted', action: 'delete', type: 'knowledgeBaseArticle' }, + { label: 'User messages', action: 'create', type: 'userMessages' }, + { label: 'Customer messages', action: 'create', type: 'customerMessages' }, + { label: 'Engage messages', action: 'create', type: 'engageMessages' }, + { label: 'Popup submitted', action: 'create', type: 'popupSubmitted' }, +]; + +export const WEBHOOK_STATUS = { + AVAILABLE: 'available', + UNAVAILABLE: 'unavailable', + ALL: ['available', 'unavailable'], +}; diff --git a/src/db/models/definitions/conversationMessages.ts b/src/db/models/definitions/conversationMessages.ts index ad3849fc7..fbd497e91 100644 --- a/src/db/models/definitions/conversationMessages.ts +++ b/src/db/models/definitions/conversationMessages.ts @@ -1,5 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { MESSAGE_TYPES } from './constants'; +import { field } from './utils'; interface IEngageDataRules { kind: string; @@ -26,6 +27,7 @@ interface IEngageDataDocument extends IEngageData, Document { export interface IMessage { content?: string; + createdAt?: Date; attachments?: any; mentionedUserIds?: string[]; conversationId: string; @@ -35,8 +37,10 @@ export interface IMessage { fromBot?: boolean; isCustomerRead?: boolean; formWidgetData?: any; + botData?: any; messengerAppData?: any; engageData?: IEngageData; + contentType?: string; } export interface IMessageDocument extends IMessage, Document { @@ -67,6 +71,7 @@ const engageDataRuleSchema = new Schema( const engageDataSchema = new Schema( { + engageKind: field({ type: String }), messageId: field({ type: String }), brandId: field({ type: String }), content: field({ type: String }), @@ -80,7 +85,7 @@ const engageDataSchema = new Schema( export const messageSchema = new Schema({ _id: field({ pkey: true }), - content: field({ type: String }), + content: field({ type: String, optional: true }), attachments: [attachmentSchema], mentionedUserIds: field({ type: [String] }), conversationId: field({ type: String, index: true }), @@ -90,7 +95,13 @@ export const messageSchema = new Schema({ userId: field({ type: String, index: true }), createdAt: field({ type: Date, index: true }), isCustomerRead: field({ type: Boolean }), + botData: field({ type: Object }), formWidgetData: field({ type: Object }), messengerAppData: field({ type: Object }), engageData: field({ type: engageDataSchema }), + contentType: field({ + type: String, + enum: MESSAGE_TYPES.ALL, + default: MESSAGE_TYPES.TEXT, + }), }); diff --git a/src/db/models/definitions/conversations.ts b/src/db/models/definitions/conversations.ts index 383b7a870..bd6b01ea0 100644 --- a/src/db/models/definitions/conversations.ts +++ b/src/db/models/definitions/conversations.ts @@ -1,8 +1,9 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -import { CONVERSATION_STATUSES } from './constants'; +import { CONVERSATION_OPERATOR_STATUS, CONVERSATION_STATUSES } from './constants'; +import { field } from './utils'; export interface IConversation { + operatorStatus?: string; content?: string; integrationId: string; customerId?: string; @@ -11,6 +12,8 @@ export interface IConversation { participatedUserIds?: string[]; readUserIds?: string[]; + createdAt?: Date; + updatedAt?: Date; closedAt?: Date; closedUserId?: string; @@ -23,6 +26,8 @@ export interface IConversation { firstRespondedUserId?: string; firstRespondedDate?: Date; + + isCustomerRespondedLast?: boolean; } // Conversation schema @@ -35,7 +40,12 @@ export interface IConversationDocument extends IConversation, Document { // Conversation schema export const conversationSchema = new Schema({ _id: field({ pkey: true }), - content: field({ type: String }), + operatorStatus: field({ + type: String, + enum: CONVERSATION_OPERATOR_STATUS.ALL, + optional: true, + }), + content: field({ type: String, optional: true }), integrationId: field({ type: String, index: true }), customerId: field({ type: String }), userId: field({ type: String }), @@ -68,4 +78,6 @@ export const conversationSchema = new Schema({ firstRespondedUserId: field({ type: String }), firstRespondedDate: field({ type: Date }), + + isCustomerRespondedLast: field({ type: Boolean }), }); diff --git a/src/db/models/definitions/customers.ts b/src/db/models/definitions/customers.ts index d428be28a..6e3b54a20 100644 --- a/src/db/models/definitions/customers.ts +++ b/src/db/models/definitions/customers.ts @@ -1,12 +1,14 @@ import { Document, Schema } from 'mongoose'; -import { CUSTOMER_LEAD_STATUS_TYPES, CUSTOMER_LIFECYCLE_STATE_TYPES, STATUSES } from './constants'; +import { customFieldSchema, ICustomField, ILink } from './common'; +import { CUSTOMER_SELECT_OPTIONS } from './constants'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface ILocation { remoteAddress: string; country: string; + countryCode: string; city: string; region: string; hostname: string; @@ -23,29 +25,14 @@ export interface IVisitorContact { export interface IVisitorContactDocument extends IVisitorContact, Document {} -export interface IMessengerData { - lastSeenAt?: number; - sessionCount?: number; - isActive?: boolean; - customData?: any; -} - -export interface IMessengerDataDocument extends IMessengerData, Document {} - -export interface ILink { - linkedIn?: string; - twitter?: string; - facebook?: string; - github?: string; - youtube?: string; - website?: string; -} - -interface ILinkDocument extends ILink, Document {} - export interface ICustomer { + state?: 'visitor' | 'lead' | 'customer'; + + scopeBrandIds?: string[]; firstName?: string; lastName?: string; + birthDate?: Date; + sex?: number; primaryEmail?: string; emails?: string[]; avatar?: string; @@ -56,174 +43,204 @@ export interface ICustomer { position?: string; department?: string; leadStatus?: string; - lifecycleState?: string; hasAuthority?: string; description?: string; doNotDisturb?: string; - hasValidEmail?: boolean; + emailValidationStatus?: string; + phoneValidationStatus?: string; links?: ILink; - isUser?: boolean; + relatedIntegrationIds?: string[]; integrationId?: string; tagIds?: string[]; + + // TODO migrate after remove 1row companyIds?: string[]; + mergedIds?: string[]; status?: string; - customFieldsData?: any; - messengerData?: IMessengerData; + customFieldsData?: ICustomField[]; + trackedData?: ICustomField[]; location?: ILocation; visitorContactInfo?: IVisitorContact; - urlVisits?: any; deviceTokens?: string[]; + code?: string; + isOnline?: boolean; + lastSeenAt?: Date; + sessionCount?: number; +} + +export interface IValidationResponse { + email?: string; + phone?: string; + status: string; } export interface ICustomerDocument extends ICustomer, Document { _id: string; - messengerData?: IMessengerDataDocument; location?: ILocationDocument; - links?: ILinkDocument; visitorContactInfo?: IVisitorContactDocument; profileScore?: number; status?: string; createdAt: Date; modifiedAt: Date; deviceTokens?: string[]; + searchText?: string; } /* location schema */ -const locationSchema = new Schema( +export const locationSchema = new Schema( { - remoteAddress: String, - country: String, - city: String, - region: String, - hostname: String, - language: String, - userAgent: String, + remoteAddress: field({ type: String, label: 'Remote address', optional: true }), + country: field({ type: String, label: 'Country', optional: true }), + countryCode: field({ type: String, label: 'Country code', optional: true }), + city: field({ type: String, label: 'City', optional: true }), + region: field({ type: String, label: 'Region', optional: true }), + hostname: field({ type: String, label: 'Host name', optional: true }), + language: field({ type: String, label: 'Language', optional: true }), + userAgent: field({ type: String, label: 'User agent', optional: true }), }, { _id: false }, ); -const visitorContactSchema = new Schema( +export const visitorContactSchema = new Schema( { - email: String, - phone: String, + email: field({ type: String, label: 'Email' }), + phone: field({ type: String, label: 'Phone' }), }, { _id: false }, ); -/* - * messenger schema - */ -const messengerSchema = new Schema( - { - lastSeenAt: field({ - type: Date, - label: 'Last seen at', +const getEnum = (fieldName: string): string[] => { + return CUSTOMER_SELECT_OPTIONS[fieldName].map(option => option.value); +}; + +export const customerSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + + state: field({ + type: String, + esType: 'keyword', + label: 'State', + default: 'visitor', + enum: getEnum('STATE'), + selectOptions: CUSTOMER_SELECT_OPTIONS.STATE, }), - sessionCount: field({ + + createdAt: field({ type: Date, label: 'Created at', esType: 'date' }), + modifiedAt: field({ type: Date, label: 'Modified at', esType: 'date' }), + avatar: field({ type: String, optional: true, label: 'Avatar' }), + + firstName: field({ type: String, label: 'First name', optional: true }), + lastName: field({ type: String, label: 'Last name', optional: true }), + birthDate: field({ type: Date, label: 'Date of birth', optional: true, esType: 'date' }), + sex: field({ type: Number, - label: 'Session count', - }), - isActive: field({ - type: Boolean, - label: 'Is online', - }), - customData: field({ - type: Object, + label: 'Pronoun', optional: true, + esType: 'keyword', + default: 0, + enum: getEnum('SEX'), + selectOptions: CUSTOMER_SELECT_OPTIONS.SEX, }), - }, - { _id: false }, -); - -const linkSchema = new Schema( - { - linkedIn: field({ type: String, optional: true, label: 'LinkedIn' }), - twitter: field({ type: String, optional: true, label: 'Twitter' }), - facebook: field({ type: String, optional: true, label: 'Facebook' }), - github: field({ type: String, optional: true, label: 'Github' }), - youtube: field({ type: String, optional: true, label: 'Youtube' }), - website: field({ type: String, optional: true, label: 'Website' }), - }, - { _id: false }, -); - -export const customerSchema = new Schema({ - _id: field({ pkey: true }), - createdAt: field({ type: Date, label: 'Created at' }), - modifiedAt: field({ type: Date, label: 'Modified at' }), - avatar: field({ type: String, optional: true }), - - firstName: field({ type: String, label: 'First name', optional: true }), - lastName: field({ type: String, label: 'Last name', optional: true }), + primaryEmail: field({ type: String, label: 'Primary Email', optional: true, esType: 'email' }), + emails: field({ type: [String], optional: true, label: 'Emails' }), + emailValidationStatus: field({ + type: String, + enum: getEnum('EMAIL_VALIDATION_STATUSES'), + default: 'unknown', + label: 'Email validation status', + esType: 'keyword', + selectOptions: CUSTOMER_SELECT_OPTIONS.EMAIL_VALIDATION_STATUSES, + }), - primaryEmail: field({ type: String, label: 'Primary Email', optional: true }), - emails: field({ type: [String], optional: true }), - hasValidEmail: field({ type: Boolean, optional: true }), + primaryPhone: field({ type: String, label: 'Primary Phone', optional: true }), + phones: field({ type: [String], optional: true, label: 'Phones' }), - primaryPhone: field({ type: String, label: 'Primary Phone', optional: true }), - phones: field({ type: [String], optional: true }), - profileScore: field({ type: Number, index: true, optional: true }), + phoneValidationStatus: field({ + type: String, + enum: getEnum('PHONE_VALIDATION_STATUSES'), + default: 'unknown', + label: 'Phone validation status', + esType: 'keyword', + selectOptions: CUSTOMER_SELECT_OPTIONS.PHONE_VALIDATION_STATUSES, + }), + profileScore: field({ type: Number, index: true, optional: true, label: 'Profile score', esType: 'number' }), - ownerId: field({ type: String, optional: true }), - position: field({ type: String, optional: true, label: 'Position' }), - department: field({ type: String, optional: true, label: 'Department' }), + ownerId: field({ type: String, optional: true, label: 'Owner' }), + position: field({ type: String, optional: true, label: 'Position', esType: 'keyword' }), + department: field({ type: String, optional: true, label: 'Department' }), - leadStatus: field({ - type: String, - enum: CUSTOMER_LEAD_STATUS_TYPES, - optional: true, - label: 'Lead Status', - }), + leadStatus: field({ + type: String, + enum: getEnum('LEAD_STATUS_TYPES'), + optional: true, + label: 'Lead Status', + esType: 'keyword', + selectOptions: CUSTOMER_SELECT_OPTIONS.LEAD_STATUS_TYPES, + }), - status: field({ - type: String, - enum: STATUSES.ALL, - default: STATUSES.ACTIVE, - optional: true, - label: 'Status', - index: true, - }), + status: field({ + type: String, + enum: getEnum('STATUSES'), + optional: true, + label: 'Status', + default: 'Active', + esType: 'keyword', + index: true, + selectOptions: CUSTOMER_SELECT_OPTIONS.STATUSES, + }), - lifecycleState: field({ - type: String, - enum: CUSTOMER_LIFECYCLE_STATE_TYPES, - optional: true, - label: 'Lifecycle State', - }), + hasAuthority: field({ + type: String, + optional: true, + default: 'No', + label: 'Has authority', + enum: getEnum('HAS_AUTHORITY'), + selectOptions: CUSTOMER_SELECT_OPTIONS.HAS_AUTHORITY, + }), + description: field({ type: String, optional: true, label: 'Description' }), + doNotDisturb: field({ + type: String, + optional: true, + default: 'No', + enum: getEnum('DO_NOT_DISTURB'), + label: 'Do not disturb', + selectOptions: CUSTOMER_SELECT_OPTIONS.DO_NOT_DISTURB, + }), + links: field({ type: Object, default: {}, label: 'Links' }), - hasAuthority: field({ type: String, optional: true, label: 'Has authority' }), - description: field({ type: String, optional: true, label: 'Description' }), - doNotDisturb: field({ - type: String, - optional: true, - label: 'Do not disturb', - }), - links: field({ type: linkSchema, default: {} }), + relatedIntegrationIds: field({ type: [String], optional: true }), + integrationId: field({ type: String, optional: true, label: 'Integration', esType: 'keyword' }), + tagIds: field({ type: [String], optional: true, index: true, label: 'Tags' }), - isUser: field({ type: Boolean, label: 'Is user', optional: true }), + // Merged customer ids + mergedIds: field({ type: [String], optional: true }), - integrationId: field({ type: String, optional: true }), - tagIds: field({ type: [String], optional: true, index: true }), - companyIds: field({ type: [String], optional: true }), + trackedData: field({ type: [customFieldSchema], optional: true, label: 'Tracked Data' }), + customFieldsData: field({ type: [customFieldSchema], optional: true, label: 'Custom fields data' }), - // Merged customer ids - mergedIds: field({ type: [String], optional: true }), + location: field({ type: locationSchema, optional: true, label: 'Location' }), - customFieldsData: field({ type: Object, optional: true }), - messengerData: field({ type: messengerSchema, optional: true }), + // if customer is not a user then we will contact with this visitor using + // this information + visitorContactInfo: field({ + type: visitorContactSchema, + optional: true, + label: 'Visitor contact info', + }), - location: field({ type: locationSchema, optional: true }), + deviceTokens: field({ type: [String], default: [] }), + searchText: field({ type: String, optional: true, index: true }), + code: field({ type: String, label: 'Code', optional: true }), - // if customer is not a user then we will contact with this visitor using - // this information - visitorContactInfo: field({ - type: visitorContactSchema, - optional: true, - label: 'Visitor contact info', + isOnline: field({ + type: Boolean, + label: 'Is online', + optional: true, + }), + lastSeenAt: field({ type: Date, label: 'Last seen at', optional: true, esType: 'date' }), + sessionCount: field({ type: Number, label: 'Session count', optional: true, esType: 'number' }), }), - urlVisits: Object, - - deviceTokens: field({ type: [String], default: [] }), -}); +); diff --git a/src/db/models/definitions/deals.ts b/src/db/models/definitions/deals.ts index ee7eb09d5..b86942e8d 100644 --- a/src/db/models/definitions/deals.ts +++ b/src/db/models/definitions/deals.ts @@ -1,14 +1,21 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { customFieldSchema, ICustomField } from './common'; import { PRODUCT_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface IProduct { name: string; + categoryId?: string; + categoryCode?: string; type?: string; description?: string; sku?: string; + unitPrice?: number; + code: string; + customFieldsData?: ICustomField[]; productId?: string; + tagIds?: string[]; } export interface IProductDocument extends IProduct, Document { @@ -16,7 +23,20 @@ export interface IProductDocument extends IProduct, Document { createdAt: Date; } -interface IProductData extends Document { +export interface IProductCategory { + name: string; + code: string; + order: string; + description?: string; + parentId?: string; +} + +export interface IProductCategoryDocument extends IProductCategory, Document { + _id: string; + createdAt: Date; +} + +export interface IProductData extends Document { productId: string; uom: string; currency: string; @@ -27,10 +47,20 @@ interface IProductData extends Document { discountPercent?: number; discount?: number; amount?: number; + tickUsed?: boolean; + assignUserId?: string; +} + +interface IPaymentsData { + [key: string]: { + currency?: string; + amount?: number; + }; } export interface IDeal extends IItemCommonFields { productsData?: IProductData[]; + paymentsData?: IPaymentsData[]; } export interface IDealDocument extends IDeal, Document { @@ -39,41 +69,71 @@ export interface IDealDocument extends IDeal, Document { // Mongoose schemas ======================= -export const productSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - type: field({ - type: String, - enum: PRODUCT_TYPES.ALL, - default: PRODUCT_TYPES.PRODUCT, +export const productSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + code: field({ type: String, unique: true, label: 'Code' }), + categoryId: field({ type: String, label: 'Category' }), + type: field({ + type: String, + enum: PRODUCT_TYPES.ALL, + default: PRODUCT_TYPES.PRODUCT, + label: 'Type', + }), + tagIds: field({ type: [String], optional: true, label: 'Tags' }), + description: field({ type: String, optional: true, label: 'Description' }), + sku: field({ type: String, optional: true, label: 'Stock keeping unit' }), + unitPrice: field({ type: Number, optional: true, label: 'Unit price' }), + customFieldsData: field({ type: [customFieldSchema], optional: true, label: 'Custom fields data' }), + createdAt: field({ + type: Date, + default: new Date(), + label: 'Created at', + }), }), - description: field({ type: String, optional: true }), - sku: field({ type: String, optional: true }), // Stock Keeping Unit - createdAt: field({ - type: Date, - default: new Date(), +); + +export const productCategorySchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + code: field({ type: String, unique: true, label: 'Code' }), + order: field({ type: String, label: 'Order' }), + parentId: field({ type: String, optional: true, label: 'Parent' }), + description: field({ type: String, optional: true, label: 'Description' }), + createdAt: field({ + type: Date, + default: new Date(), + label: 'Created at', + }), }), -}); +); -const productDataSchema = new Schema( +export const productDataSchema = new Schema( { _id: field({ type: String }), - productId: field({ type: String }), - uom: field({ type: String }), // Units of measurement - currency: field({ type: String }), - quantity: field({ type: Number }), - unitPrice: field({ type: Number }), - taxPercent: field({ type: Number }), - tax: field({ type: Number }), - discountPercent: field({ type: Number }), - discount: field({ type: Number }), - amount: field({ type: Number }), + productId: field({ type: String, label: 'Product' }), + uom: field({ type: String, label: 'Units of measurement' }), + currency: field({ type: String, label: 'Currency' }), + quantity: field({ type: Number, label: 'Quantity' }), + unitPrice: field({ type: Number, label: 'Unit price' }), + taxPercent: field({ type: Number, label: 'Tax percent' }), + tax: field({ type: Number, label: 'Tax' }), + discountPercent: field({ type: Number, label: 'Discount percent' }), + discount: field({ type: Number, label: 'Discount' }), + amount: field({ type: Number, label: 'Amount' }), + tickUsed: field({ type: Boolean, label: 'TickUsed' }), + assignUserId: field({ type: String, label: 'AssignUserId' }), }, { _id: false }, ); -export const dealSchema = new Schema({ - ...commonItemFieldsSchema, +export const dealSchema = schemaWrapper( + new Schema({ + ...commonItemFieldsSchema, - productsData: field({ type: [productDataSchema] }), -}); + productsData: field({ type: [productDataSchema], label: 'Products' }), + paymentsData: field({ type: Object, optional: true, label: 'Payments' }), + }), +); diff --git a/src/db/models/definitions/emailDeliveries.ts b/src/db/models/definitions/emailDeliveries.ts index a7b283748..9f868ae6c 100644 --- a/src/db/models/definitions/emailDeliveries.ts +++ b/src/db/models/definitions/emailDeliveries.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; interface IAttachmentParams { data: string; @@ -9,49 +9,44 @@ interface IAttachmentParams { } export interface IEmailDeliveries { - cocType: string; - cocId?: string; subject: string; body: string; - toEmails: string; - cc?: string; - bcc?: string; + to: string[]; + cc?: string[]; + bcc?: string[]; attachments?: IAttachmentParams[]; - fromEmail?: string; - type?: string; - userId: string; + from: string; + kind: string; + userId?: string; + customerId?: string; + status?: string; } export interface IEmailDeliveriesDocument extends IEmailDeliveries, Document { id: string; } -// Mongoose schemas =========== - -const attachmentSchema = new Schema( - { - data: field({ type: String }), - filename: field({ type: String }), - size: field({ type: Number }), - mimeType: field({ type: String }), - }, - { _id: false }, -); +export const EMAIL_DELIVERY_STATUS = { + PENDING: 'pending', + RECEIVED: 'received', + ALL: ['pending', 'received'], +}; export const emailDeliverySchema = new Schema({ _id: field({ pkey: true }), - cocType: field({ type: String }), - cocId: field({ type: String }), - subject: field({ type: String, optional: true }), + subject: field({ type: String }), body: field({ type: String }), - toEmails: field({ type: String }), - cc: field({ type: String, optional: true }), - bcc: field({ type: String, optional: true }), - attachments: field({ type: [attachmentSchema] }), - fromEmail: field({ type: String }), + to: field({ type: [String] }), + cc: field({ type: [String], optional: true }), + bcc: field({ type: [String], optional: true }), + attachments: field({ type: [Object] }), + from: field({ type: String }), + kind: field({ type: String }), + customerId: field({ type: String }), userId: field({ type: String }), - - type: { type: String }, - createdAt: field({ type: Date, default: Date.now }), + status: field({ + type: String, + enum: EMAIL_DELIVERY_STATUS.ALL, + }), }); diff --git a/src/db/models/definitions/emailTemplates.ts b/src/db/models/definitions/emailTemplates.ts index e3669297c..2c1a690a5 100644 --- a/src/db/models/definitions/emailTemplates.ts +++ b/src/db/models/definitions/emailTemplates.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IEmailTemplate { name: string; @@ -10,8 +10,10 @@ export interface IEmailTemplateDocument extends IEmailTemplate, Document { _id: string; } -export const emailTemplateSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - content: field({ type: String, optional: true }), -}); +export const emailTemplateSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + content: field({ type: String, optional: true, label: 'Content' }), + }), +); diff --git a/src/db/models/definitions/engages.ts b/src/db/models/definitions/engages.ts index 0d142a142..2d8c58c84 100644 --- a/src/db/models/definitions/engages.ts +++ b/src/db/models/definitions/engages.ts @@ -1,13 +1,12 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { IRule, ruleSchema } from './common'; -import { MESSENGER_KINDS, METHODS, SENT_AS_CHOICES } from './constants'; +import { ENGAGE_KINDS, MESSENGER_KINDS, METHODS, SENT_AS_CHOICES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface IScheduleDate { type?: string; month?: string | number; day?: string | number; - time?: string; } interface IScheduleDateDocument extends IScheduleDate, Document {} @@ -16,6 +15,8 @@ export interface IEmail { attachments?: any; subject?: string; content?: string; + replyTo?: string; + sender?: string; templateId?: string; } @@ -25,25 +26,18 @@ export interface IMessenger { brandId?: string; kind?: string; sentAs?: string; - content?: string; + content: string; rules?: IRule[]; } interface IMessengerDocument extends IMessenger, Document {} -export interface IStats { - open: number; - click: number; - complaint: number; - delivery: number; - bounce: number; - reject: number; - send: number; - renderingfailure: number; +interface IShortMessage { + from?: string; + content: string; + fromIntegrationId: string; } -interface IStatsDocument extends IStats, Document {} - export interface IEngageMessage { kind?: string; segmentIds?: string[]; @@ -60,8 +54,11 @@ export interface IEngageMessage { email?: IEmail; scheduleDate?: IScheduleDate; messenger?: IMessenger; - deliveryReports?: any; - stats?: IStats; + lastRunAt?: Date; + shortMessage?: IShortMessage; + + totalCustomersCount?: number; + validCustomersCount?: number; } export interface IEngageMessageDocument extends IEngageMessage, Document { @@ -69,92 +66,98 @@ export interface IEngageMessageDocument extends IEngageMessage, Document { email?: IEmailDocument; messenger?: IMessengerDocument; - stats?: IStatsDocument; _id: string; } // Mongoose schemas ======================= -const scheduleDateSchema = new Schema( +export const scheduleDateSchema = new Schema( { - type: field({ type: String, optional: true }), - month: field({ type: String, optional: true }), - day: field({ type: String, optional: true }), - time: field({ type: Date, optional: true }), + type: field({ type: String, optional: true, label: 'Type' }), + month: field({ type: String, optional: true, label: 'Month' }), + day: field({ type: String, optional: true, label: 'Day' }), }, { _id: false }, ); -const emailSchema = new Schema( +export const emailSchema = new Schema( { - attachments: field({ type: Object, optional: true }), - subject: field({ type: String }), - content: field({ type: String }), - templateId: field({ type: String, optional: true }), + attachments: field({ type: Object, optional: true, label: 'Attachments' }), + subject: field({ type: String, label: 'Subject' }), + sender: field({ type: String, optional: true, label: 'Sender' }), + replyTo: field({ type: String, optional: true, label: 'Reply to' }), + content: field({ type: String, label: 'Content' }), + templateId: field({ type: String, optional: true, label: 'Template' }), }, { _id: false }, ); -const messengerSchema = new Schema( +export const messengerSchema = new Schema( { - brandId: field({ type: String }), + brandId: field({ type: String, label: 'Brand' }), kind: field({ type: String, enum: MESSENGER_KINDS.ALL, + label: 'Kind', }), sentAs: field({ type: String, enum: SENT_AS_CHOICES.ALL, + label: 'Sent as', }), - content: field({ type: String }), - rules: field({ type: [ruleSchema] }), + content: field({ type: String, label: 'Content' }), + rules: field({ type: [ruleSchema], label: 'Rules' }), }, { _id: false }, ); -const statsSchema = new Schema( +export const smsSchema = new Schema( { - open: field({ type: Number }), - click: field({ type: Number }), - complaint: field({ type: Number }), - delivery: field({ type: Number }), - bounce: field({ type: Number }), - reject: field({ type: Number }), - send: field({ type: Number }), - renderingfailure: field({ type: Number }), + from: field({ type: String, label: 'From text', optional: true }), + content: field({ type: String, label: 'SMS content' }), + fromIntegrationId: field({ type: String, label: 'Configured integration' }), }, { _id: false }, ); -export const engageMessageSchema = new Schema({ - _id: field({ pkey: true }), - kind: field({ type: String }), - segmentId: field({ type: String, optional: true }), // TODO Remove - segmentIds: field({ - type: [String], - optional: true, - }), - brandIds: field({ - type: [String], - optional: true, - }), - customerIds: field({ type: [String] }), - title: field({ type: String }), - fromUserId: field({ type: String }), - method: field({ - type: String, - enum: METHODS.ALL, +export const engageMessageSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + kind: field({ type: String, label: 'Kind', enum: ENGAGE_KINDS.ALL }), + segmentId: field({ type: String, optional: true }), // TODO Remove + segmentIds: field({ + type: [String], + optional: true, + label: 'Segments', + }), + brandIds: field({ + type: [String], + optional: true, + label: 'Brands', + }), + customerIds: field({ type: [String], label: 'Customers' }), + title: field({ type: String, label: 'Title' }), + fromUserId: field({ type: String, label: 'From user' }), + method: field({ + type: String, + enum: METHODS.ALL, + label: 'Method', + }), + isDraft: field({ type: Boolean, label: 'Is draft' }), + isLive: field({ type: Boolean, label: 'Is live' }), + stopDate: field({ type: Date, label: 'Stop date' }), + createdAt: field({ type: Date, default: Date.now, label: 'Created at' }), + tagIds: field({ type: [String], optional: true, label: 'Tags' }), + messengerReceivedCustomerIds: field({ type: [String], label: 'Received customers' }), + + email: field({ type: emailSchema, label: 'Email' }), + scheduleDate: field({ type: scheduleDateSchema, label: 'Schedule date' }), + messenger: field({ type: messengerSchema, label: 'Messenger' }), + lastRunAt: field({ type: Date, optional: true }), + + totalCustomersCount: field({ type: Number, optional: true }), + validCustomersCount: field({ type: Number, optional: true }), + + shortMessage: field({ type: smsSchema, label: 'Short message' }), }), - isDraft: field({ type: Boolean }), - isLive: field({ type: Boolean }), - stopDate: field({ type: Date }), - createdDate: field({ type: Date }), - tagIds: field({ type: [String], optional: true }), - messengerReceivedCustomerIds: field({ type: [String] }), - - email: field({ type: emailSchema }), - scheduleDate: field({ type: scheduleDateSchema }), - messenger: field({ type: messengerSchema }), - deliveryReports: field({ type: Object }), - stats: field({ type: statsSchema, default: {} }), -}); +); diff --git a/src/db/models/definitions/fields.ts b/src/db/models/definitions/fields.ts index 482fd9b57..d6835532c 100644 --- a/src/db/models/definitions/fields.ts +++ b/src/db/models/definitions/fields.ts @@ -1,13 +1,13 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { FIELDS_GROUPS_CONTENT_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface IField { contentType?: string; contentTypeId?: string; type?: string; validation?: string; - text?: string; + text: string; description?: string; options?: string[]; isRequired?: boolean; @@ -41,44 +41,47 @@ export const fieldSchema = new Schema({ _id: field({ pkey: true }), // form, customer, company - contentType: field({ type: String }), + contentType: field({ type: String, label: 'Content type' }), // formId when contentType is form - contentTypeId: field({ type: String }), + contentTypeId: field({ type: String, label: 'Content type item' }), - type: field({ type: String }), + type: field({ type: String, label: 'Type' }), validation: field({ type: String, optional: true, + label: 'Validation', }), - text: field({ type: String }), + text: field({ type: String, label: 'Text' }), description: field({ type: String, optional: true, + label: 'Description', }), options: field({ type: [String], optional: true, + label: 'Options', }), - isRequired: field({ type: Boolean }), - isDefinedByErxes: field({ type: Boolean }), - order: field({ type: Number }), - groupId: field({ type: String }), - isVisible: field({ type: Boolean, default: true }), - lastUpdatedUserId: field({ type: String }), + isRequired: field({ type: Boolean, label: 'Is required' }), + isDefinedByErxes: field({ type: Boolean, label: 'Is defined by erxes' }), + order: field({ type: Number, label: 'Order' }), + groupId: field({ type: String, label: 'Field group' }), + isVisible: field({ type: Boolean, default: true, label: 'Is visible' }), + lastUpdatedUserId: field({ type: String, label: 'Last updated by' }), }); -export const fieldGroupSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - // customer, company - contentType: field({ type: String, enum: FIELDS_GROUPS_CONTENT_TYPES.ALL }), - order: field({ type: Number }), - isDefinedByErxes: field({ type: Boolean, default: false }), - description: field({ - type: String, +export const fieldGroupSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + // customer, company + contentType: field({ type: String, enum: FIELDS_GROUPS_CONTENT_TYPES.ALL, label: 'Content type' }), + order: field({ type: Number, label: 'Order' }), + isDefinedByErxes: field({ type: Boolean, default: false, label: 'Is defined by erxes' }), + description: field({ type: String, label: 'Description' }), + // Id of user who updated the group + lastUpdatedUserId: field({ type: String, label: 'Last updated by' }), + isVisible: field({ type: Boolean, default: true, label: 'Is visible' }), }), - // Id of user who updated the group - lastUpdatedUserId: field({ type: String }), - isVisible: field({ type: Boolean, default: true }), -}); +); diff --git a/src/db/models/definitions/forms.ts b/src/db/models/definitions/forms.ts index c03a69bdc..f29038251 100644 --- a/src/db/models/definitions/forms.ts +++ b/src/db/models/definitions/forms.ts @@ -1,79 +1,110 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { IRule, ruleSchema } from './common'; - -export interface ICallout extends Document { - title?: string; - body?: string; - buttonText?: string; - featuredImage?: string; - skip?: boolean; -} - -interface ISubmission extends Document { - customerId: string; - submittedAt: Date; -} +import { FORM_TYPES } from './constants'; +import { calloutSchema, ICallout, ISubmission, submissionSchema } from './integrations'; +import { field, schemaWrapper } from './utils'; export interface IForm { title: string; code?: string; + type: string; description?: string; buttonText?: string; - themeColor?: string; - callout?: ICallout; - rules?: IRule; } export interface IFormDocument extends IForm, Document { _id: string; createdUserId: string; createdDate: Date; - viewCount: number; - contactsGathered: number; - submissions: ISubmission[]; + // TODO: remove + contactsGathered?: number; + // TODO: remove + viewCount?: number; + // TODO: remove + submissions?: ISubmission[]; + // TODO: remove + themeColor?: string; + // TODO: remove + callout?: ICallout; + // TODO: remove + rules?: IRule; } -// schema for form's callout component -const calloutSchema = new Schema( - { +// schema for form document +export const formSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), title: field({ type: String, optional: true }), - body: field({ type: String, optional: true }), + type: field({ type: String, enum: FORM_TYPES.ALL, required: true }), + description: field({ + type: String, + optional: true, + }), buttonText: field({ type: String, optional: true }), - featuredImage: field({ type: String, optional: true }), - skip: field({ type: Boolean, optional: true }), - }, - { _id: false }, -); + code: field({ type: String }), + createdUserId: field({ type: String }), + createdDate: field({ + type: Date, + default: Date.now, + }), -// schema for form submission details -const submissionSchema = new Schema( - { - customerId: field({ type: String }), - submittedAt: field({ type: Date }), - }, - { _id: false }, + // TODO: remove + themeColor: field({ + type: String, + optional: true, + }), + // TODO: remove + callout: field({ + type: calloutSchema, + optional: true, + }), + // TODO: remove + viewCount: field({ + type: Number, + optional: true, + }), + // TODO: remove + contactsGathered: field({ + type: Number, + optional: true, + }), + // TODO: remove + submissions: field({ + type: [submissionSchema], + optional: true, + }), + // TODO: remove + rules: field({ + type: [ruleSchema], + optional: true, + }), + }), ); -// schema for form document -export const formSchema = new Schema({ - _id: field({ pkey: true }), - title: field({ type: String, optional: true }), - description: field({ - type: String, - optional: true, - }), - buttonText: field({ type: String, optional: true }), - themeColor: field({ type: String, optional: true }), - code: field({ type: String }), - createdUserId: field({ type: String }), - createdDate: field({ - type: Date, - default: Date.now, +export interface IFormSubmission { + customerId?: string; + contentType?: string; + contentTypeId?: string; + formId?: string; + formFieldId?: string; + value?: JSON; + submittedAt?: Date; +} + +export interface IFormSubmissionDocument extends IFormSubmission, Document { + _id: string; +} + +// schema for form submission document +export const formSubmissionSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + customerId: field({ type: String, optional: true }), + contentType: field({ type: String, optional: true }), + contentTypeId: field({ type: String, optional: true }), + value: field({ type: Object, optional: true }), + submittedAt: field({ type: Date, default: Date.now }), + formId: field({ type: String, optional: true }), + formFieldId: field({ type: String, optional: true }), }), - callout: field({ type: calloutSchema, default: {} }), - viewCount: field({ type: Number }), - contactsGathered: field({ type: Number }), - submissions: field({ type: [submissionSchema] }), - rules: field({ type: [ruleSchema] }), -}); +); diff --git a/src/db/models/definitions/growthHacks.ts b/src/db/models/definitions/growthHacks.ts new file mode 100644 index 000000000..98bbce3f6 --- /dev/null +++ b/src/db/models/definitions/growthHacks.ts @@ -0,0 +1,32 @@ +import { Document, Schema } from 'mongoose'; +import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { field, schemaWrapper } from './utils'; + +export interface IGrowthHack extends IItemCommonFields { + voteCount?: number; + votedUserIds?: string[]; + + hackStages?: string; + reach?: number; + impact?: number; + confidence?: number; + ease?: number; +} + +export interface IGrowthHackDocument extends IGrowthHack, Document { + _id: string; +} + +export const growthHackSchema = schemaWrapper( + new Schema({ + ...commonItemFieldsSchema, + voteCount: field({ type: Number, default: 0, optional: true, label: 'Vote count' }), + votedUserIds: field({ type: [String], label: 'Voted users' }), + + hackStages: field({ type: [String], optional: true, label: 'Stages' }), + reach: field({ type: Number, default: 0, optional: true, label: 'React' }), + impact: field({ type: Number, default: 0, optional: true, label: 'Impact' }), + confidence: field({ type: Number, default: 0, optional: true, label: 'Confidence' }), + ease: field({ type: Number, default: 0, optional: true, label: 'Ease' }), + }), +); diff --git a/src/db/models/definitions/importHistory.ts b/src/db/models/definitions/importHistory.ts index 72f1095d8..04f522a81 100644 --- a/src/db/models/definitions/importHistory.ts +++ b/src/db/models/definitions/importHistory.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IImportHistory { success: number; @@ -7,7 +7,6 @@ export interface IImportHistory { total: number; ids: string[]; contentType: string; - errorMsgs?: string[]; status?: string; percentage?: number; } @@ -16,18 +15,19 @@ export interface IImportHistoryDocument extends IImportHistory, Document { _id: string; userId: string; date: Date; + errorMsgs: string[]; } export const importHistorySchema = new Schema({ _id: field({ pkey: true }), - success: field({ type: Number, default: 0 }), - failed: field({ type: Number, default: 0 }), - total: field({ type: Number }), - ids: field({ type: [String], default: [] }), - contentType: field({ type: String }), - userId: field({ type: String }), - date: field({ type: Date }), - errorMsgs: field({ type: [String], default: [] }), - status: field({ type: String, default: 'In Progress' }), - percentage: field({ type: Number, default: 0 }), + success: field({ type: Number, default: 0, label: 'Successful attempts' }), + failed: field({ type: Number, default: 0, label: 'Failed attempts' }), + total: field({ type: Number, label: 'Total attempts' }), + ids: field({ type: [String], default: [], label: 'Ids' }), + contentType: field({ type: String, label: 'Content type' }), + userId: field({ type: String, label: 'Created by' }), + date: field({ type: Date, label: 'Date of import' }), + errorMsgs: field({ type: [String], default: [], label: 'Error messages' }), + status: field({ type: String, default: 'In Progress', label: 'Status' }), + percentage: field({ type: Number, default: 0, label: 'Percentage' }), }); diff --git a/src/db/models/definitions/integrations.ts b/src/db/models/definitions/integrations.ts index e99494309..7504dbe83 100644 --- a/src/db/models/definitions/integrations.ts +++ b/src/db/models/definitions/integrations.ts @@ -1,6 +1,12 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -import { FORM_LOAD_TYPES, FORM_SUCCESS_ACTIONS, KIND_CHOICES, MESSENGER_DATA_AVAILABILITY } from './constants'; +import { IRule, ruleSchema } from './common'; +import { KIND_CHOICES, LEAD_LOAD_TYPES, LEAD_SUCCESS_ACTIONS, MESSENGER_DATA_AVAILABILITY } from './constants'; +import { field } from './utils'; + +export interface ISubmission extends Document { + customerId: string; + submittedAt: Date; +} export interface ILink { twitter?: string; @@ -28,6 +34,7 @@ export interface IMessageDataMessages { } export interface IMessengerData { + botEndpointUrl?: string; supporterIds?: string[]; notifyCustomer?: boolean; availabilityMethod?: string; @@ -40,11 +47,20 @@ export interface IMessengerData { showLauncher?: boolean; requireAuth?: boolean; forceLogoutWhenResolve?: boolean; + showVideoCallRequest?: boolean; } export interface IMessengerDataDocument extends IMessengerData, Document {} -export interface IFormData { +export interface ICallout extends Document { + title?: string; + body?: string; + buttonText?: string; + featuredImage?: string; + skip?: boolean; +} + +export interface ILeadData { loadType?: string; successAction?: string; fromEmail?: string; @@ -55,35 +71,56 @@ export interface IFormData { adminEmailContent?: string; thankContent?: string; redirectUrl?: string; + themeColor?: string; + callout?: ICallout; + rules?: IRule; + viewCount?: number; + contactsGathered?: number; + isRequireOnce?: boolean; } -export interface IFormDataDocument extends IFormData, Document {} +export interface IWebhookData { + script: string; + token: string; +} + +export interface ILeadDataDocument extends ILeadData, Document { + viewCount?: number; + contactsGathered?: number; +} export interface IUiOptions { color?: string; wallpaper?: string; logo?: string; + textColor?: string; } // subdocument schema for messenger UiOptions export interface IUiOptionsDocument extends IUiOptions, Document {} export interface IIntegration { - kind?: string; + kind: string; name?: string; brandId?: string; languageCode?: string; tagIds?: string[]; formId?: string; - formData?: IFormData; + leadData?: ILeadData; messengerData?: IMessengerData; uiOptions?: IUiOptions; + isActive?: boolean; + channelIds?: string[]; } export interface IIntegrationDocument extends IIntegration, Document { _id: string; - formData?: IFormDataDocument; + createdUserId: string; + // TODO remove + formData?: ILeadData; + leadData?: ILeadDataDocument; messengerData?: IMessengerDataDocument; + webhookData?: IWebhookData; uiOptions?: IUiOptionsDocument; } @@ -100,6 +137,7 @@ const messengerOnlineHoursSchema = new Schema( // subdocument schema for MessengerData const messengerDataSchema = new Schema( { + botEndpointUrl: field({ type: String }), supporterIds: field({ type: [String] }), notifyCustomer: field({ type: Boolean }), availabilityMethod: field({ @@ -124,53 +162,116 @@ const messengerDataSchema = new Schema( showChat: field({ type: Boolean, default: true }), showLauncher: field({ type: Boolean, default: true }), forceLogoutWhenResolve: field({ type: Boolean, default: false }), + showVideoCallRequest: field({ type: Boolean, default: false }), + }, + { _id: false }, +); + +// schema for lead's callout component +export const calloutSchema = new Schema( + { + title: field({ type: String, optional: true, label: 'Title' }), + body: field({ type: String, optional: true, label: 'Body' }), + buttonText: field({ type: String, optional: true, label: 'Button text' }), + featuredImage: field({ type: String, optional: true, label: 'Featured image' }), + skip: field({ type: Boolean, optional: true, label: 'Skip' }), }, { _id: false }, ); -// subdocument schema for FormData -const formDataSchema = new Schema( +// TODO: remove +// schema for lead submission details +export const submissionSchema = new Schema( + { + customerId: field({ type: String }), + submittedAt: field({ type: Date }), + }, + { _id: false }, +); + +// subdocument schema for LeadData +export const leadDataSchema = new Schema( { loadType: field({ type: String, - enum: FORM_LOAD_TYPES.ALL, + enum: LEAD_LOAD_TYPES.ALL, + label: 'Load type', }), successAction: field({ type: String, - enum: FORM_SUCCESS_ACTIONS.ALL, + enum: LEAD_SUCCESS_ACTIONS.ALL, optional: true, + label: 'Success action', }), fromEmail: field({ type: String, optional: true, + label: 'From email', }), userEmailTitle: field({ type: String, optional: true, + label: 'User email title', }), userEmailContent: field({ type: String, optional: true, + label: 'User email content', }), adminEmails: field({ type: [String], optional: true, + label: 'Admin emails', }), adminEmailTitle: field({ type: String, optional: true, + label: 'Admin email title', }), adminEmailContent: field({ type: String, optional: true, + label: 'Admin email content', }), thankContent: field({ type: String, optional: true, + label: 'Thank content', }), redirectUrl: field({ type: String, optional: true, + label: 'Redirect URL', + }), + themeColor: field({ + type: String, + optional: true, + label: 'Theme color code', + }), + callout: field({ + type: calloutSchema, + optional: true, + label: 'Callout', + }), + viewCount: field({ + type: Number, + optional: true, + label: 'View count', + }), + contactsGathered: field({ + type: Number, + optional: true, + label: 'Contacts gathered', + }), + rules: field({ + type: [ruleSchema], + optional: true, + label: 'Rules', + }), + isRequireOnce: field({ + type: Boolean, + optional: true, + label: 'Do now show again if already filled out', }), }, { _id: false }, @@ -180,31 +281,47 @@ const formDataSchema = new Schema( const uiOptionsSchema = new Schema( { color: field({ type: String }), + textColor: field({ type: String }), wallpaper: field({ type: String }), logo: field({ type: String }), }, { _id: false }, ); +const webhookDataSchema = new Schema( + { + script: field({ type: String, optional: true }), + token: field({ type: String }), + }, + { _id: false }, +); + // schema for integration document export const integrationSchema = new Schema({ _id: field({ pkey: true }), + createdUserId: field({ type: String, label: 'Created by' }), kind: field({ type: String, enum: KIND_CHOICES.ALL, + label: 'Kind', }), - name: field({ type: String }), - brandId: field({ type: String }), + name: field({ type: String, label: 'Name' }), + brandId: field({ type: String, label: 'Brand' }), languageCode: field({ type: String, optional: true, + label: 'Language code', }), - tagIds: field({ type: [String], optional: true }), - formId: field({ type: String }), - formData: field({ type: formDataSchema }), + tagIds: field({ type: [String], label: 'Tags' }), + formId: field({ type: String, label: 'Form' }), + leadData: field({ type: leadDataSchema, label: 'Lead data' }), + isActive: field({ type: Boolean, optional: true, default: true, label: 'Is active' }), + webhookData: field({ type: webhookDataSchema }), + // TODO: remove + formData: field({ type: leadDataSchema }), messengerData: field({ type: messengerDataSchema }), uiOptions: field({ type: uiOptionsSchema }), }); diff --git a/src/db/models/definitions/internalNotes.ts b/src/db/models/definitions/internalNotes.ts index 18b16153e..389c99883 100644 --- a/src/db/models/definitions/internalNotes.ts +++ b/src/db/models/definitions/internalNotes.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { ACTIVITY_CONTENT_TYPES } from './constants'; +import { field } from './utils'; export interface IInternalNote { contentType: string; @@ -12,7 +12,7 @@ export interface IInternalNote { export interface IInternalNoteDocument extends IInternalNote, Document { _id: string; createdUserId: string; - createdDate: Date; + createdAt: Date; } // Mongoose schemas ======================= @@ -22,15 +22,10 @@ export const internalNoteSchema = new Schema({ contentType: field({ type: String, enum: ACTIVITY_CONTENT_TYPES.ALL, + label: 'Content type', }), - contentTypeId: field({ type: String }), - content: field({ - type: String, - }), - createdUserId: field({ - type: String, - }), - createdDate: field({ - type: Date, - }), + contentTypeId: field({ type: String, label: 'Content item' }), + content: field({ type: String, label: 'Content' }), + createdUserId: field({ type: String, label: 'Created by' }), + createdAt: field({ type: Date, label: 'Created at' }), }); diff --git a/src/db/models/definitions/knowledgebase.ts b/src/db/models/definitions/knowledgebase.ts index 8a778ada2..77c76ddca 100644 --- a/src/db/models/definitions/knowledgebase.ts +++ b/src/db/models/definitions/knowledgebase.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { PUBLISH_STATUSES } from './constants'; +import { field, schemaWrapper } from './utils'; interface ICommonFields { createdBy: string; @@ -14,6 +14,8 @@ export interface IArticle { summary?: string; content?: string; status?: string; + reactionChoices?: string[]; + reactionCounts?: { [key: string]: number }; } export interface IArticleDocument extends ICommonFields, IArticle, Document { @@ -49,56 +51,57 @@ export interface ITopicDocument extends ICommonFields, ITopic, Document { // Schema for common fields const commonFields = { - createdBy: field({ type: String }), - createdDate: field({ - type: Date, - }), - modifiedBy: field({ type: String }), - modifiedDate: field({ - type: Date, - }), + createdBy: field({ type: String, label: 'Created by' }), + createdDate: field({ type: Date, label: 'Created at' }), + modifiedBy: field({ type: String, label: 'Modified by' }), + modifiedDate: field({ type: Date, label: 'Modified at' }), + title: field({ type: String, label: 'Title' }), }; export const articleSchema = new Schema({ _id: field({ pkey: true }), - title: field({ type: String }), - summary: field({ type: String, optional: true }), - content: field({ type: String }), + summary: field({ type: String, optional: true, label: 'Summary' }), + content: field({ type: String, label: 'Content' }), status: field({ type: String, enum: PUBLISH_STATUSES.ALL, default: PUBLISH_STATUSES.DRAFT, + label: 'Status', }), + reactionChoices: field({ type: [String], default: [], label: 'Reaction choices' }), + reactionCounts: field({ type: Object, label: 'Reaction counts' }), ...commonFields, }); export const categorySchema = new Schema({ _id: field({ pkey: true }), - title: field({ type: String }), - description: field({ type: String, optional: true }), - articleIds: field({ type: [String] }), - icon: field({ type: String, optional: true }), + description: field({ type: String, optional: true, label: 'Description' }), + articleIds: field({ type: [String], label: 'Articles' }), + icon: field({ type: String, optional: true, label: 'Icon' }), ...commonFields, }); -export const topicSchema = new Schema({ - _id: field({ pkey: true }), - title: field({ type: String }), - description: field({ type: String, optional: true }), - brandId: field({ type: String, optional: true }), +export const topicSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + description: field({ type: String, optional: true, label: 'Description' }), + brandId: field({ type: String, optional: true, label: 'Brand' }), - categoryIds: field({ - type: [String], - required: false, - }), + categoryIds: field({ + type: [String], + required: false, + label: 'Categories', + }), - color: field({ type: String, optional: true }), - backgroundImage: field({ type: String, optional: true }), + color: field({ type: String, optional: true, label: 'Color' }), + backgroundImage: field({ type: String, optional: true, label: 'Background image' }), - languageCode: field({ - type: String, - optional: true, - }), + languageCode: field({ + type: String, + optional: true, + label: 'Language codes', + }), - ...commonFields, -}); + ...commonFields, + }), +); diff --git a/src/db/models/definitions/messengerApps.ts b/src/db/models/definitions/messengerApps.ts index 564054605..888f4eeef 100644 --- a/src/db/models/definitions/messengerApps.ts +++ b/src/db/models/definitions/messengerApps.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IGoogleCredentials { access_token: string; @@ -21,7 +21,7 @@ export interface ILeadCredentials { export type IMessengerAppCrendentials = IGoogleCredentials | IKnowledgebaseCredentials | ILeadCredentials; export interface IMessengerApp { - kind: 'googleMeet' | 'knowledgebase' | 'lead'; + kind: 'googleMeet' | 'knowledgebase' | 'lead' | 'website'; name: string; accountId?: string; showInInbox?: boolean; @@ -33,16 +33,18 @@ export interface IMessengerAppDocument extends IMessengerApp, Document { } // Messenger apps =============== -export const messengerAppSchema = new Schema({ - _id: field({ pkey: true }), - - kind: field({ - type: String, - enum: ['googleMeet', 'knowledgebase', 'lead'], +export const messengerAppSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + + kind: field({ + type: String, + enum: ['googleMeet', 'knowledgebase', 'lead', 'website'], + }), + + name: field({ type: String }), + accountId: field({ type: String, optional: true }), + showInInbox: field({ type: Boolean, default: false }), + credentials: field({ type: Object }), }), - - name: field({ type: String }), - accountId: field({ type: String, optional: true }), - showInInbox: field({ type: Boolean, default: false }), - credentials: field({ type: Object }), -}); +); diff --git a/src/db/models/definitions/notifications.ts b/src/db/models/definitions/notifications.ts index fba2b1ca3..62d43d4db 100644 --- a/src/db/models/definitions/notifications.ts +++ b/src/db/models/definitions/notifications.ts @@ -1,13 +1,16 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { NOTIFICATION_TYPES } from './constants'; +import { field } from './utils'; export interface INotification { notifType?: string; title?: string; content?: string; link?: string; + contentType?: string; + contentTypeId?: string; receiver?: string; + action?: string; } export interface INotificationDocument extends INotification, Document { @@ -24,11 +27,17 @@ export const notificationSchema = new Schema({ type: String, enum: NOTIFICATION_TYPES.ALL, }), + action: field({ + type: String, + optional: true, + }), title: field({ type: String }), link: field({ type: String }), content: field({ type: String }), createdUser: field({ type: String }), - receiver: field({ type: String }), + receiver: field({ type: String, index: true }), + contentType: field({ type: String, index: true }), + contentTypeId: field({ type: String, index: true }), date: field({ type: Date, default: Date.now, diff --git a/src/db/models/definitions/permissions.ts b/src/db/models/definitions/permissions.ts index f458baf84..047f781fe 100644 --- a/src/db/models/definitions/permissions.ts +++ b/src/db/models/definitions/permissions.ts @@ -1,8 +1,8 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field } from './utils'; export interface IPermission { - module?: string; + module: string; action: string; userId?: string; groupId?: string; @@ -11,12 +11,11 @@ export interface IPermission { } export interface IPermissionParams { - module?: string; - actions?: string[]; + module: string; + actions: string[]; userIds?: string[]; groupIds?: string[]; - requiredActions?: string[]; - allowed?: boolean; + allowed: boolean; } export interface IPermissionDocument extends IPermission, Document { @@ -25,12 +24,12 @@ export interface IPermissionDocument extends IPermission, Document { export const permissionSchema = new Schema({ _id: field({ pkey: true }), - module: field({ type: String }), - action: field({ type: String }), - userId: field({ type: String }), - groupId: field({ type: String }), - requiredActions: field({ type: [String], default: [] }), - allowed: field({ type: Boolean, default: false }), + module: field({ type: String, label: 'Module' }), + action: field({ type: String, label: 'Action' }), + userId: field({ type: String, label: 'User' }), + groupId: field({ type: String, label: 'User group' }), + requiredActions: field({ type: [String], default: [], label: 'Required actions' }), + allowed: field({ type: Boolean, default: false, label: 'Allowed' }), }); export interface IUserGroup { @@ -44,6 +43,6 @@ export interface IUserGroupDocument extends IUserGroup, Document { export const userGroupSchema = new Schema({ _id: field({ pkey: true }), - name: field({ type: String, unique: true }), - description: field({ type: String }), + name: field({ type: String, unique: true, label: 'Name' }), + description: field({ type: String, label: 'Description' }), }); diff --git a/src/db/models/definitions/pipelineLabels.ts b/src/db/models/definitions/pipelineLabels.ts new file mode 100644 index 000000000..97b38750a --- /dev/null +++ b/src/db/models/definitions/pipelineLabels.ts @@ -0,0 +1,27 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IPipelineLabel { + name: string; + colorCode: string; + pipelineId: string; + createdBy?: string; + createdAt?: Date; +} + +export interface IPipelineLabelDocument extends IPipelineLabel, Document { + _id: string; +} + +export const pipelineLabelSchema = new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + colorCode: field({ type: String, label: 'Color code' }), + pipelineId: field({ type: String, label: 'Pipeline' }), + createdBy: field({ type: String, label: 'Created by' }), + createdAt: field({ + type: Date, + default: new Date(), + label: 'Created at', + }), +}); diff --git a/src/db/models/definitions/pipelineTemplates.ts b/src/db/models/definitions/pipelineTemplates.ts new file mode 100644 index 000000000..e83d5685b --- /dev/null +++ b/src/db/models/definitions/pipelineTemplates.ts @@ -0,0 +1,47 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IPipelineTemplateStage { + _id: string; + name: string; + formId: string; +} + +export interface IPipelineTemplate { + name: string; + description?: string; + type: string; + isDefinedByErxes: boolean; + stages: IPipelineTemplateStage[]; + createdBy: string; + createdDate: Date; +} + +export interface IPipelineTemplateDocument extends IPipelineTemplate, Document { + _id: string; +} + +export const stageSchema = new Schema( + { + _id: field({ type: String }), + name: field({ type: String, label: 'Stage name' }), + formId: field({ type: String, optional: true, label: 'Form' }), + order: field({ type: Number, label: 'Order' }), + }, + { _id: false }, +); + +export const pipelineTemplateSchema = new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + type: field({ type: String, label: 'Type' }), + description: field({ type: String, optional: true, label: 'Description' }), + stages: field({ type: [stageSchema], default: [], label: 'Stages' }), + isDefinedByErxes: field({ type: Boolean, default: false, label: 'Is defined by erxes' }), + createdBy: field({ type: String, label: 'Created by' }), + createdAt: field({ + type: Date, + default: new Date(), + label: 'Created at', + }), +}); diff --git a/src/db/models/definitions/responseTemplates.ts b/src/db/models/definitions/responseTemplates.ts index 1522ed882..29f878ae6 100644 --- a/src/db/models/definitions/responseTemplates.ts +++ b/src/db/models/definitions/responseTemplates.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IResponseTemplate { name?: string; @@ -12,10 +12,12 @@ export interface IResponseTemplateDocument extends IResponseTemplate, Document { _id: string; } -export const responseTemplateSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - content: field({ type: String }), - brandId: field({ type: String }), - files: field({ type: Array }), -}); +export const responseTemplateSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + content: field({ type: String, label: 'Content' }), + brandId: field({ type: String, label: 'Brand' }), + files: field({ type: Array, label: 'Files' }), + }), +); diff --git a/src/db/models/definitions/robot.ts b/src/db/models/definitions/robot.ts new file mode 100644 index 000000000..cb310ce6f --- /dev/null +++ b/src/db/models/definitions/robot.ts @@ -0,0 +1,42 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +// entry ==================== +export interface IRobotEntry { + parentId?: string; + isNotified: boolean; + action: string; + data: object; +} + +export interface IRobotEntryDocument extends IRobotEntry, Document { + _id: string; +} + +export const robotEntrySchema = new Schema({ + _id: field({ pkey: true }), + parentId: field({ type: String, optional: true }), + isNotified: field({ type: Boolean, default: false }), + action: field({ type: String }), + data: field({ type: Object }), +}); + +// onboarding history ==================== +export interface IOnboardingHistory { + userId: string; + totalPoint: number; + isCompleted: boolean; + completedSteps: string[]; +} + +export interface IOnboardingHistoryDocument extends IOnboardingHistory, Document { + _id: string; +} + +export const onboardingHistorySchema = new Schema({ + _id: field({ pkey: true }), + userId: field({ type: String }), + totalPoint: field({ type: Number }), + isCompleted: field({ type: Boolean }), + completedSteps: field({ type: [String] }), +}); diff --git a/src/db/models/definitions/scripts.ts b/src/db/models/definitions/scripts.ts index b47db88eb..32e60d1cb 100644 --- a/src/db/models/definitions/scripts.ts +++ b/src/db/models/definitions/scripts.ts @@ -1,5 +1,5 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { field, schemaWrapper } from './utils'; export interface IScript { name: string; @@ -12,14 +12,17 @@ export interface IScript { export interface IScriptDocument extends IScript, Document { _id: string; + scopeBrandIds?: string[]; } -export const scriptSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - messengerId: field({ type: String }), - messengerBrandCode: field({ type: String }), - kbTopicId: field({ type: String }), - leadIds: field({ type: [String] }), - leadMaps: field({ type: [Object] }), -}); +export const scriptSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + messengerId: field({ type: String, optional: true, label: 'Messenger integration' }), + messengerBrandCode: field({ type: String, optional: true, label: 'Messenger brand code' }), + kbTopicId: field({ type: String, optional: true, label: 'Knowledgebase topic' }), + leadIds: field({ type: [String], optional: true, label: 'Leads' }), + leadMaps: field({ type: [Object], optional: true }), + }), +); diff --git a/src/db/models/definitions/segments.ts b/src/db/models/definitions/segments.ts index 65972e0df..bc2dfe4cd 100644 --- a/src/db/models/definitions/segments.ts +++ b/src/db/models/definitions/segments.ts @@ -1,14 +1,32 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; -import { ACTIVITY_CONTENT_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; -export interface ICondition { - field: string; +export const CONTENT_TYPES = { + CUSTOMER: 'customer', + LEAD: 'lead', + VISITOR: 'visitor', + COMPANY: 'company', + + ALL: ['customer', 'lead', 'visitor', 'company'], +}; + +export interface IAttributeFilter { + name: string; operator: string; - type: string; - value?: string; - brandId?: string; - dateUnit?: string; + value: string; +} + +export interface ICondition { + type: 'property' | 'event'; + + propertyName?: string; + propertyOperator?: string; + propertyValue?: string; + + eventName?: string; + eventOccurence?: 'exactly' | 'atleast' | 'atmost'; + eventOccurenceValue?: number; + eventAttributeFilters?: IAttributeFilter[]; } export interface IConditionDocument extends ICondition, Document {} @@ -19,8 +37,8 @@ export interface ISegment { description?: string; subOf: string; color: string; - connector: string; conditions: ICondition[]; + scopeBrandIds?: string[]; } export interface ISegmentDocument extends ISegment, Document { @@ -28,41 +46,66 @@ export interface ISegmentDocument extends ISegment, Document { } // Mongoose schemas ======================= - -const conditionSchema = new Schema( +const eventAttributeSchema = new Schema( { - field: field({ type: String }), + name: field({ type: String }), operator: field({ type: String }), + value: field({ type: String }), + }, + { _id: false }, +); + +export const conditionSchema = new Schema( + { type: field({ type: String }), - value: field({ + propertyName: field({ type: String, optional: true, }), - dateUnit: field({ + propertyOperator: field({ type: String, optional: true, }), - brandId: field({ + propertyValue: field({ type: String, optional: true, }), + + eventName: field({ + type: String, + optional: true, + }), + + eventOccurence: field({ + type: String, + optional: true, + }), + + eventOccurenceValue: field({ + type: Number, + optional: true, + }), + + eventAttributeFilters: field({ type: [eventAttributeSchema] }), }, { _id: false }, ); -export const segmentSchema = new Schema({ - _id: field({ pkey: true }), - contentType: field({ - type: String, - enum: ACTIVITY_CONTENT_TYPES.ALL, +export const segmentSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + contentType: field({ + type: String, + enum: CONTENT_TYPES.ALL, + label: 'Content type', + }), + name: field({ type: String }), + description: field({ type: String, optional: true }), + subOf: field({ type: String, optional: true }), + color: field({ type: String }), + conditions: field({ type: [conditionSchema] }), }), - name: field({ type: String }), - description: field({ type: String, optional: true }), - subOf: field({ type: String, optional: true }), - color: field({ type: String }), - connector: field({ type: String }), - conditions: field({ type: [conditionSchema] }), -}); +); diff --git a/src/db/models/definitions/tags.ts b/src/db/models/definitions/tags.ts index 61c167976..b35aecd08 100644 --- a/src/db/models/definitions/tags.ts +++ b/src/db/models/definitions/tags.ts @@ -1,6 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { TAG_TYPES } from './constants'; +import { field, schemaWrapper } from './utils'; export interface ITag { name: string; @@ -14,14 +14,17 @@ export interface ITagDocument extends ITag, Document { createdAt: Date; } -export const tagSchema = new Schema({ - _id: field({ pkey: true }), - name: field({ type: String }), - type: field({ - type: String, - enum: TAG_TYPES.ALL, +export const tagSchema = schemaWrapper( + new Schema({ + _id: field({ pkey: true }), + name: field({ type: String, label: 'Name' }), + type: field({ + type: String, + enum: TAG_TYPES.ALL, + label: 'Type', + }), + colorCode: field({ type: String, label: 'Color code' }), + createdAt: field({ type: Date, label: 'Created at' }), + objectCount: field({ type: Number, label: 'Object count' }), }), - colorCode: field({ type: String }), - createdAt: field({ type: Date }), - objectCount: field({ type: Number }), -}); +); diff --git a/src/db/models/definitions/tasks.ts b/src/db/models/definitions/tasks.ts index 84aca92d1..8ad177afc 100644 --- a/src/db/models/definitions/tasks.ts +++ b/src/db/models/definitions/tasks.ts @@ -1,18 +1,14 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { schemaWrapper } from './utils'; -export interface ITask extends IItemCommonFields { - priority?: string; -} - -export interface ITaskDocument extends ITask, Document { +export interface ITaskDocument extends IItemCommonFields, Document { _id: string; } // Mongoose schemas ======================= -export const taskSchema = new Schema({ - ...commonItemFieldsSchema, - - priority: field({ type: String, optional: true }), -}); +export const taskSchema = schemaWrapper( + new Schema({ + ...commonItemFieldsSchema, + }), +); diff --git a/src/db/models/definitions/tickets.ts b/src/db/models/definitions/tickets.ts index 459993f9c..f3002b782 100644 --- a/src/db/models/definitions/tickets.ts +++ b/src/db/models/definitions/tickets.ts @@ -1,9 +1,8 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; import { commonItemFieldsSchema, IItemCommonFields } from './boards'; +import { field, schemaWrapper } from './utils'; export interface ITicket extends IItemCommonFields { - priority?: string; source?: string; } @@ -12,9 +11,10 @@ export interface ITicketDocument extends ITicket, Document { } // Mongoose schemas ======================= -export const ticketSchema = new Schema({ - ...commonItemFieldsSchema, +export const ticketSchema = schemaWrapper( + new Schema({ + ...commonItemFieldsSchema, - priority: field({ type: String }), - source: field({ type: String }), -}); + source: field({ type: String, label: 'Source' }), + }), +); diff --git a/src/db/models/definitions/users.ts b/src/db/models/definitions/users.ts index 31b836fbe..6032a22b9 100644 --- a/src/db/models/definitions/users.ts +++ b/src/db/models/definitions/users.ts @@ -1,5 +1,6 @@ import { Document, Schema } from 'mongoose'; -import { field } from '../utils'; +import { ILink } from './common'; +import { field } from './utils'; export interface IEmailSignature { brandId?: string; @@ -15,22 +16,13 @@ export interface IDetail { position?: string; location?: string; description?: string; + operatorPhone?: string; } export interface IDetailDocument extends IDetail, Document {} -export interface ILink { - linkedIn?: string; - twitter?: string; - facebook?: string; - github?: string; - youtube?: string; - website?: string; -} - -interface ILinkDocument extends ILink, Document {} - export interface IUser { + createdAt?: Date; username?: string; password: string; resetPasswordToken?: string; @@ -38,7 +30,6 @@ export interface IUser { registrationToken?: string; registrationTokenExpires?: Date; isOwner?: boolean; - hasSeenOnBoard?: boolean; email?: string; getNotificationByEmail?: boolean; emailSignatures?: IEmailSignature[]; @@ -46,22 +37,23 @@ export interface IUser { details?: IDetail; links?: ILink; isActive?: boolean; + brandIds?: string[]; groupIds?: string[]; deviceTokens?: string[]; + doNotDisturb?: string; } export interface IUserDocument extends IUser, Document { _id: string; emailSignatures?: IEmailSignatureDocument[]; details?: IDetailDocument; - links?: ILinkDocument; } // Mongoose schemas =============================== const emailSignatureSchema = new Schema( { - brandId: field({ type: String }), - signature: field({ type: String }), + brandId: field({ type: String, label: 'Brand' }), + signature: field({ type: String, label: 'Signature' }), }, { _id: false }, ); @@ -69,24 +61,13 @@ const emailSignatureSchema = new Schema( // Detail schema const detailSchema = new Schema( { - avatar: field({ type: String }), - shortName: field({ type: String, optional: true }), - fullName: field({ type: String }), - position: field({ type: String }), - location: field({ type: String, optional: true }), - description: field({ type: String, optional: true }), - }, - { _id: false }, -); - -const linkSchema = new Schema( - { - linkedIn: field({ type: String, optional: true }), - twitter: field({ type: String, optional: true }), - facebook: field({ type: String, optional: true }), - github: field({ type: String, optional: true }), - youtube: field({ type: String, optional: true }), - website: field({ type: String, optional: true }), + avatar: field({ type: String, label: 'Avatar' }), + shortName: field({ type: String, optional: true, label: 'Short name' }), + fullName: field({ type: String, label: 'Full name' }), + position: field({ type: String, label: 'Position' }), + location: field({ type: String, optional: true, label: 'Location' }), + description: field({ type: String, optional: true, label: 'Description' }), + operatorPhone: field({ type: String, optional: true, label: 'Company phone' }), }, { _id: false }, ); @@ -94,25 +75,31 @@ const linkSchema = new Schema( // User schema export const userSchema = new Schema({ _id: field({ pkey: true }), - username: field({ type: String }), + createdAt: field({ + type: Date, + default: Date.now, + }), + username: field({ type: String, label: 'Username' }), password: field({ type: String }), resetPasswordToken: field({ type: String }), registrationToken: field({ type: String }), registrationTokenExpires: field({ type: Date }), resetPasswordExpires: field({ type: Date }), - isOwner: field({ type: Boolean }), - hasSeenOnBoard: field({ type: Boolean }), + isOwner: field({ type: Boolean, label: 'Is owner' }), email: field({ type: String, unique: true, - match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,8})+$/, 'Please fill a valid email address'], + match: [/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,10})+$/, 'Please fill a valid email address'], + label: 'Email', }), - getNotificationByEmail: field({ type: Boolean }), - emailSignatures: field({ type: [emailSignatureSchema] }), - starredConversationIds: field({ type: [String] }), - details: field({ type: detailSchema, default: {} }), - links: field({ type: linkSchema, default: {} }), - isActive: field({ type: Boolean, default: true }), - groupIds: field({ type: [String] }), - deviceTokens: field({ type: [String], default: [] }), + getNotificationByEmail: field({ type: Boolean, label: 'Get notification by email' }), + emailSignatures: field({ type: [emailSignatureSchema], label: 'Email signatures' }), + starredConversationIds: field({ type: [String], label: 'Starred conversations' }), + details: field({ type: detailSchema, default: {}, label: 'Details' }), + links: field({ type: Object, default: {}, label: 'Links' }), + isActive: field({ type: Boolean, default: true, label: 'Is active' }), + brandIds: field({ type: [String], label: 'Brands' }), + groupIds: field({ type: [String], label: 'Groups' }), + deviceTokens: field({ type: [String], default: [], label: 'Device tokens' }), + doNotDisturb: field({ type: String, optional: true, default: 'No', label: 'Do not disturb' }), }); diff --git a/src/db/models/definitions/utils.ts b/src/db/models/definitions/utils.ts new file mode 100644 index 000000000..faa05981e --- /dev/null +++ b/src/db/models/definitions/utils.ts @@ -0,0 +1,26 @@ +import * as Random from 'meteor-random'; + +/* + * Mongoose field options wrapper + */ +export const field = options => { + const { pkey, type, optional } = options; + + if (type === String && !pkey && !optional) { + options.validate = /\S+/; + } + + // TODO: remove + if (pkey) { + options.type = String; + options.default = () => Random.id(); + } + + return options; +}; + +export const schemaWrapper = schema => { + schema.add({ scopeBrandIds: [String] }); + + return schema; +}; diff --git a/src/db/models/definitions/webhook.ts b/src/db/models/definitions/webhook.ts new file mode 100644 index 000000000..3fdfd9718 --- /dev/null +++ b/src/db/models/definitions/webhook.ts @@ -0,0 +1,40 @@ +import { Document, Schema } from 'mongoose'; +import { field } from './utils'; + +export interface IWebhookAction { + action?: string; + type?: string; + label?: string; +} + +const webhookActionSchema = new Schema( + { + action: field({ type: String }), + type: field({ type: String }), + label: field({ type: String }), + }, + { _id: false }, +); + +export interface IWebhookActionDocument extends IWebhookAction, Document {} + +export interface IWebhook { + url: string; + token?: string; + actions: IWebhookActionDocument[]; + status?: string; +} + +export interface IWebhookDocument extends IWebhook, Document { + _id: string; +} + +// Mongoose schemas =========== + +export const webhookSchema = new Schema({ + _id: field({ pkey: true }), + url: field({ type: String, required: true, unique: true }), + token: field({ type: String }), + actions: field({ type: [webhookActionSchema], label: 'actions' }), + status: field({ type: String }), +}); diff --git a/src/db/models/index.ts b/src/db/models/index.ts index 348028990..cd9baed6a 100644 --- a/src/db/models/index.ts +++ b/src/db/models/index.ts @@ -2,8 +2,10 @@ import ActivityLogs from './ActivityLogs'; import { Boards, Pipelines, Stages } from './Boards'; import Brands from './Brands'; import Channels from './Channels'; +import { ChecklistItems, Checklists } from './Checklists'; import Companies from './Companies'; import Configs from './Configs'; +import Conformities from './Conformities'; import ConversationMessages from './ConversationMessages'; import Conversations from './Conversations'; import Customers from './Customers'; @@ -12,7 +14,8 @@ import EmailDeliveries from './EmailDeliveries'; import EmailTemplates from './EmailTemplates'; import EngageMessages from './Engages'; import { Fields, FieldsGroups } from './Fields'; -import Forms from './Forms'; +import { Forms, FormSubmissions } from './Forms'; +import GrowthHacks from './GrowthHacks'; import ImportHistory from './ImportHistory'; import Integrations from './Integrations'; import InternalNotes from './InternalNotes'; @@ -20,16 +23,21 @@ import { KnowledgeBaseArticles, KnowledgeBaseCategories, KnowledgeBaseTopics } f import MessengerApps from './MessengerApps'; import { NotificationConfigurations, Notifications } from './Notifications'; import { Permissions, UsersGroups } from './Permissions'; -import Products from './Products'; +import PipelineLabels from './PipelineLabels'; +import PipelineTemplates from './PipelineTemplates'; +import { ProductCategories, Products } from './Products'; import ResponseTemplates from './ResponseTemplates'; +import { OnboardingHistories, RobotEntries } from './Robot'; import Scripts from './Scripts'; import Segments from './Segments'; import Tags from './Tags'; import Tasks from './Tasks'; import Tickets from './Tickets'; import Users from './Users'; +import Webhooks from './Webhook'; export { + EmailDeliveries, Users, Channels, ResponseTemplates, @@ -38,6 +46,7 @@ export { Brands, Integrations, Forms, + FormSubmissions, EngageMessages, Tags, Fields, @@ -45,7 +54,7 @@ export { InternalNotes, Customers, Companies, - EmailDeliveries, + Conformities, Conversations, ConversationMessages, KnowledgeBaseArticles, @@ -59,6 +68,7 @@ export { Stages, Deals, Products, + ProductCategories, Configs, FieldsGroups, ImportHistory, @@ -67,4 +77,12 @@ export { UsersGroups, Tickets, Tasks, + RobotEntries, + GrowthHacks, + PipelineTemplates, + PipelineLabels, + Checklists, + ChecklistItems, + OnboardingHistories, + Webhooks, }; diff --git a/src/db/models/utils.ts b/src/db/models/utils.ts deleted file mode 100644 index 67a3b9c2a..000000000 --- a/src/db/models/utils.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as Random from 'meteor-random'; -import { COMPANY_BASIC_INFOS, CUSTOMER_BASIC_INFOS } from '../../data/constants'; -import { Fields } from './'; - -/* - * Mongoose field options wrapper - */ -export const field = options => { - const { pkey, type, optional } = options; - - if (type === String && !pkey && !optional) { - options.validate = /\S+/; - } - - // TODO: remove - if (pkey) { - options.type = String; - options.default = () => Random.id(); - } - - return options; -}; - -// Checking field names, All field names must be configured correctly -export const checkFieldNames = async (type: string, fields: string[]) => { - let basicInfos = CUSTOMER_BASIC_INFOS; - - if (type === 'company') { - basicInfos = COMPANY_BASIC_INFOS; - } - - const properties: any[] = []; - - for (const fieldName of fields) { - const property: { [key: string]: any } = {}; - - const fieldObj = await Fields.findOne({ text: fieldName }); - - // Collecting basic fields - if (basicInfos.includes(fieldName)) { - property.name = fieldName; - property.type = 'basic'; - } - - // Collecting messengerData.customData fields - if (fieldName.startsWith('messengerData.customData')) { - property.name = fieldName; - property.type = 'customData'; - } - - // Collecting custom fields - if (fieldObj) { - property.type = 'customProperty'; - property.id = fieldObj._id; - } - - if (!property.type) { - throw new Error('Bad column name'); - } - - properties.push(property); - } - - return properties; -}; diff --git a/src/db/watchers.ts b/src/db/watchers.ts new file mode 100644 index 000000000..8be8b8b04 --- /dev/null +++ b/src/db/watchers.ts @@ -0,0 +1,49 @@ +import { getEnv } from '../data/utils'; +import { fetchElk } from '../elasticsearch'; +import { Companies, Customers } from './models'; + +const sendElkRequest = (data, index: string) => { + const { operationType, documentKey } = data; + + switch (operationType) { + case 'update': { + const body = { + doc: data.updateDescription.updatedFields || {}, + }; + + return fetchElk('update', index, body, documentKey._id); + } + + case 'insert': { + const body = data.fullDocument || {}; + + delete body._id; + + return fetchElk('create', index, body, documentKey._id); + } + } + + return fetchElk('delete', index, {}, documentKey._id); +}; + +const init = () => { + if (!(process.env.MONGO_URL || '').includes('replicaSet')) { + return; + } + + const ELK_SYNCER = getEnv({ name: 'ELK_SYNCER', defaultValue: 'true' }); + + if (ELK_SYNCER === 'true') { + return; + } + + Customers.watch().on('change', data => { + sendElkRequest(data, 'customers'); + }); + + Companies.watch().on('change', data => { + sendElkRequest(data, 'companies'); + }); +}; + +export default init; diff --git a/src/debuggers.ts b/src/debuggers.ts index 519a732e1..0ba826b19 100644 --- a/src/debuggers.ts +++ b/src/debuggers.ts @@ -12,6 +12,7 @@ export const debugEmail = debug('erxes-api:email'); export const debugRequest = (debugInstance, req) => debugInstance(` Receiving ${req.path} request from ${req.headers.origin} + header: ${JSON.stringify(req.headers || {})} body: ${JSON.stringify(req.body || {})} queryParams: ${JSON.stringify(req.query)} `); diff --git a/src/elasticsearch.ts b/src/elasticsearch.ts new file mode 100644 index 000000000..4b52bf5da --- /dev/null +++ b/src/elasticsearch.ts @@ -0,0 +1,58 @@ +import * as dotenv from 'dotenv'; +import * as elasticsearch from 'elasticsearch'; +import * as telemetry from 'erxes-telemetry'; +import * as mongoUri from 'mongo-uri'; +import { debugBase } from './debuggers'; + +// load environment variables +dotenv.config(); + +const { NODE_ENV, MONGO_URL, ELASTICSEARCH_URL = 'http://localhost:9200' } = process.env; + +export const client = new elasticsearch.Client({ + hosts: [ELASTICSEARCH_URL], +}); + +export const getMappings = async (index: string) => { + return client.indices.getMapping({ index }); +}; + +export const getIndexPrefix = () => { + if (ELASTICSEARCH_URL === 'https://elasticsearch.erxes.io' && NODE_ENV === 'production') { + return `${telemetry.getMachineId().toString()}__`; + } + + const uriObject = mongoUri.parse(MONGO_URL); + const dbName = uriObject.database; + + return `${dbName}__`; +}; + +export const fetchElk = async (action, index: string, body: any, id?: string, defaultValue?: any) => { + if (NODE_ENV === 'test') { + return action === 'search' ? { hits: { total: { value: 0 }, hits: [] } } : 0; + } + + try { + const params: { index: string; body: any; id?: string } = { + index: `${getIndexPrefix()}${index}`, + body, + }; + + if (id) { + params.id = id; + } + + const response = await client[action](params); + + return response; + } catch (e) { + debugBase(`Error during elk query ${e}`); + + if (defaultValue) { + return defaultValue; + } + + throw new Error(e); + } +}; diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 000000000..dad218d82 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,190 @@ +import * as getUuid from 'uuid-by-string'; +import { Customers, Fields } from './db/models'; +import { debugBase } from './debuggers'; +import { client, fetchElk, getIndexPrefix } from './elasticsearch'; + +interface ISaveEventArgs { + type?: string; + name?: string; + customerId?: string; + attributes?: any; + additionalQuery?: any; +} + +interface ICustomerIdentifyParams { + email?: string; + phone?: string; + code?: string; + integrationId?: string; +} + +export const saveEvent = async (args: ISaveEventArgs) => { + const { type, name, attributes, additionalQuery } = args; + + if (!type) { + throw new Error('Type is required'); + } + + if (!name) { + throw new Error('Name is required'); + } + + let customerId = args.customerId; + let newlyCreatedId; + + if (!customerId) { + customerId = await Customers.createVisitor(); + newlyCreatedId = customerId; + } + + const searchQuery = { + bool: { + must: [{ term: { name } }, { term: { customerId } }], + }, + }; + + if (additionalQuery) { + searchQuery.bool.must.push(additionalQuery); + } + + const index = `${getIndexPrefix()}events`; + + try { + const response = await client.update({ + index, + // generate unique id based on searchQuery + id: getUuid(JSON.stringify(searchQuery)), + body: { + script: { source: 'ctx._source["count"] += 1', lang: 'painless' }, + upsert: { + type, + name, + customerId, + createdAt: new Date(), + count: 1, + attributes: Fields.generateTypedListFromMap(attributes || {}), + }, + }, + }); + + debugBase(`Response ${JSON.stringify(response)}`); + } catch (e) { + debugBase(`Save event error ${e.message}`); + + if (newlyCreatedId) { + await Customers.remove({ _id: newlyCreatedId }); + } + + customerId = undefined; + } + + return { customerId }; +}; + +export const getNumberOfVisits = async (customerId: string, url: string): Promise => { + try { + const response = await fetchElk('search', 'events', { + query: { + bool: { + must: [{ term: { name: 'viewPage' } }, { term: { customerId } }, { term: { 'attributes.url.keyword': url } }], + }, + }, + }); + + const hits = response.hits.hits; + + if (hits.length === 0) { + return 0; + } + + const [firstHit] = hits; + + return firstHit._source.count; + } catch (e) { + debugBase(`Error occured during getNumberOfVisits ${e.message}`); + return 0; + } +}; + +export const trackViewPageEvent = (args: { customerId: string; attributes: any }) => { + const { attributes, customerId } = args; + + return saveEvent({ + type: 'lifeCycle', + name: 'viewPage', + customerId, + attributes, + additionalQuery: { + bool: { + must: [ + { + term: { + 'attributes.field': 'url', + }, + }, + { + term: { + 'attributes.value': attributes.url, + }, + }, + ], + }, + }, + }); +}; + +export const trackCustomEvent = (args: { name: string; customerId: string; attributes: any }) => { + return saveEvent({ + type: 'custom', + name: args.name, + customerId: args.customerId, + attributes: args.attributes, + }); +}; + +export const identifyCustomer = async (args: ICustomerIdentifyParams) => { + // get or create customer + let customer = await Customers.getWidgetCustomer(args); + + if (!customer) { + customer = await Customers.createCustomer({ + primaryEmail: args.email, + code: args.code, + primaryPhone: args.phone, + }); + } + + return { customerId: customer._id }; +}; + +export const updateCustomerProperty = async ({ + customerId, + name, + value, +}: { + customerId: string; + name: string; + value: any; +}) => { + if (!customerId) { + throw new Error('Customer id is required'); + } + + let modifier: any = { [name]: value }; + + if (!['firstName', 'lastName', 'primaryPhone', 'primaryEmail', 'code'].includes(name)) { + const customer = await Customers.findOne({ _id: customerId }); + + if (customer) { + const prev = {}; + (customer.trackedData || []).forEach(td => (prev[td.field] = td.value)); + prev[name] = value; + + modifier = { trackedData: Fields.generateTypedListFromMap(prev) }; + } + } + + await Customers.updateOne({ _id: customerId }, { $set: modifier }); + + return { status: 'ok' }; +}; diff --git a/src/index.ts b/src/index.ts index e04db77d6..8daa918f6 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,107 +1,248 @@ -import * as bodyParser from 'body-parser'; import * as cookieParser from 'cookie-parser'; import * as cors from 'cors'; import * as dotenv from 'dotenv'; +import * as telemetry from 'erxes-telemetry'; import * as express from 'express'; -import * as formidable from 'formidable'; import * as fs from 'fs'; +import * as helmet from 'helmet'; import { createServer } from 'http'; +import * as mongoose from 'mongoose'; import * as path from 'path'; import * as request from 'request'; +import { filterXSS } from 'xss'; import apolloServer from './apolloClient'; -import { companiesExport, customersExport } from './data/modules/coc/exporter'; +import { buildFile } from './data/modules/fileExporter/exporter'; +import { templateExport } from './data/modules/fileExporter/templateExport'; import insightExports from './data/modules/insights/insightExports'; -import { handleEngageUnSubscribe } from './data/resolvers/mutations/engageUtils'; -import { checkFile, getEnv, readFileRequest, uploadFile } from './data/utils'; -import { connect } from './db/connection'; -import { debugExternalApi, debugInit } from './debuggers'; -import integrationsApiMiddleware from './middlewares/integrationsApiMiddleware'; +import { + authCookieOptions, + deleteFile, + frontendEnv, + getEnv, + getSubServiceDomain, + handleUnsubscription, + readFileRequest, + registerOnboardHistory, +} from './data/utils'; +import { updateContactsValidationStatus, updateContactValidationStatus } from './data/verifierUtils'; +import { connect, mongoStatus } from './db/connection'; +import { Users } from './db/models'; +import initWatchers from './db/watchers'; +import { debugBase, debugExternalApi, debugInit } from './debuggers'; +import { identifyCustomer, trackCustomEvent, trackViewPageEvent, updateCustomerProperty } from './events'; +import { initMemoryStorage } from './inmemoryStorage'; +import { initBroker } from './messageBroker'; +import { importer, uploader } from './middlewares/fileMiddleware'; import userMiddleware from './middlewares/userMiddleware'; -import { initRedis } from './redisClient'; -import { init } from './startup'; +import webhookMiddleware from './middlewares/webhookMiddleware'; +import widgetsMiddleware from './middlewares/widgetsMiddleware'; +import init from './startup'; // load environment variables dotenv.config(); -const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN', defaultValue: '' }); -const WIDGETS_DOMAIN = getEnv({ name: 'WIDGETS_DOMAIN', defaultValue: '' }); +const { NODE_ENV, JWT_TOKEN_SECRET } = process.env; -// firebase app initialization -fs.exists(path.join(__dirname, '..', '/google_cred.json'), exists => { - if (!exists) { - return; - } +if (!JWT_TOKEN_SECRET) { + throw new Error('Please configure JWT_TOKEN_SECRET environment variable.'); +} - const admin = require('firebase-admin').default; - const serviceAccount = require('../google_cred.json'); - const firebaseServiceAccount = serviceAccount; +const pipeRequest = (req: any, res: any, next: any, url: string) => { + return req.pipe( + request + .post(url) + .on('response', response => { + if (response.statusCode !== 200) { + return next(response.statusMessage); + } - if (firebaseServiceAccount.private_key) { - admin.initializeApp({ - credential: admin.credential.cert(firebaseServiceAccount), - }); + return response.pipe(res); + }) + .on('error', e => { + debugExternalApi(`Error from pipe ${e.message}`); + next(e); + }), + ); +}; + +const handleTelnyxWebhook = (req, res, next, hookName: string) => { + const ENGAGES_API_DOMAIN = getSubServiceDomain({ name: 'ENGAGES_API_DOMAIN' }); + + if (NODE_ENV === 'test') { + return res.json(req.body); } -}); -// connect to mongo database -connect(); + return pipeRequest(req, res, next, `${ENGAGES_API_DOMAIN}/telnyx/${hookName}`); +}; + +const MAIN_APP_DOMAIN = getEnv({ name: 'MAIN_APP_DOMAIN' }); +const WIDGETS_DOMAIN = getSubServiceDomain({ name: 'WIDGETS_DOMAIN' }); +const INTEGRATIONS_API_DOMAIN = getSubServiceDomain({ name: 'INTEGRATIONS_API_DOMAIN' }); +const CLIENT_PORTAL_DOMAIN = getSubServiceDomain({ name: 'CLIENT_PORTAL_DOMAIN' }); + +export const app = express(); + +app.disable('x-powered-by'); -// connect to redis server -initRedis(); +// handle engage trackers +app.post(`/service/engage/tracker`, async (req, res, next) => { + const ENGAGES_API_DOMAIN = getSubServiceDomain({ name: 'ENGAGES_API_DOMAIN' }); -const app = express(); + debugBase('SES notification received ======'); + + return pipeRequest(req, res, next, `${ENGAGES_API_DOMAIN}/service/engage/tracker`); +}); + +app.use(express.urlencoded({ extended: true })); -app.use(bodyParser.urlencoded({ extended: true })); app.use( - bodyParser.json({ - limit: '10mb', + express.json({ + limit: '15mb', }), ); + +// relay telnyx sms web hook +app.post(`/telnyx/webhook`, (req, res, next) => { + return handleTelnyxWebhook(req, res, next, 'webhook'); +}); + +// relay telnyx sms web hook fail over url +app.post(`/telnyx/webhook-failover`, (req, res, next) => { + return handleTelnyxWebhook(req, res, next, 'webhook-failover'); +}); + app.use(cookieParser()); const corsOptions = { credentials: true, - origin: [MAIN_APP_DOMAIN, WIDGETS_DOMAIN], + origin: [MAIN_APP_DOMAIN, WIDGETS_DOMAIN, CLIENT_PORTAL_DOMAIN], }; app.use(cors(corsOptions)); +app.use(helmet({ frameguard: { action: 'sameorigin' } })); + +app.get('/initial-setup', async (req: any, res) => { + const userCount = await Users.countDocuments(); + + if (userCount === 0) { + return res.send('no owner'); + } + + const envMaps = JSON.parse(req.query.envs || '{}'); + + for (const key of Object.keys(envMaps)) { + res.cookie(key, envMaps[key], authCookieOptions(req.secure)); + } + + return res.send('success'); +}); + +app.post('/webhooks/:id', webhookMiddleware); +app.get('/script-manager', cors({ origin: '*' }), widgetsMiddleware); + +// events +app.post('/events-receive', async (req, res) => { + const { name, customerId, attributes } = req.body; + + try { + const response = + name === 'pageView' + ? await trackViewPageEvent({ customerId, attributes }) + : await trackCustomEvent({ name, customerId, attributes }); + return res.json(response); + } catch (e) { + debugBase(e.message); + return res.json({ status: 'success' }); + } +}); + +app.post('/events-identify-customer', async (req, res) => { + const { args } = req.body; + + try { + const response = await identifyCustomer(args); + return res.json(response); + } catch (e) { + debugBase(e.message); + return res.json({}); + } +}); + +app.post('/events-update-customer-property', async (req, res) => { + try { + const response = await updateCustomerProperty(req.body); + return res.json(response); + } catch (e) { + debugBase(e.message); + return res.json({}); + } +}); + app.use(userMiddleware); app.use('/static', express.static(path.join(__dirname, 'private'))); +app.get('/download-template', async (req: any, res) => { + const name = req.query.name; + + registerOnboardHistory({ type: `${name}Download`, user: req.user }); + + return res.redirect(`${frontendEnv({ name: 'API_URL', req })}/static/importTemplates/${name}`); +}); + // for health check -app.get('/status', async (_req, res) => { +app.get('/health', async (_req, res, next) => { + try { + await mongoStatus(); + } catch (e) { + debugBase('MongoDB is not running'); + return next(e); + } + res.end('ok'); }); -// export coc -app.get('/coc-export', async (req: any, res) => { - const { query, user } = req; - const { type } = query; - +// export insights +app.get('/insights-export', async (req: any, res) => { try { - const { name, response } = - type === 'customers' ? await customersExport(query, user) : await companiesExport(query, user); + const { name, response } = await insightExports(req.query, req.user); res.attachment(`${name}.xlsx`); return res.send(response); } catch (e) { - return res.end(e.message); + return res.end(filterXSS(e.message)); } }); -// export insights -app.get('/insights-export', async (req: any, res) => { +// export board +app.get('/file-export', async (req: any, res) => { + const { query, user } = req; + + let result: { name: string; response: string }; + try { - const { name, response } = await insightExports(req.query, req.user); + result = await buildFile(query, user); - res.attachment(`${name}.xlsx`); + res.attachment(`${result.name}.xlsx`); + return res.send(result.response); + } catch (e) { + return res.end(filterXSS(e.message)); + } +}); + +app.get('/template-export', async (req: any, res) => { + const { importType } = req.query; + + try { + const { name, response } = await templateExport(req.query); + + res.attachment(`${name}.${importType}`); return res.send(response); } catch (e) { - return res.end(e.message); + return res.end(filterXSS(e.message)); } }); @@ -120,79 +261,88 @@ app.get('/read-file', async (req: any, res) => { return res.send(response); } catch (e) { - return res.end(e.message); + return res.end(filterXSS(e.message)); } }); -// file upload -app.post('/upload-file', async (req, res) => { - const form = new formidable.IncomingForm(); - - form.parse(req, async (_error, _fields, response) => { - const file = response.file || response.upload; - - // check file ==== - const status = await checkFile(file); +// get mail attachment file +app.get('/read-mail-attachment', async (req: any, res) => { + const { messageId, attachmentId, kind, integrationId, filename, contentType } = req.query; - if (status === 'ok') { - try { - const result = await uploadFile(file, response.upload ? true : false); + if (!messageId || !attachmentId || !integrationId || !contentType) { + return res.status(404).send('Attachment not found'); + } - return res.send(result); - } catch (e) { - return res.status(500).send(e.message); - } - } + const integrationPath = kind.includes('nylas') ? 'nylas' : kind; - return res.status(500).send(status); - }); + res.redirect( + `${INTEGRATIONS_API_DOMAIN}/${integrationPath}/get-attachment?messageId=${messageId}&attachmentId=${attachmentId}&integrationId=${integrationId}&filename=${filename}&contentType=${contentType}&userId=${req.user._id}`, + ); }); -// file import -app.post('/import-file', async (req: any, res, next) => { +// delete file +app.post('/delete-file', async (req: any, res) => { // require login if (!req.user) { return res.end('foribidden'); } - const WORKERS_API_DOMAIN = getEnv({ name: 'WORKERS_API_DOMAIN' }); - - debugExternalApi(`Pipeing request to ${WORKERS_API_DOMAIN}`); + const status = await deleteFile(req.body.fileName); - return req.pipe( - request - .post(`${WORKERS_API_DOMAIN}/import-file`) - .on('response', response => { - if (response.statusCode !== 200) { - return next(response.statusMessage); - } + if (status === 'ok') { + return res.send(status); + } - return response.pipe(res); - }) - .on('error', e => { - debugExternalApi(`Error from pipe ${e.message}`); - next(e); - }), - ); + return res.status(500).send(status); }); -// engage unsubscribe -app.get('/unsubscribe', async (req, res) => { - const unsubscribed = await handleEngageUnSubscribe(req.query); +app.post('/upload-file', uploader); + +app.post('/upload-file&responseType=json', uploader); - if (unsubscribed) { - res.setHeader('Content-Type', 'text/html'); - const template = fs.readFileSync(__dirname + '/private/emailTemplates/unsubscribe.html'); - res.send(template); +// redirect to integration +app.get('/connect-integration', async (req: any, res, _next) => { + if (!req.user) { + return res.end('forbidden'); } - res.end(); + const { link, kind } = req.query; + + return res.redirect(`${INTEGRATIONS_API_DOMAIN}/${link}?kind=${kind}&userId=${req.user._id}`); +}); + +// file import +app.post('/import-file', importer); + +// unsubscribe +app.get('/unsubscribe', async (req: any, res) => { + await handleUnsubscription(req.query); + + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + + const template = fs.readFileSync(__dirname + '/private/emailTemplates/unsubscribe.html'); + + return res.send(template); }); apolloServer.applyMiddleware({ app, path: '/graphql', cors: corsOptions }); -// handle integrations api requests -app.post('/integrations-api', integrationsApiMiddleware); +// verifier web hook +app.post(`/verifier/webhook`, async (req, res) => { + const { emails, phones, email, phone } = req.body; + + if (email) { + await updateContactValidationStatus(email); + } else if (emails) { + await updateContactsValidationStatus('email', emails); + } else if (phone) { + await updateContactValidationStatus(phone); + } else if (phones) { + await updateContactsValidationStatus('phone', phones); + } + + return res.send('success'); +}); // Error handling middleware app.use((error, _req, res, _next) => { @@ -203,14 +353,64 @@ app.use((error, _req, res, _next) => { // Wrap the Express server const httpServer = createServer(app); -// subscriptions server const PORT = getEnv({ name: 'PORT' }); +const MONGO_URL = getEnv({ name: 'MONGO_URL' }); +const TEST_MONGO_URL = getEnv({ name: 'TEST_MONGO_URL' }); +// subscriptions server apolloServer.installSubscriptionHandlers(httpServer); httpServer.listen(PORT, () => { - debugInit(`GraphQL Server is now running on ${PORT}`); + let mongoUrl = MONGO_URL; + + if (NODE_ENV === 'test') { + mongoUrl = TEST_MONGO_URL; + } + + // connect to mongo database + connect(mongoUrl).then(async () => { + initBroker(app).catch(e => { + debugBase(`Error ocurred during message broker init ${e.message}`); + }); - // execute startup actions - init(app); + initMemoryStorage(); + + initWatchers(); + + init() + .then(() => { + telemetry.trackCli('server_started'); + telemetry.startBackgroundUpdate(); + + debugBase('Startup successfully started'); + }) + .catch(e => { + debugBase(`Error occured while starting init: ${e.message}`); + }); + }); + + debugInit(`GraphQL Server is now running on ${PORT}`); }); + +// GRACEFULL SHUTDOWN +process.stdin.resume(); // so the program will not close instantly + +// If the Node process ends, close the Mongoose connection +if (NODE_ENV === 'production') { + (['SIGINT', 'SIGTERM'] as NodeJS.Signals[]).forEach(sig => { + process.on(sig, () => { + // Stops the server from accepting new connections and finishes existing connections. + httpServer.close((error: Error | undefined) => { + if (error) { + console.error(error.message); + process.exit(1); + } + + mongoose.connection.close(() => { + console.log('Mongoose connection disconnected'); + process.exit(0); + }); + }); + }); + }); +} diff --git a/src/initialData/common/activity_logs.bson b/src/initialData/common/activity_logs.bson new file mode 100644 index 000000000..a967e89ef Binary files /dev/null and b/src/initialData/common/activity_logs.bson differ diff --git a/src/initialData/common/activity_logs.metadata.json b/src/initialData/common/activity_logs.metadata.json new file mode 100644 index 000000000..8160c4324 --- /dev/null +++ b/src/initialData/common/activity_logs.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.activity_logs"},{"v":2,"key":{"contentId":1},"name":"contentId_1","ns":"erxes_aaa.activity_logs","background":true},{"v":2,"key":{"contentType":1},"name":"contentType_1","ns":"erxes_aaa.activity_logs","background":true},{"v":2,"key":{"action":1},"name":"action_1","ns":"erxes_aaa.activity_logs","background":true}],"uuid":"8f25ca5c706d4789931ebbc8d9793c6d"} \ No newline at end of file diff --git a/src/initialData/common/boards.bson b/src/initialData/common/boards.bson new file mode 100644 index 000000000..b05e06dd7 Binary files /dev/null and b/src/initialData/common/boards.bson differ diff --git a/src/initialData/common/boards.metadata.json b/src/initialData/common/boards.metadata.json new file mode 100644 index 000000000..4911189ec --- /dev/null +++ b/src/initialData/common/boards.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.boards"}],"uuid":"b3de251ff6cf41dfbe89b85b760b95a3"} \ No newline at end of file diff --git a/src/initialData/common/brands.bson b/src/initialData/common/brands.bson new file mode 100644 index 000000000..6d1fc82f7 Binary files /dev/null and b/src/initialData/common/brands.bson differ diff --git a/src/initialData/common/brands.metadata.json b/src/initialData/common/brands.metadata.json new file mode 100644 index 000000000..8d1750bb0 --- /dev/null +++ b/src/initialData/common/brands.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.brands"}],"uuid":"90250f5fd87149368a485f2ccba8305b"} \ No newline at end of file diff --git a/initialData/channels.bson b/src/initialData/common/channels.bson similarity index 100% rename from initialData/channels.bson rename to src/initialData/common/channels.bson diff --git a/src/initialData/common/channels.metadata.json b/src/initialData/common/channels.metadata.json new file mode 100644 index 000000000..1bbeb7fa8 --- /dev/null +++ b/src/initialData/common/channels.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.channels"}],"uuid":"e4e39ee31d684d74895aafd4e290c2ed"} \ No newline at end of file diff --git a/src/initialData/common/checklist_items.bson b/src/initialData/common/checklist_items.bson new file mode 100644 index 000000000..e6ecc3faf Binary files /dev/null and b/src/initialData/common/checklist_items.bson differ diff --git a/src/initialData/common/checklist_items.metadata.json b/src/initialData/common/checklist_items.metadata.json new file mode 100644 index 000000000..385d25659 --- /dev/null +++ b/src/initialData/common/checklist_items.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.checklist_items"}],"uuid":"dfdfeac679e7459eb21127cb8799b13a"} \ No newline at end of file diff --git a/src/initialData/common/checklists.bson b/src/initialData/common/checklists.bson new file mode 100644 index 000000000..31c8477cd Binary files /dev/null and b/src/initialData/common/checklists.bson differ diff --git a/src/initialData/common/checklists.metadata.json b/src/initialData/common/checklists.metadata.json new file mode 100644 index 000000000..7a25d6a04 --- /dev/null +++ b/src/initialData/common/checklists.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.checklists"}],"uuid":"c813f4b6c67a418c8a0c02d5f471d5a8"} \ No newline at end of file diff --git a/src/initialData/common/companies.bson b/src/initialData/common/companies.bson new file mode 100644 index 000000000..85a64b4ce Binary files /dev/null and b/src/initialData/common/companies.bson differ diff --git a/src/initialData/common/companies.metadata.json b/src/initialData/common/companies.metadata.json new file mode 100644 index 000000000..b25bc12d1 --- /dev/null +++ b/src/initialData/common/companies.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.companies"},{"v":2,"key":{"searchText":1},"name":"searchText_1","background":true,"ns":"erxes_aaa.companies"}],"uuid":"454f0149a4d44fe8ac429e0847ab4325"} \ No newline at end of file diff --git a/src/initialData/common/configs.bson b/src/initialData/common/configs.bson new file mode 100644 index 000000000..e0d7b7377 Binary files /dev/null and b/src/initialData/common/configs.bson differ diff --git a/src/initialData/common/configs.metadata.json b/src/initialData/common/configs.metadata.json new file mode 100644 index 000000000..424510797 --- /dev/null +++ b/src/initialData/common/configs.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.configs"},{"v":2,"unique":true,"key":{"code":1},"name":"code_1","ns":"erxes_aaa.configs","background":true}],"uuid":"4f8043d840e24843a6d40e9b5f3d4ac5"} \ No newline at end of file diff --git a/src/initialData/common/conformities.bson b/src/initialData/common/conformities.bson new file mode 100644 index 000000000..2fb9d2a63 Binary files /dev/null and b/src/initialData/common/conformities.bson differ diff --git a/src/initialData/common/conformities.metadata.json b/src/initialData/common/conformities.metadata.json new file mode 100644 index 000000000..2100a89f1 --- /dev/null +++ b/src/initialData/common/conformities.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.conformities"},{"v":2,"key":{"mainType":1},"name":"mainType_1","ns":"erxes_aaa.conformities","background":true},{"v":2,"key":{"relType":1},"name":"relType_1","ns":"erxes_aaa.conformities","background":true},{"v":2,"key":{"mainTypeId":1},"name":"mainTypeId_1","ns":"erxes_aaa.conformities","background":true},{"v":2,"key":{"relTypeId":1},"name":"relTypeId_1","ns":"erxes_aaa.conformities","background":true}],"uuid":"3d34b68bdcf44cb7b38ffea03a54b713"} \ No newline at end of file diff --git a/src/initialData/common/conversation_messages.bson b/src/initialData/common/conversation_messages.bson new file mode 100644 index 000000000..6e16ac404 Binary files /dev/null and b/src/initialData/common/conversation_messages.bson differ diff --git a/src/initialData/common/conversation_messages.metadata.json b/src/initialData/common/conversation_messages.metadata.json new file mode 100644 index 000000000..d86f5e0d7 --- /dev/null +++ b/src/initialData/common/conversation_messages.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.conversation_messages"},{"v":2,"key":{"conversationId":1},"name":"conversationId_1","ns":"erxes_aaa.conversation_messages","background":true},{"v":2,"key":{"createdAt":1},"name":"createdAt_1","ns":"erxes_aaa.conversation_messages","background":true},{"v":2,"key":{"internal":1},"name":"internal_1","ns":"erxes_aaa.conversation_messages","background":true},{"v":2,"key":{"customerId":1},"name":"customerId_1","ns":"erxes_aaa.conversation_messages","background":true},{"v":2,"key":{"userId":1},"name":"userId_1","ns":"erxes_aaa.conversation_messages","background":true}],"uuid":"56cf77d45d7941b3b95fb17679cad4dc"} \ No newline at end of file diff --git a/src/initialData/common/conversations.bson b/src/initialData/common/conversations.bson new file mode 100644 index 000000000..355b92efa Binary files /dev/null and b/src/initialData/common/conversations.bson differ diff --git a/src/initialData/common/conversations.metadata.json b/src/initialData/common/conversations.metadata.json new file mode 100644 index 000000000..ff2cc3a16 --- /dev/null +++ b/src/initialData/common/conversations.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.conversations"},{"v":2,"key":{"createdAt":1},"name":"createdAt_1","ns":"erxes_aaa.conversations","background":true},{"v":2,"key":{"integrationId":1},"name":"integrationId_1","ns":"erxes_aaa.conversations","background":true},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.conversations","background":true}],"uuid":"8838a3d4f9bb4135bd8492f8e31e427e"} \ No newline at end of file diff --git a/src/initialData/common/customers.bson b/src/initialData/common/customers.bson new file mode 100644 index 000000000..267d0598a Binary files /dev/null and b/src/initialData/common/customers.bson differ diff --git a/src/initialData/common/customers.metadata.json b/src/initialData/common/customers.metadata.json new file mode 100644 index 000000000..15ce3c927 --- /dev/null +++ b/src/initialData/common/customers.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.customers"},{"v":2,"key":{"profileScore":1},"name":"profileScore_1","background":true,"ns":"erxes_aaa.customers"},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.customers","background":true},{"v":2,"key":{"tagIds":1},"name":"tagIds_1","ns":"erxes_aaa.customers","background":true},{"v":2,"key":{"searchText":1},"name":"searchText_1","ns":"erxes_aaa.customers","background":true}],"uuid":"f0430395a6c74f6aa713cbf2c48a7556"} \ No newline at end of file diff --git a/src/initialData/common/deals.bson b/src/initialData/common/deals.bson new file mode 100644 index 000000000..8cea3354b Binary files /dev/null and b/src/initialData/common/deals.bson differ diff --git a/src/initialData/common/deals.metadata.json b/src/initialData/common/deals.metadata.json new file mode 100644 index 000000000..c0c575322 --- /dev/null +++ b/src/initialData/common/deals.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.deals"},{"v":2,"key":{"searchText":1},"name":"searchText_1","ns":"erxes_aaa.deals","background":true},{"v":2,"key":{"stageId":1},"name":"stageId_1","ns":"erxes_aaa.deals","background":true},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.deals","background":true}],"uuid":"38daa354b3844bae880d87a684210830"} \ No newline at end of file diff --git a/src/initialData/common/email_templates.bson b/src/initialData/common/email_templates.bson new file mode 100644 index 000000000..06f248a5b Binary files /dev/null and b/src/initialData/common/email_templates.bson differ diff --git a/src/initialData/common/email_templates.metadata.json b/src/initialData/common/email_templates.metadata.json new file mode 100644 index 000000000..e3429f57e --- /dev/null +++ b/src/initialData/common/email_templates.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.email_templates"}],"uuid":"eaf5730363b44a248e5ca7d377542896"} \ No newline at end of file diff --git a/initialData/engage_messages.bson b/src/initialData/common/engage_messages.bson similarity index 89% rename from initialData/engage_messages.bson rename to src/initialData/common/engage_messages.bson index 42736629c..84253b328 100644 Binary files a/initialData/engage_messages.bson and b/src/initialData/common/engage_messages.bson differ diff --git a/src/initialData/common/engage_messages.metadata.json b/src/initialData/common/engage_messages.metadata.json new file mode 100644 index 000000000..f8e684118 --- /dev/null +++ b/src/initialData/common/engage_messages.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.engage_messages"}],"uuid":"db6f29210187489bb82d7b1ce92e0fea"} \ No newline at end of file diff --git a/src/initialData/common/fields.bson b/src/initialData/common/fields.bson new file mode 100644 index 000000000..5c08f73fc Binary files /dev/null and b/src/initialData/common/fields.bson differ diff --git a/src/initialData/common/fields.metadata.json b/src/initialData/common/fields.metadata.json new file mode 100644 index 000000000..8505d7556 --- /dev/null +++ b/src/initialData/common/fields.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.fields"}],"uuid":"e9ce94f8392340c7ac565299fe3f7a49"} \ No newline at end of file diff --git a/src/initialData/common/fields_groups.bson b/src/initialData/common/fields_groups.bson new file mode 100644 index 000000000..e69de29bb diff --git a/src/initialData/common/fields_groups.metadata.json b/src/initialData/common/fields_groups.metadata.json new file mode 100644 index 000000000..82f6ae67b --- /dev/null +++ b/src/initialData/common/fields_groups.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.fields_groups"}],"uuid":"1f4c4496255e495499cfc57c22d92983"} \ No newline at end of file diff --git a/src/initialData/common/form_submissions.bson b/src/initialData/common/form_submissions.bson new file mode 100644 index 000000000..2017c5152 Binary files /dev/null and b/src/initialData/common/form_submissions.bson differ diff --git a/src/initialData/common/form_submissions.metadata.json b/src/initialData/common/form_submissions.metadata.json new file mode 100644 index 000000000..9e6a7daab --- /dev/null +++ b/src/initialData/common/form_submissions.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.form_submissions"}],"uuid":"904f0a99a2a540da9a3bfd452080c200"} \ No newline at end of file diff --git a/src/initialData/common/forms.bson b/src/initialData/common/forms.bson new file mode 100644 index 000000000..920c29e66 Binary files /dev/null and b/src/initialData/common/forms.bson differ diff --git a/initialData/customers.metadata.json b/src/initialData/common/forms.metadata.json similarity index 50% rename from initialData/customers.metadata.json rename to src/initialData/common/forms.metadata.json index cbe2e38ea..f6dbc6dd3 100644 --- a/initialData/customers.metadata.json +++ b/src/initialData/common/forms.metadata.json @@ -1 +1 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.customers"}],"uuid":"d0a7475bf3a041919474eab4418ad40f"} \ No newline at end of file +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.forms"}],"uuid":"824129ec7fa84158913d66c41016e80c"} \ No newline at end of file diff --git a/src/initialData/common/growth_hacks.bson b/src/initialData/common/growth_hacks.bson new file mode 100644 index 000000000..113326051 Binary files /dev/null and b/src/initialData/common/growth_hacks.bson differ diff --git a/src/initialData/common/growth_hacks.metadata.json b/src/initialData/common/growth_hacks.metadata.json new file mode 100644 index 000000000..c59754b9f --- /dev/null +++ b/src/initialData/common/growth_hacks.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.growth_hacks"},{"v":2,"key":{"searchText":1},"name":"searchText_1","ns":"erxes_aaa.growth_hacks","background":true},{"v":2,"key":{"stageId":1},"name":"stageId_1","ns":"erxes_aaa.growth_hacks","background":true},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.growth_hacks","background":true}],"uuid":"e8f634c9b49241e2bffdf610a1fa0319"} \ No newline at end of file diff --git a/src/initialData/common/integrations.bson b/src/initialData/common/integrations.bson new file mode 100644 index 000000000..c79b8f7a7 Binary files /dev/null and b/src/initialData/common/integrations.bson differ diff --git a/src/initialData/common/integrations.metadata.json b/src/initialData/common/integrations.metadata.json new file mode 100644 index 000000000..b84b1d3c1 --- /dev/null +++ b/src/initialData/common/integrations.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.integrations"}],"uuid":"f1bb32df25ef477a9565a71acef4cd05"} \ No newline at end of file diff --git a/initialData/internal_notes.bson b/src/initialData/common/internal_notes.bson similarity index 100% rename from initialData/internal_notes.bson rename to src/initialData/common/internal_notes.bson diff --git a/src/initialData/common/internal_notes.metadata.json b/src/initialData/common/internal_notes.metadata.json new file mode 100644 index 000000000..f293681eb --- /dev/null +++ b/src/initialData/common/internal_notes.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.internal_notes"}],"uuid":"23e60b9ef35240a48674e373ee48ce1a"} \ No newline at end of file diff --git a/src/initialData/common/knowledgebase_articles.bson b/src/initialData/common/knowledgebase_articles.bson new file mode 100644 index 000000000..983b01ddb Binary files /dev/null and b/src/initialData/common/knowledgebase_articles.bson differ diff --git a/src/initialData/common/knowledgebase_articles.metadata.json b/src/initialData/common/knowledgebase_articles.metadata.json new file mode 100644 index 000000000..eb4825b96 --- /dev/null +++ b/src/initialData/common/knowledgebase_articles.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.knowledgebase_articles"}],"uuid":"a77f2227a2294f8abbb804928b69cb3a"} \ No newline at end of file diff --git a/src/initialData/common/knowledgebase_categories.bson b/src/initialData/common/knowledgebase_categories.bson new file mode 100644 index 000000000..79c391b3f Binary files /dev/null and b/src/initialData/common/knowledgebase_categories.bson differ diff --git a/src/initialData/common/knowledgebase_categories.metadata.json b/src/initialData/common/knowledgebase_categories.metadata.json new file mode 100644 index 000000000..6b19c3a67 --- /dev/null +++ b/src/initialData/common/knowledgebase_categories.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.knowledgebase_categories"}],"uuid":"ba9c739b69a4413f9eae0a912971dba5"} \ No newline at end of file diff --git a/initialData/knowledgebase_topics.bson b/src/initialData/common/knowledgebase_topics.bson similarity index 100% rename from initialData/knowledgebase_topics.bson rename to src/initialData/common/knowledgebase_topics.bson diff --git a/src/initialData/common/knowledgebase_topics.metadata.json b/src/initialData/common/knowledgebase_topics.metadata.json new file mode 100644 index 000000000..9aa86e1cb --- /dev/null +++ b/src/initialData/common/knowledgebase_topics.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.knowledgebase_topics"}],"uuid":"cb14db202e264d068880bff8d2e7efa7"} \ No newline at end of file diff --git a/src/initialData/common/migrations.bson b/src/initialData/common/migrations.bson new file mode 100644 index 000000000..f0f5e039e Binary files /dev/null and b/src/initialData/common/migrations.bson differ diff --git a/src/initialData/common/migrations.metadata.json b/src/initialData/common/migrations.metadata.json new file mode 100644 index 000000000..72b4b8a08 --- /dev/null +++ b/src/initialData/common/migrations.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.migrations"}],"uuid":"706f48233c2a4593a5b65e7ded48e9e6"} \ No newline at end of file diff --git a/src/initialData/common/notifications.bson b/src/initialData/common/notifications.bson new file mode 100644 index 000000000..da4a67c66 Binary files /dev/null and b/src/initialData/common/notifications.bson differ diff --git a/src/initialData/common/notifications.metadata.json b/src/initialData/common/notifications.metadata.json new file mode 100644 index 000000000..5ce26e0e5 --- /dev/null +++ b/src/initialData/common/notifications.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.notifications"},{"v":2,"key":{"receiver":1},"name":"receiver_1","ns":"erxes_aaa.notifications","background":true},{"v":2,"key":{"contentType":1},"name":"contentType_1","ns":"erxes_aaa.notifications","background":true},{"v":2,"key":{"contentTypeId":1},"name":"contentTypeId_1","ns":"erxes_aaa.notifications","background":true}],"uuid":"580acc42183d46899dd033e497070804"} \ No newline at end of file diff --git a/src/initialData/common/onboarding_histories.bson b/src/initialData/common/onboarding_histories.bson new file mode 100644 index 000000000..ce07e9345 Binary files /dev/null and b/src/initialData/common/onboarding_histories.bson differ diff --git a/src/initialData/common/onboarding_histories.metadata.json b/src/initialData/common/onboarding_histories.metadata.json new file mode 100644 index 000000000..95d2dbc1a --- /dev/null +++ b/src/initialData/common/onboarding_histories.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.onboarding_histories"}],"uuid":"159502a80a814d549158bd5c3d62277c"} \ No newline at end of file diff --git a/src/initialData/common/permissions.bson b/src/initialData/common/permissions.bson new file mode 100644 index 000000000..79b2718d6 Binary files /dev/null and b/src/initialData/common/permissions.bson differ diff --git a/src/initialData/common/permissions.metadata.json b/src/initialData/common/permissions.metadata.json new file mode 100644 index 000000000..ef61a4f98 --- /dev/null +++ b/src/initialData/common/permissions.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.permissions"}],"uuid":"8a111b1c95494c92959af2e6f7dfdec6"} \ No newline at end of file diff --git a/src/initialData/common/pipeline_labels.bson b/src/initialData/common/pipeline_labels.bson new file mode 100644 index 000000000..eee97d0b6 Binary files /dev/null and b/src/initialData/common/pipeline_labels.bson differ diff --git a/src/initialData/common/pipeline_labels.metadata.json b/src/initialData/common/pipeline_labels.metadata.json new file mode 100644 index 000000000..81bd1c7ed --- /dev/null +++ b/src/initialData/common/pipeline_labels.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.pipeline_labels"}],"uuid":"1d97d3f44c5f48648bfa4ad948a1a7f1"} \ No newline at end of file diff --git a/src/initialData/common/pipeline_templates.bson b/src/initialData/common/pipeline_templates.bson new file mode 100644 index 000000000..8bf14de09 Binary files /dev/null and b/src/initialData/common/pipeline_templates.bson differ diff --git a/src/initialData/common/pipeline_templates.metadata.json b/src/initialData/common/pipeline_templates.metadata.json new file mode 100644 index 000000000..6098e883c --- /dev/null +++ b/src/initialData/common/pipeline_templates.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.pipeline_templates"}],"uuid":"d77ac193ac0f427d880f129b5366e824"} \ No newline at end of file diff --git a/src/initialData/common/pipelines.bson b/src/initialData/common/pipelines.bson new file mode 100644 index 000000000..ab0cde693 Binary files /dev/null and b/src/initialData/common/pipelines.bson differ diff --git a/src/initialData/common/pipelines.metadata.json b/src/initialData/common/pipelines.metadata.json new file mode 100644 index 000000000..4038f8e5e --- /dev/null +++ b/src/initialData/common/pipelines.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.pipelines"}],"uuid":"606c6ecbe8f24de5b9e2f6bb240a93fc"} \ No newline at end of file diff --git a/src/initialData/common/product_categories.bson b/src/initialData/common/product_categories.bson new file mode 100644 index 000000000..ce3abfaaa Binary files /dev/null and b/src/initialData/common/product_categories.bson differ diff --git a/src/initialData/common/product_categories.metadata.json b/src/initialData/common/product_categories.metadata.json new file mode 100644 index 000000000..daed8e938 --- /dev/null +++ b/src/initialData/common/product_categories.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.product_categories"},{"v":2,"unique":true,"key":{"code":1},"name":"code_1","ns":"erxes_aaa.product_categories","background":true}],"uuid":"0015d01ac438497096453f9992a615a3"} \ No newline at end of file diff --git a/src/initialData/common/products.bson b/src/initialData/common/products.bson new file mode 100644 index 000000000..4f9510c2b Binary files /dev/null and b/src/initialData/common/products.bson differ diff --git a/src/initialData/common/products.metadata.json b/src/initialData/common/products.metadata.json new file mode 100644 index 000000000..d27536895 --- /dev/null +++ b/src/initialData/common/products.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.products"},{"v":2,"unique":true,"key":{"code":1},"name":"code_1","ns":"erxes_aaa.products","background":true}],"uuid":"e67d3c6e29af45449797c27c91b7edf3"} \ No newline at end of file diff --git a/initialData/response_templates.bson b/src/initialData/common/response_templates.bson similarity index 81% rename from initialData/response_templates.bson rename to src/initialData/common/response_templates.bson index 4f11cbef6..91aa74b08 100644 Binary files a/initialData/response_templates.bson and b/src/initialData/common/response_templates.bson differ diff --git a/src/initialData/common/response_templates.metadata.json b/src/initialData/common/response_templates.metadata.json new file mode 100644 index 000000000..b06a4ff6c --- /dev/null +++ b/src/initialData/common/response_templates.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.response_templates"}],"uuid":"95acf39692e94cce8a7b70238fc9c867"} \ No newline at end of file diff --git a/src/initialData/common/segments.bson b/src/initialData/common/segments.bson new file mode 100644 index 000000000..83eaf1f12 Binary files /dev/null and b/src/initialData/common/segments.bson differ diff --git a/src/initialData/common/segments.metadata.json b/src/initialData/common/segments.metadata.json new file mode 100644 index 000000000..f84c93c63 --- /dev/null +++ b/src/initialData/common/segments.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.segments"}],"uuid":"db8ec3ab72d740a5a5aa126ff1fcaac5"} \ No newline at end of file diff --git a/src/initialData/common/stages.bson b/src/initialData/common/stages.bson new file mode 100644 index 000000000..fb99847d2 Binary files /dev/null and b/src/initialData/common/stages.bson differ diff --git a/src/initialData/common/stages.metadata.json b/src/initialData/common/stages.metadata.json new file mode 100644 index 000000000..c6541cb5c --- /dev/null +++ b/src/initialData/common/stages.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.stages"}],"uuid":"b85d1302971b4e23b6baa5b17ce05216"} \ No newline at end of file diff --git a/src/initialData/common/tags.bson b/src/initialData/common/tags.bson new file mode 100644 index 000000000..6532f8718 Binary files /dev/null and b/src/initialData/common/tags.bson differ diff --git a/initialData/tags.metadata.json b/src/initialData/common/tags.metadata.json similarity index 50% rename from initialData/tags.metadata.json rename to src/initialData/common/tags.metadata.json index fb73c3940..67de0c0e0 100644 --- a/initialData/tags.metadata.json +++ b/src/initialData/common/tags.metadata.json @@ -1 +1 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.tags"}],"uuid":"4dcf8eb86254449db489394e0599011e"} \ No newline at end of file +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.tags"}],"uuid":"fbfc7eff23a846f0804907e95ebd3187"} \ No newline at end of file diff --git a/src/initialData/common/tasks.bson b/src/initialData/common/tasks.bson new file mode 100644 index 000000000..32706853c Binary files /dev/null and b/src/initialData/common/tasks.bson differ diff --git a/src/initialData/common/tasks.metadata.json b/src/initialData/common/tasks.metadata.json new file mode 100644 index 000000000..c3b017519 --- /dev/null +++ b/src/initialData/common/tasks.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.tasks"},{"v":2,"key":{"searchText":1},"name":"searchText_1","ns":"erxes_aaa.tasks","background":true},{"v":2,"key":{"stageId":1},"name":"stageId_1","ns":"erxes_aaa.tasks","background":true},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.tasks","background":true}],"uuid":"539e1c7a631b4820bf1b346fcecca86d"} \ No newline at end of file diff --git a/src/initialData/common/tickets.bson b/src/initialData/common/tickets.bson new file mode 100644 index 000000000..e69de29bb diff --git a/src/initialData/common/tickets.metadata.json b/src/initialData/common/tickets.metadata.json new file mode 100644 index 000000000..c1044a2de --- /dev/null +++ b/src/initialData/common/tickets.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.tickets"},{"v":2,"key":{"searchText":1},"name":"searchText_1","background":true,"ns":"erxes_aaa.tickets"},{"v":2,"key":{"stageId":1},"name":"stageId_1","ns":"erxes_aaa.tickets","background":true},{"v":2,"key":{"status":1},"name":"status_1","ns":"erxes_aaa.tickets","background":true}],"uuid":"a0debc3862f8436c8af2660bfb1283c3"} \ No newline at end of file diff --git a/src/initialData/common/user_groups.bson b/src/initialData/common/user_groups.bson new file mode 100644 index 000000000..b601afdd0 Binary files /dev/null and b/src/initialData/common/user_groups.bson differ diff --git a/src/initialData/common/user_groups.metadata.json b/src/initialData/common/user_groups.metadata.json new file mode 100644 index 000000000..ec46305a1 --- /dev/null +++ b/src/initialData/common/user_groups.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.user_groups"},{"v":2,"unique":true,"key":{"name":1},"name":"name_1","background":true,"ns":"erxes_aaa.user_groups"}],"uuid":"38c1b9c8903543848d901d455ca6a6c5"} \ No newline at end of file diff --git a/src/initialData/common/users.bson b/src/initialData/common/users.bson new file mode 100644 index 000000000..75c1b18d5 Binary files /dev/null and b/src/initialData/common/users.bson differ diff --git a/src/initialData/common/users.metadata.json b/src/initialData/common/users.metadata.json new file mode 100644 index 000000000..83ce48232 --- /dev/null +++ b/src/initialData/common/users.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes_aaa.users"},{"v":2,"unique":true,"key":{"email":1},"name":"email_1","background":true,"ns":"erxes_aaa.users"}],"uuid":"8592bf37bbec4876b93305f22e8a6d16"} \ No newline at end of file diff --git a/src/initialData/growthHack/fields.bson b/src/initialData/growthHack/fields.bson new file mode 100644 index 000000000..5c08f73fc Binary files /dev/null and b/src/initialData/growthHack/fields.bson differ diff --git a/initialData/fields.metadata.json b/src/initialData/growthHack/fields.metadata.json similarity index 50% rename from initialData/fields.metadata.json rename to src/initialData/growthHack/fields.metadata.json index af1cf233d..f424d4c2e 100644 --- a/initialData/fields.metadata.json +++ b/src/initialData/growthHack/fields.metadata.json @@ -1 +1 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.fields"}],"uuid":"ad91f4aa9e8446df97087fb789a2cff4"} \ No newline at end of file +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.fields"}],"uuid":"7347a92ab9ff44a6a61cf47d534c32ff"} \ No newline at end of file diff --git a/src/initialData/growthHack/fields_groups.bson b/src/initialData/growthHack/fields_groups.bson new file mode 100644 index 000000000..e69de29bb diff --git a/src/initialData/growthHack/fields_groups.metadata.json b/src/initialData/growthHack/fields_groups.metadata.json new file mode 100644 index 000000000..35a2fced0 --- /dev/null +++ b/src/initialData/growthHack/fields_groups.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.fields_groups"}],"uuid":"94fef4e73db0404a80a42c2eb73e7bed"} \ No newline at end of file diff --git a/src/initialData/growthHack/form_submissions.bson b/src/initialData/growthHack/form_submissions.bson new file mode 100644 index 000000000..2017c5152 Binary files /dev/null and b/src/initialData/growthHack/form_submissions.bson differ diff --git a/src/initialData/growthHack/form_submissions.metadata.json b/src/initialData/growthHack/form_submissions.metadata.json new file mode 100644 index 000000000..cf4bf3a45 --- /dev/null +++ b/src/initialData/growthHack/form_submissions.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.form_submissions"}],"uuid":"a2434675bb574483acfbbf5752833c63"} \ No newline at end of file diff --git a/src/initialData/growthHack/forms.bson b/src/initialData/growthHack/forms.bson new file mode 100644 index 000000000..920c29e66 Binary files /dev/null and b/src/initialData/growthHack/forms.bson differ diff --git a/initialData/forms.metadata.json b/src/initialData/growthHack/forms.metadata.json similarity index 51% rename from initialData/forms.metadata.json rename to src/initialData/growthHack/forms.metadata.json index 457745515..c805abbf7 100644 --- a/initialData/forms.metadata.json +++ b/src/initialData/growthHack/forms.metadata.json @@ -1 +1 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.forms"}],"uuid":"826e52fceb964791bcce7951896572cb"} \ No newline at end of file +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.forms"}],"uuid":"3d8bbd78cfa14cf28dd748ba66d5ff9f"} \ No newline at end of file diff --git a/src/initialData/growthHack/pipeline_templates.bson b/src/initialData/growthHack/pipeline_templates.bson new file mode 100644 index 000000000..8bf14de09 Binary files /dev/null and b/src/initialData/growthHack/pipeline_templates.bson differ diff --git a/src/initialData/growthHack/pipeline_templates.metadata.json b/src/initialData/growthHack/pipeline_templates.metadata.json new file mode 100644 index 000000000..b4ce58fda --- /dev/null +++ b/src/initialData/growthHack/pipeline_templates.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.pipeline_templates"}],"uuid":"7f217d5e97ea4a0da541299d5007f5de"} \ No newline at end of file diff --git a/src/initialData/permission/permissions.bson b/src/initialData/permission/permissions.bson new file mode 100644 index 000000000..ae0f9aa13 Binary files /dev/null and b/src/initialData/permission/permissions.bson differ diff --git a/src/initialData/permission/permissions.metadata.json b/src/initialData/permission/permissions.metadata.json new file mode 100644 index 000000000..a0c4a293f --- /dev/null +++ b/src/initialData/permission/permissions.metadata.json @@ -0,0 +1 @@ +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.permissions"}],"uuid":"1c1e4de5f6b2495fb0ec4c1916fe65b9"} \ No newline at end of file diff --git a/src/initialData/permission/user_groups.bson b/src/initialData/permission/user_groups.bson new file mode 100644 index 000000000..b601afdd0 Binary files /dev/null and b/src/initialData/permission/user_groups.bson differ diff --git a/initialData/user_groups.metadata.json b/src/initialData/permission/user_groups.metadata.json similarity index 55% rename from initialData/user_groups.metadata.json rename to src/initialData/permission/user_groups.metadata.json index 1594ea523..989fdb2b1 100644 --- a/initialData/user_groups.metadata.json +++ b/src/initialData/permission/user_groups.metadata.json @@ -1 +1 @@ -{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.user_groups"},{"v":2,"unique":true,"key":{"name":1},"name":"name_1","ns":"erxes.user_groups","background":true}],"uuid":"137452c8f8b1458e8403ff286292e2f1"} \ No newline at end of file +{"options":{},"indexes":[{"v":2,"key":{"_id":1},"name":"_id_","ns":"erxes.user_groups"},{"v":2,"unique":true,"key":{"name":1},"name":"name_1","background":true,"ns":"erxes.user_groups"}],"uuid":"e05038708aa041bb81342f8f8ea98f6a"} \ No newline at end of file diff --git a/src/initialData/xls/fakeCompanies.xlsx b/src/initialData/xls/fakeCompanies.xlsx new file mode 100644 index 000000000..efb267054 Binary files /dev/null and b/src/initialData/xls/fakeCompanies.xlsx differ diff --git a/src/initialData/xls/fakeCustomers.xlsx b/src/initialData/xls/fakeCustomers.xlsx new file mode 100644 index 000000000..d499483e6 Binary files /dev/null and b/src/initialData/xls/fakeCustomers.xlsx differ diff --git a/src/inmemoryStorage.ts b/src/inmemoryStorage.ts new file mode 100644 index 000000000..077d03880 --- /dev/null +++ b/src/inmemoryStorage.ts @@ -0,0 +1,21 @@ +import * as dotenv from 'dotenv'; +import memoryStorage from 'erxes-inmemory-storage'; + +// load environment variables +dotenv.config(); + +const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD } = process.env; + +let client; + +export const initMemoryStorage = () => { + client = memoryStorage({ + host: REDIS_HOST, + port: REDIS_PORT, + password: REDIS_PASSWORD, + }); +}; + +export default function() { + return client; +} diff --git a/src/messageBroker.ts b/src/messageBroker.ts new file mode 100644 index 000000000..dcd5cff4a --- /dev/null +++ b/src/messageBroker.ts @@ -0,0 +1,42 @@ +import * as dotenv from 'dotenv'; +import messageBroker from 'erxes-message-broker'; +import { + receiveEngagesNotification, + receiveIntegrationsNotification, + receiveRpcMessage, +} from './data/modules/integrations/receiveMessage'; +import { graphqlPubsub } from './pubsub'; + +dotenv.config(); + +let client; + +export const initBroker = async (server?) => { + client = await messageBroker({ + name: 'api', + server, + envs: process.env, + }); + + const { consumeQueue, consumeRPCQueue } = client; + + // listen for rpc queue ========= + consumeRPCQueue('rpc_queue:integrations_to_api', async data => receiveRpcMessage(data)); + + // graphql subscriptions call ========= + consumeQueue('callPublish', params => { + graphqlPubsub.publish(params.name, params.data); + }); + + consumeQueue('integrationsNotification', async data => { + await receiveIntegrationsNotification(data); + }); + + consumeQueue('engagesNotification', async data => { + await receiveEngagesNotification(data); + }); +}; + +export default function() { + return client; +} diff --git a/src/middlewares/fileMiddleware.ts b/src/middlewares/fileMiddleware.ts new file mode 100644 index 000000000..04387dac0 --- /dev/null +++ b/src/middlewares/fileMiddleware.ts @@ -0,0 +1,118 @@ +import * as formidable from 'formidable'; +import * as request from 'request'; +import * as _ from 'underscore'; +import { filterXSS } from 'xss'; +import { RABBITMQ_QUEUES } from '../data/constants'; +import { can } from '../data/permissions/utils'; +import { + checkFile, + frontendEnv, + getConfig, + getSubServiceDomain, + uploadFile, + uploadFileAWS, + uploadFileLocal, +} from '../data/utils'; +import { debugExternalApi } from '../debuggers'; +import messageBroker from '../messageBroker'; + +export const importer = async (req: any, res, next) => { + if (!(await can('importXlsFile', req.user))) { + return next(new Error('Permission denied!')); + } + + try { + const UPLOAD_SERVICE_TYPE = await getConfig('UPLOAD_SERVICE_TYPE', 'AWS'); + + const scopeBrandIds = JSON.parse(req.cookies.scopeBrandIds || '[]'); + const form = new formidable.IncomingForm(); + + form.parse(req, async (_err, fields: any, response) => { + let status = ''; + let fileType = 'xlsx'; + + try { + status = await checkFile(response.file); + } catch (e) { + return res.json({ status: e.message }); + } + + // if file is not ok then send error + if (status !== 'ok') { + return res.json({ status }); + } + + try { + const fileName = + UPLOAD_SERVICE_TYPE === 'local' + ? await uploadFileLocal(response.file) + : await uploadFileAWS(response.file, true); + + if (fileName.includes('.csv')) { + fileType = 'csv'; + } + + const result = await messageBroker().sendRPCMessage(RABBITMQ_QUEUES.RPC_API_TO_WORKERS, { + action: 'createImport', + type: fields.type, + fileType, + fileName, + uploadType: UPLOAD_SERVICE_TYPE, + scopeBrandIds, + user: req.user, + }); + + return res.json(result); + } catch (e) { + return res.json({ status: 'error', message: e.message }); + } + }); + } catch (e) { + return res.json({ status: 'error', message: e.message }); + } +}; + +export const uploader = async (req: any, res, next) => { + const INTEGRATIONS_API_DOMAIN = getSubServiceDomain({ name: 'INTEGRATIONS_API_DOMAIN' }); + + if (req.query.kind === 'nylas') { + debugExternalApi(`Pipeing request to ${INTEGRATIONS_API_DOMAIN}`); + + return req.pipe( + request + .post(`${INTEGRATIONS_API_DOMAIN}/nylas/upload`) + .on('response', response => { + if (response.statusCode !== 200) { + return next(response.statusMessage); + } + + return response.pipe(res); + }) + .on('error', e => { + debugExternalApi(`Error from pipe ${e.message}`); + next(e); + }), + ); + } + + const form = new formidable.IncomingForm(); + + form.parse(req, async (_error, _fields, response) => { + const file = response.file || response.upload; + + // check file ==== + const status = await checkFile(file, req.headers.source); + + if (status === 'ok') { + try { + const result = await uploadFile(frontendEnv({ name: 'API_URL', req }), file, response.upload ? true : false); + + return res.send(result); + } catch (e) { + return res.status(500).send(filterXSS(e.message)); + } + } + + return res.status(500).send(status); + }); +}; diff --git a/src/middlewares/integrationsApiMiddleware.ts b/src/middlewares/integrationsApiMiddleware.ts deleted file mode 100644 index b0e95dde1..000000000 --- a/src/middlewares/integrationsApiMiddleware.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { ActivityLogs, ConversationMessages, Conversations, Customers } from '../db/models'; -import { CONVERSATION_STATUSES } from '../db/models/definitions/constants'; -import { graphqlPubsub } from '../pubsub'; - -/* - * Handle requests from integrations api - */ -const integrationsApiMiddleware = async (req, res) => { - const { action, payload } = req.body; - const doc = JSON.parse(payload); - - if (action === 'create-customer') { - const customer = await Customers.createCustomer(doc); - - return res.json({ _id: customer._id }); - } - - if (action === 'create-conversation') { - const conversation = await Conversations.createConversation(doc); - - await ActivityLogs.createConversationLog(conversation); - - return res.json({ _id: conversation._id }); - } - - if (action === 'create-conversation-message') { - const message = await ConversationMessages.createMessage(doc); - - await Conversations.updateOne( - { _id: message.conversationId }, - { - $set: { - // Reopen its conversation if it's closed - status: CONVERSATION_STATUSES.OPEN, - - // setting conversation's content to last message - content: message.content, - - attachments: message.attachments, - - // Mark as unread - readUserIds: [], - }, - }, - ); - - graphqlPubsub.publish('conversationClientMessageInserted', { - conversationClientMessageInserted: message, - }); - - graphqlPubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, - }); - - return res.json({ _id: message._id }); - } -}; - -export default integrationsApiMiddleware; diff --git a/src/middlewares/userMiddleware.ts b/src/middlewares/userMiddleware.ts index 3d891a1db..514fbde49 100644 --- a/src/middlewares/userMiddleware.ts +++ b/src/middlewares/userMiddleware.ts @@ -1,6 +1,7 @@ +import * as telemetry from 'erxes-telemetry'; import * as jwt from 'jsonwebtoken'; import { Users } from '../db/models'; - +import memoryStorage from '../inmemoryStorage'; /* * Finds user object by passed tokens * @param {Object} req - Request object @@ -18,6 +19,17 @@ const userMiddleware = async (req, _res, next) => { // save user in request req.user = user; req.user.loginToken = token; + + const currentDate = new Date(); + const machineId = telemetry.getMachineId(); + + const lastLoginDate = new Date(await memoryStorage().get(machineId)); + + if (lastLoginDate.getDay() !== currentDate.getDay()) { + memoryStorage().set(machineId, currentDate); + + telemetry.trackCli('last_login', { updatedAt: currentDate }); + } } catch (e) { return next(); } diff --git a/src/middlewares/webhookMiddleware.ts b/src/middlewares/webhookMiddleware.ts new file mode 100644 index 000000000..cebc8ea2e --- /dev/null +++ b/src/middlewares/webhookMiddleware.ts @@ -0,0 +1,99 @@ +import { NodeVM } from 'vm2'; + +import { ConversationMessages, Conversations, Customers, Integrations } from '../db/models'; +import { graphqlPubsub } from '../pubsub'; + +const findCustomer = async doc => { + let customer; + + if (doc.customerPrimaryEmail) { + customer = await Customers.findOne({ primaryEmail: doc.customerPrimaryEmail }); + } + + if (!customer && doc.customerPrimaryPhone) { + customer = await Customers.findOne({ primaryPhone: doc.customerPrimaryPhone }); + } + + if (!customer && doc.customerPrimaryPhone) { + customer = await Customers.findOne({ code: doc.customerPrimaryPhone }); + } + + return customer; +}; + +const webhookMiddleware = async (req, res, next) => { + try { + const integration = await Integrations.findOne({ _id: req.params.id }); + + if (!integration) { + return next(new Error('Invalid request')); + } + + const webhookData = integration.webhookData; + + if (!webhookData || !Object.values(req.headers).includes(webhookData.token)) { + return next(new Error('Invalid request')); + } + + const params = req.body; + + if (webhookData.script) { + const vm = new NodeVM({ + sandbox: { params }, + }); + + vm.run(webhookData.script); + } + + // get or create customer + let customer = await findCustomer(params); + + if (!customer) { + customer = await Customers.createCustomer({ + primaryEmail: params.customerPrimaryEmail, + primaryPhone: params.customerPrimaryPhone, + code: params.customerCode, + firstName: params.customerFirstName, + lastName: params.customerLastName, + avatar: params.customerAvatar, + }); + } + + // get or create conversation + let conversation = await Conversations.findOne({ customerId: customer._id, integrationId: integration._id }); + + if (!conversation) { + conversation = await Conversations.createConversation({ + customerId: customer._id, + integrationId: integration._id, + content: params.content, + }); + } else { + if (conversation.status === 'closed') { + await Conversations.updateOne({ _id: conversation._id }, { status: 'open' }); + } + } + + // create conversation message + const message = await ConversationMessages.createMessage({ + conversationId: conversation._id, + customerId: customer._id, + content: params.content, + attachments: params.attachments, + }); + + graphqlPubsub.publish('conversationClientMessageInserted', { + conversationClientMessageInserted: message, + }); + + graphqlPubsub.publish('conversationMessageInserted', { + conversationMessageInserted: message, + }); + + return res.send('ok'); + } catch (e) { + return next(e); + } +}; + +export default webhookMiddleware; diff --git a/src/middlewares/widgetsMiddleware.ts b/src/middlewares/widgetsMiddleware.ts new file mode 100644 index 000000000..92bcc0542 --- /dev/null +++ b/src/middlewares/widgetsMiddleware.ts @@ -0,0 +1,55 @@ +import { getSubServiceDomain } from '../data/utils'; +import { Scripts } from '../db/models'; + +const widgetsMiddleware = async (req, res) => { + const domain = getSubServiceDomain({ name: 'WIDGETS_DOMAIN' }); + + const script = await Scripts.findOne({ _id: req.query.id }); + + if (!script) { + return res.end('Not found'); + } + + const generateScript = type => { + return ` + (function() { + var script = document.createElement('script'); + script.src = "${domain}/build/${type}Widget.bundle.js"; + script.async = true; + + var entry = document.getElementsByTagName('script')[0]; + entry.parentNode.insertBefore(script, entry); + })(); + `; + }; + + let erxesSettings = '{'; + let includeScripts = ''; + + if (script.messengerBrandCode) { + erxesSettings += `messenger: { brand_id: "${script.messengerBrandCode}" },`; + includeScripts += generateScript('messenger'); + } + + if (script.kbTopicId) { + erxesSettings += `knowledgeBase: { topic_id: "${script.kbTopicId}" },`; + includeScripts += generateScript('knowledgebase'); + } + + if (script.leadMaps) { + erxesSettings += 'forms: ['; + + script.leadMaps.forEach(map => { + erxesSettings += `{ brand_id: "${map.brandCode}", form_id: "${map.formCode}" },`; + includeScripts += generateScript('form'); + }); + + erxesSettings += '],'; + } + + erxesSettings = `${erxesSettings}}`; + + res.end(`window.erxesSettings=${erxesSettings};${includeScripts}`); +}; + +export default widgetsMiddleware; diff --git a/src/migrations/12312421457128-accounts-integration.ts b/src/migrations/12312421457128-accounts-integration.ts index 570f76010..88d942922 100644 --- a/src/migrations/12312421457128-accounts-integration.ts +++ b/src/migrations/12312421457128-accounts-integration.ts @@ -38,11 +38,15 @@ module.exports.up = async () => { // Switch to erxes-integrations database const integrationMongoClient = apiMongoClient.useDb(INTEGRATIONS_DB_NAME); - if (accounts && accounts.length > 0) { - await integrationMongoClient.db.collection('accounts').insertMany(accounts); - } - - if (integrations && integrations.length > 0) { - await integrationMongoClient.db.collection('integrations').insertMany(integrations); + try { + if (accounts && accounts.length > 0) { + await integrationMongoClient.db.collection('accounts').insertMany(accounts); + } + + if (integrations && integrations.length > 0) { + await integrationMongoClient.db.collection('integrations').insertMany(integrations); + } + } catch (e) { + console.log(e); } }; diff --git a/src/migrations/1559715560692-segment-brand.ts b/src/migrations/1559715560692-segment-brand.ts index 3a564339e..45cbce4e8 100644 --- a/src/migrations/1559715560692-segment-brand.ts +++ b/src/migrations/1559715560692-segment-brand.ts @@ -1,5 +1,4 @@ import { connect } from '../db/connection'; -import { Brands, Customers, Integrations, Segments } from '../db/models'; /** * Updates segment's condition with pairing brandId @@ -8,49 +7,5 @@ import { Brands, Customers, Integrations, Segments } from '../db/models'; module.exports.up = async () => { await connect(); - const segments = await Segments.find({ conditions: { $exists: true } }); - - for (const segment of segments) { - const { conditions = [] } = segment; - - const updatedConditions: any[] = []; - - for (const condition of conditions) { - const updatedCondition: any = condition; - - if (condition.field.startsWith('messengerData.customData')) { - const lastCustomers = await Customers.find({ - $and: [ - { [condition.field]: { $exists: true } }, - { [condition.field]: { $ne: null } }, - { [condition.field]: { $ne: {} } }, - ], - }) - .sort({ createdAt: -1 }) - .limit(1); - - const [lastCustomer] = lastCustomers; - - const integration = await Integrations.findOne({ _id: lastCustomer.integrationId }); - - if (!integration) { - return; - } - - const brand = await Brands.findOne({ _id: integration.brandId }); - - if (!brand) { - return; - } - - updatedCondition.brandId = brand._id; - } - - updatedConditions.push(updatedCondition); - } - - await Segments.updateOne({ _id: segment._id }, { $set: { conditions: updatedConditions } }); - } - return Promise.resolve('done'); }; diff --git a/src/migrations/1561498065457-profile-score.ts b/src/migrations/1561498065457-profile-score.ts index b722f24a2..76df6a462 100644 --- a/src/migrations/1561498065457-profile-score.ts +++ b/src/migrations/1561498065457-profile-score.ts @@ -1,27 +1,7 @@ -import { connect } from '../db/connection'; -import { Customers } from '../db/models'; - /** * Updating profile scores on customers * */ module.exports.up = async () => { - await connect(); - - await Customers.ensureIndexes(); - - let customerCount = await Customers.countDocuments({ profileScore: { $exists: false } }); - - while (customerCount > 0) { - const bulks = []; - - for (const customer of await Customers.find({ profileScore: { $exists: false } }).limit(200)) { - bulks.push(await Customers.updateProfileScore(customer._id, false)); - customerCount -= 1; - } - - await Customers.bulkWrite(bulks); - } - - return Promise.resolve('ok'); + console.log('Profile score migration'); }; diff --git a/src/migrations/1566455185985-convert-to-lead.ts b/src/migrations/1566455185985-convert-to-lead.ts new file mode 100644 index 000000000..5ce8ed8be --- /dev/null +++ b/src/migrations/1566455185985-convert-to-lead.ts @@ -0,0 +1,67 @@ +import { connect } from '../db/connection'; +import { Forms, FormSubmissions, Integrations } from '../db/models'; +import { IFormDocument } from '../db/models/definitions/forms'; + +module.exports.up = async () => { + await connect(); + + const forms: IFormDocument[] = await Forms.find(); + + for (const form of forms) { + const integration = await Integrations.findOne({ formId: form._id }); + + if (integration && integration.formData) { + const leadData = { + loadType: integration.formData.loadType, + successAction: integration.formData.successAction, + fromEmail: integration.formData.fromEmail, + userEmailTitle: integration.formData.userEmailTitle, + userEmailContent: integration.formData.userEmailContent, + adminEmails: integration.formData.adminEmails, + adminEmailTitle: integration.formData.adminEmailTitle, + adminEmailContent: integration.formData.adminEmailContent, + thankContent: integration.formData.thankContent, + redirectUrl: integration.formData.redirectUrl, + themeColor: form.themeColor, + callout: form.callout, + rules: form.rules, + viewCount: form.viewCount, + contactsGathered: form.contactsGathered, + }; + + const submissions = form.submissions || []; + + for (const submission of submissions) { + await FormSubmissions.createFormSubmission({ + formId: form._id, + customerId: submission.customerId, + submittedAt: submission.submittedAt, + }); + } + + await Integrations.updateOne( + { formId: form._id }, + { + $set: { kind: 'lead', leadData }, + $unset: { formData: 1 }, + }, + ); + + await Forms.updateOne( + { _id: form._id }, + { + $unset: { + themeColor: 1, + callout: 1, + rules: 1, + viewCount: 1, + contactsGathered: 1, + submissions: 1, + }, + }, + ); + } + } + + return Promise.resolve('ok'); +}; diff --git a/src/migrations/1568433377396-convert-to-lead-permission.ts b/src/migrations/1568433377396-convert-to-lead-permission.ts new file mode 100644 index 000000000..e7be633bc --- /dev/null +++ b/src/migrations/1568433377396-convert-to-lead-permission.ts @@ -0,0 +1,35 @@ +import { connect } from '../db/connection'; +import { Permissions } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const integrationPermissions = await Permissions.find({ module: 'integrations' }); + + for (const permission of integrationPermissions) { + const requiredActions = permission.requiredActions || []; + const updatedActions: string[] = []; + + for (const action of requiredActions) { + switch (action) { + case 'integrationsCreateFormIntegration': { + updatedActions.push('integrationsCreateLeadIntegration'); + + break; + } + case 'integrationsEditFormIntegration': { + updatedActions.push('integrationsEditLeadIntegration'); + + break; + } + default: { + updatedActions.push(action); + } + } + } + + permission.requiredActions = updatedActions; + + permission.save(); + } +}; diff --git a/src/migrations/1570425302665-fix-growth-hack-template-permission.ts b/src/migrations/1570425302665-fix-growth-hack-template-permission.ts new file mode 100644 index 000000000..cbdc71c36 --- /dev/null +++ b/src/migrations/1570425302665-fix-growth-hack-template-permission.ts @@ -0,0 +1,23 @@ +import { connect } from '../db/connection'; +import { Permissions } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + await Permissions.updateMany( + { action: 'growthHacksAll' }, + { + $push: { + requiredActions: { + $each: [ + 'growthHackTemplatesAdd', + 'growthHackTemplatesEdit', + 'growthHackTemplatesRemove', + 'growthHackTemplatesDuplicate', + 'showGrowthHackTemplates', + ], + }, + }, + }, + ); +}; diff --git a/src/migrations/1578124274205-remove-pipelines-stages-no-board.ts b/src/migrations/1578124274205-remove-pipelines-stages-no-board.ts new file mode 100644 index 000000000..e7493c0fa --- /dev/null +++ b/src/migrations/1578124274205-remove-pipelines-stages-no-board.ts @@ -0,0 +1,45 @@ +import { connect } from '../db/connection'; +import { Boards, Forms, Pipelines, Stages } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const removeStages = async (filter?: any) => { + const stages = await Stages.find(filter); + + for (const stage of stages) { + const pipeline = await Pipelines.findOne({ _id: stage.pipelineId }); + + // no pipeline or specific stages + if (!pipeline || filter) { + await Stages.deleteOne({ _id: stage._id }); + + if (stage.formId) { + await Forms.deleteOne({ _id: stage.formId }); + } + } + } + }; + + const removePipelines = async () => { + const pipelines = await Pipelines.find(); + + // removing pipelines no board + for (const pipeline of pipelines) { + const board = await Boards.findOne({ _id: pipeline.boardId }); + + // no board + if (!board) { + await removeStages({ pipelineId: pipeline._id }); + + await Pipelines.deleteOne({ _id: pipeline._id }); + } + } + }; + + console.log('start migration on remove stages no pipeline'); + await removeStages(); + + console.log('start migration on remove pipelines no board'); + await removePipelines(); +}; diff --git a/src/migrations/1584585476163-add-customerId-to-popup-messages.ts b/src/migrations/1584585476163-add-customerId-to-popup-messages.ts new file mode 100644 index 000000000..ea8ae29ca --- /dev/null +++ b/src/migrations/1584585476163-add-customerId-to-popup-messages.ts @@ -0,0 +1,23 @@ +import { connect, disconnect } from '../db/connection'; +import { ConversationMessages, Conversations } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + console.log('start migration to add customerId to popup messages'); + + const popupMessages = await ConversationMessages.find({ + customerId: { $exists: false }, + formWidgetData: { $exists: true }, + }); + + for (const message of popupMessages) { + const conversation = await Conversations.findOne({ _id: message.conversationId }); + + if (conversation && conversation.customerId) { + await ConversationMessages.updateOne({ _id: message._id }, { $set: { customerId: conversation.customerId } }); + } + } + + await disconnect(); +}; diff --git a/src/migrations/16580782691310-convert-conformity.ts b/src/migrations/16580782691310-convert-conformity.ts new file mode 100644 index 000000000..6898608d0 --- /dev/null +++ b/src/migrations/16580782691310-convert-conformity.ts @@ -0,0 +1,7 @@ +/** + * Rename coc field to contentType + * + */ +module.exports.up = async () => { + console.log('start migration on convert conformity'); +}; diff --git a/src/migrations/16580782691319-convert-conformity_1.ts b/src/migrations/16580782691319-convert-conformity_1.ts new file mode 100644 index 000000000..cb444dcdc --- /dev/null +++ b/src/migrations/16580782691319-convert-conformity_1.ts @@ -0,0 +1,145 @@ +import { connect } from '../db/connection'; +import { Conformities, Customers, Deals, Tasks, Tickets } from '../db/models'; + +/** + * Rename coc field to contentType + * + */ +module.exports.up = async () => { + await connect(); + + console.log('start migration on convert conformity'); + try { + const executer = async (mainType, relType, fieldName, entries) => { + console.log('start migration', mainType, '-', relType, ' on conformity'); + + const modifier: any[] = []; + + for (const entry of entries) { + for (const subEntryId of entry[fieldName]) { + modifier.push({ + mainType, + mainTypeId: entry._id, + relType, + relTypeId: subEntryId, + }); + } + } + + return Conformities.insertMany(modifier); + }; + + await executer( + 'customer', + 'company', + 'companyIds', + await Customers.aggregate([ + { + $project: { + _id: 1, + companyIds: 1, + idsLength: { $cond: { if: { $isArray: '$companyIds' }, then: { $size: '$companyIds' }, else: 0 } }, + }, + }, + { $match: { companyIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + + await executer( + 'deal', + 'customer', + 'customerIds', + await Deals.aggregate([ + { + $project: { + _id: 1, + customerIds: 1, + idsLength: { $cond: { if: { $isArray: '$customerIds' }, then: { $size: '$customerIds' }, else: 0 } }, + }, + }, + { $match: { customerIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + await executer( + 'deal', + 'company', + 'companyIds', + await Deals.aggregate([ + { + $project: { + _id: 1, + companyIds: 1, + idsLength: { $cond: { if: { $isArray: '$companyIds' }, then: { $size: '$companyIds' }, else: 0 } }, + }, + }, + { $match: { companyIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + + await executer( + 'ticket', + 'customer', + 'customerIds', + await Tickets.aggregate([ + { + $project: { + _id: 1, + customerIds: 1, + idsLength: { $cond: { if: { $isArray: '$customerIds' }, then: { $size: '$customerIds' }, else: 0 } }, + }, + }, + { $match: { customerIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + await executer( + 'ticket', + 'company', + 'companyIds', + await Tickets.aggregate([ + { + $project: { + _id: 1, + companyIds: 1, + idsLength: { $cond: { if: { $isArray: '$companyIds' }, then: { $size: '$companyIds' }, else: 0 } }, + }, + }, + { $match: { companyIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + + await executer( + 'task', + 'customer', + 'customerIds', + await Tasks.aggregate([ + { + $project: { + _id: 1, + customerIds: 1, + idsLength: { $cond: { if: { $isArray: '$customerIds' }, then: { $size: '$customerIds' }, else: 0 } }, + }, + }, + { $match: { customerIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + await executer( + 'task', + 'company', + 'companyIds', + await Tasks.aggregate([ + { + $project: { + _id: 1, + companyIds: 1, + idsLength: { $cond: { if: { $isArray: '$companyIds' }, then: { $size: '$companyIds' }, else: 0 } }, + }, + }, + { $match: { companyIds: { $exists: true }, idsLength: { $gt: 0 } } }, + ]), + ); + } catch (e) { + console.log('conformity migration ', e.message); + } + + return Promise.resolve('ok'); +}; diff --git a/src/migrations/16783145459825-facebook-integration-kind.ts b/src/migrations/16783145459825-facebook-integration-kind.ts new file mode 100644 index 000000000..1f0605786 --- /dev/null +++ b/src/migrations/16783145459825-facebook-integration-kind.ts @@ -0,0 +1,10 @@ +import { connect } from '../db/connection'; +import { Integrations } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + await Integrations.updateMany({ kind: 'facebook' }, { $set: { kind: 'facebook-messenger' } }); + + return Promise.resolve('ok'); +}; diff --git a/src/migrations/16951472568745-product-category.ts b/src/migrations/16951472568745-product-category.ts new file mode 100644 index 000000000..071948343 --- /dev/null +++ b/src/migrations/16951472568745-product-category.ts @@ -0,0 +1,18 @@ +import { connect } from '../db/connection'; +import { ProductCategories, Products } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const count = await ProductCategories.find({}).countDocuments(); + + if (count > 0) { + return; + } + + const category = await ProductCategories.createProductCategory({ name: 'General', code: '0', order: 'General0' }); + + await Products.updateMany({}, { $set: { categoryId: category._id } }); + + return Promise.resolve('ok'); +}; diff --git a/src/migrations/17041472568745-search-text-coc-item.ts b/src/migrations/17041472568745-search-text-coc-item.ts new file mode 100644 index 000000000..a7954d81e --- /dev/null +++ b/src/migrations/17041472568745-search-text-coc-item.ts @@ -0,0 +1,65 @@ +import { validSearchText } from '../data/utils'; +import { connect } from '../db/connection'; +import { Companies, Customers, Deals, GrowthHacks, Tasks, Tickets } from '../db/models'; +import { fillSearchTextItem } from '../db/models/boardUtils'; +import { ICustomer } from '../db/models/definitions/customers'; + +module.exports.up = async () => { + await connect(); + + const executer = async (objectType, converter, project) => { + const entries = await objectType.aggregate([{ $project: project }]); + console.log(objectType.modelName, entries.length); + + for (const entry of entries) { + const searchText = converter(entry); + await objectType.updateOne({ _id: entry._id }, { $set: { searchText } }); + } + }; + + const fillSearchTextCustomer = (doc: ICustomer) => { + return validSearchText([ + doc.firstName || '', + doc.lastName || '', + (doc.emails || []).join(' '), + (doc.phones || []).join(' '), + doc.visitorContactInfo + ? (doc.visitorContactInfo.email || '').concat(' ', doc.visitorContactInfo.phone || '') + : '', + ]); + }; + + const itemsFillSearchText = (item: any) => { + return fillSearchTextItem({ stageId: '' }, item); + }; + + await executer(Customers, fillSearchTextCustomer, { + _id: 1, + firstName: 1, + lastName: 1, + emails: 1, + phones: 1, + visitorContactInfo: 1, + }); + + await executer(Companies, Companies.fillSearchText, { + _id: 1, + names: 1, + emails: 1, + phones: 1, + website: 1, + industry: 1, + plan: 1, + description: 1, + }); + + await executer(Deals, itemsFillSearchText, { _id: 1, name: 1, description: 1 }); + + await executer(Tasks, itemsFillSearchText, { _id: 1, name: 1, description: 1 }); + + await executer(Tickets, itemsFillSearchText, { _id: 1, name: 1, description: 1 }); + + await executer(GrowthHacks, itemsFillSearchText, { _id: 1, name: 1, description: 1 }); + + return Promise.resolve('ok'); +}; diff --git a/src/migrations/17736260166250-set-integration-status.ts b/src/migrations/17736260166250-set-integration-status.ts new file mode 100644 index 000000000..a5e62e87a --- /dev/null +++ b/src/migrations/17736260166250-set-integration-status.ts @@ -0,0 +1,18 @@ +import { connect } from '../db/connection'; +import { Integrations, Permissions } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + await Integrations.updateMany({}, { $set: { isActive: true } }); + + const permissions = await Permissions.find({ action: 'integrationsAll', module: 'integrations' }); + + for (const perm of permissions) { + const requiredActions = perm.requiredActions; + + requiredActions.push('integrationsArchive'); + + await Permissions.updateOne({ _id: perm._id }, { $set: { requiredActions } }); + } +}; diff --git a/src/migrations/17737260261250-clean-conversation-content.ts b/src/migrations/17737260261250-clean-conversation-content.ts new file mode 100644 index 000000000..ad12ef6ec --- /dev/null +++ b/src/migrations/17737260261250-clean-conversation-content.ts @@ -0,0 +1,13 @@ +import { cleanHtml } from '../data/utils'; +import { connect } from '../db/connection'; +import { Conversations } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const conversations = await Conversations.find({}, { _id: 1, content: 1 }); + + for (const conversation of conversations) { + await Conversations.updateOne({ _id: conversation._id }, { $set: { content: cleanHtml(conversation.content) } }); + } +}; diff --git a/src/migrations/17737260261250-edit-integration-permission.ts b/src/migrations/17737260261250-edit-integration-permission.ts new file mode 100644 index 000000000..71988b8fd --- /dev/null +++ b/src/migrations/17737260261250-edit-integration-permission.ts @@ -0,0 +1,16 @@ +import { connect } from '../db/connection'; +import { Permissions } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const permissions = await Permissions.find({ action: 'integrationsAll', module: 'integrations' }); + + for (const perm of permissions) { + const requiredActions = perm.requiredActions; + + requiredActions.push('integrationsEdit'); + + await Permissions.updateOne({ _id: perm._id }, { $set: { requiredActions } }); + } +}; diff --git a/src/migrations/17737260261350-add-file-export-permissions.ts b/src/migrations/17737260261350-add-file-export-permissions.ts new file mode 100644 index 000000000..37463095f --- /dev/null +++ b/src/migrations/17737260261350-add-file-export-permissions.ts @@ -0,0 +1,32 @@ +import { connect } from '../db/connection'; +import { Permissions } from '../db/models'; + +const updatePermissions = async (action: string, moduleName: string, permName: string) => { + const permissions = await Permissions.find({ action, module: moduleName }); + + for (const perm of permissions) { + const requiredActions = perm.requiredActions; + + requiredActions.push(permName); + + await Permissions.updateOne({ _id: perm._id }, { $set: { requiredActions } }); + } +}; + +module.exports.up = async () => { + await connect(); + + await updatePermissions('dealsAll', 'deals', 'exportDeals'); + + await updatePermissions('tasksAll', 'tasks', 'exportTasks'); + + await updatePermissions('ticketsAll', 'tickets', 'exportTickets'); + + await updatePermissions('brandsAll', 'brands', 'exportBrands'); + + await updatePermissions('channelsAll', 'channels', 'exportChannels'); + + await updatePermissions('permissionsAll', 'permissions', 'exportPermissions'); + + await updatePermissions('usersAll', 'users', 'exportUsers'); +}; diff --git a/src/migrations/18052642328142-activity-log.ts b/src/migrations/18052642328142-activity-log.ts new file mode 100644 index 000000000..ede4994db --- /dev/null +++ b/src/migrations/18052642328142-activity-log.ts @@ -0,0 +1,83 @@ +import { createConnection } from 'mongoose'; +import { connect } from '../db/connection'; +import { ActivityLogs, Conversations, Integrations } from '../db/models'; + +/** + * Rename createdDate field to createdAt + * + */ + +const options = { + useNewUrlParser: true, + useCreateIndex: true, +}; + +module.exports.up = async () => { + await connect(); + + const mongoClient = await createConnection(process.env.MONGO_URL || '', options); + + const internalNotes = mongoClient.db.collection('internal_notes'); + const engageMessages = mongoClient.db.collection('engage_messages'); + + await internalNotes.updateMany({}, { $rename: { createdDate: 'createdAt' } }); + await engageMessages.updateMany({}, { $rename: { createdDate: 'createdAt' } }); + + const activityLogs = mongoClient.db.collection('activity_logs'); + + const activities: any = await activityLogs.find({ 'activity.action': { $in: ['merge', 'create'] } }).toArray(); + + for (const activity of activities) { + if (activity.activity) { + const { action, content, id, type } = activity.activity; + const contentType = activity.contentType; + + const { performedBy } = activity; + + if (action === 'merge') { + await ActivityLogs.create({ + contentId: id, + contentType: type, + content: content.split(','), + action, + createdBy: performedBy.id, + createdAt: activity.createdAt, + }); + + await ActivityLogs.deleteOne({ _id: activity._id }); + } + + if (type === 'segment') { + await ActivityLogs.create({ + contentId: contentType.id, + contentType: contentType.type, + content: { + id, + content, + }, + action: 'segment', + createdBy: performedBy.id, + createdAt: activity.createdAt, + }); + + await ActivityLogs.deleteOne({ _id: activity._id }); + } + + if (action === 'create' && type !== 'segment' && type !== 'conversation' && type !== 'internal_note') { + await ActivityLogs.create({ + contentId: id, + contentType: type, + action, + createdBy: performedBy.id, + createdAt: activity.createdAt, + }); + + await ActivityLogs.deleteOne({ _id: activity._id }); + } + } + } + + const integrationIds = await Integrations.find({}).distinct('_id'); + + return Conversations.remove({ integrationId: { $nin: integrationIds } }); +}; diff --git a/src/migrations/18052642328143-dtth-scope-brand-ids.ts b/src/migrations/18052642328143-dtth-scope-brand-ids.ts new file mode 100644 index 000000000..12fd75257 --- /dev/null +++ b/src/migrations/18052642328143-dtth-scope-brand-ids.ts @@ -0,0 +1,49 @@ +import { connect } from '../db/connection'; +import { Boards, Deals, GrowthHacks, Pipelines, Stages, Tasks, Tickets } from '../db/models'; + +/** + * Add scopeBranIds field on deal, task, ticket, growthHack + */ + +module.exports.up = async () => { + await connect(); + + const { USE_BRAND_RESTRICTIONS } = process.env; + + if (!USE_BRAND_RESTRICTIONS) { + return; + } + + const boards = await Boards.find({}); + + for (const board of boards) { + let collection; + + if (board.type === 'deal') { + collection = Deals; + } + + if (board.type === 'ticket') { + collection = Tickets; + } + + if (board.type === 'task') { + collection = Tasks; + } + + if (board.type === 'growthHack') { + collection = GrowthHacks; + } + + const pipelines = await Pipelines.find({ boardId: board._id }, { _id: 1 }); + const pipelineIds = pipelines.map(p => p._id); + + const stages = await Stages.find({ pipelineId: { $in: pipelineIds } }, { _id: 1 }); + const stageIds = stages.map(s => s._id); + + await collection.updateMany( + { stageId: { $in: stageIds }, scopeBrandIds: null }, + { $set: { scopeBrandIds: (board as any).scopeBrandIds } }, + ); + } +}; diff --git a/src/migrations/18061342528133-delete-callpro-conv.ts b/src/migrations/18061342528133-delete-callpro-conv.ts new file mode 100644 index 000000000..b93c6e076 --- /dev/null +++ b/src/migrations/18061342528133-delete-callpro-conv.ts @@ -0,0 +1,15 @@ +import { connect } from '../db/connection'; +import { ConversationMessages, Conversations, Integrations } from '../db/models'; + +/** + * Add scopeBranIds field on deal, task, ticket, growthHack + */ + +module.exports.up = async () => { + await connect(); + + const integrationIds = await Integrations.find({ kind: 'callpro' }).distinct('_id'); + const converstationIds = await Conversations.find({ integrationId: { $in: integrationIds } }).distinct('_id'); + + await ConversationMessages.deleteMany({ conversationId: { $in: converstationIds } }); +}; diff --git a/src/migrations/18061342528144-productsData-tickUsed.ts b/src/migrations/18061342528144-productsData-tickUsed.ts new file mode 100644 index 000000000..bff8f2baa --- /dev/null +++ b/src/migrations/18061342528144-productsData-tickUsed.ts @@ -0,0 +1,23 @@ +import { connect } from '../db/connection'; +import { Deals } from '../db/models'; + +/** + * Add scopeBranIds field on deal, task, ticket, growthHack + */ + +module.exports.up = async () => { + await connect(); + + console.log('start: migrate to productData.tickUsed = true on deal.productsData....'); + const deals = await Deals.find({ productsData: { $exists: true } }); + + for (const deal of deals) { + const productsData = deal.productsData || []; + + for (const pd of productsData) { + pd.tickUsed = true; + } + + await Deals.updateOne({ _id: deal._id }, { $set: { productsData } }); + } +}; diff --git a/src/migrations/19051342284534-activitylogclear.ts b/src/migrations/19051342284534-activitylogclear.ts new file mode 100644 index 000000000..29f80b45b --- /dev/null +++ b/src/migrations/19051342284534-activitylogclear.ts @@ -0,0 +1,8 @@ +import { connect } from '../db/connection'; +import { ActivityLogs } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + return ActivityLogs.deleteMany({ 'content.content.scopeBrandIds': { $exists: true } }); +}; diff --git a/src/migrations/19051342284544-elasticsearch.ts b/src/migrations/19051342284544-elasticsearch.ts new file mode 100644 index 000000000..714a6b8c2 --- /dev/null +++ b/src/migrations/19051342284544-elasticsearch.ts @@ -0,0 +1,35 @@ +import * as dotenv from 'dotenv'; +import { connect } from '../db/connection'; +import { Segments } from '../db/models'; +import { ICondition } from '../db/models/definitions/segments'; + +dotenv.config(); + +module.exports.up = async () => { + await connect(); + + const segments = await Segments.find(); + + for (const segment of segments) { + const { conditions = [] } = segment; + const newConditions: ICondition[] = []; + + for (const condition of conditions) { + const cond: any = condition; + + newConditions.push({ + ...condition, + type: 'property', + propertyName: cond.field, + propertyOperator: cond.operator, + propertyValue: cond.value, + }); + } + + try { + await Segments.updateOne({ _id: segment._id }, { $set: { conditions: newConditions } }); + } catch (e) { + console.log(e.message); + } + } +}; diff --git a/src/migrations/19051342284548-move-envs.ts b/src/migrations/19051342284548-move-envs.ts new file mode 100644 index 000000000..5a8ae0512 --- /dev/null +++ b/src/migrations/19051342284548-move-envs.ts @@ -0,0 +1,3 @@ +module.exports.up = async () => { + console.log('empty'); +}; diff --git a/src/migrations/19051342284549-move-envs.ts b/src/migrations/19051342284549-move-envs.ts new file mode 100644 index 000000000..a473f98c9 --- /dev/null +++ b/src/migrations/19051342284549-move-envs.ts @@ -0,0 +1,49 @@ +import { connect } from '../db/connection'; +import { Configs } from '../db/models'; + +const ENVS = [ + 'PUBSUB_TYPE', + 'UPLOAD_SERVICE_TYPE', + 'FILE_SYSTEM_PUBLIC', + 'COMPANY_EMAIL_FROM', + 'DEFAULT_EMAIL_SERVICE', + 'MAIL_SERVICE', + 'MAIL_PORT', + 'MAIL_USER', + 'MAIL_PASS', + 'MAIL_HOST', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + 'AWS_BUCKET', + 'AWS_PREFIX', + 'AWS_COMPATIBLE_SERVICE_ENDPOINT', + 'AWS_FORCE_PATH_STYLE', + + 'AWS_SES_ACCESS_KEY_ID', + 'AWS_SES_SECRET_ACCESS_KEY', + 'AWS_REGION', + 'AWS_SES_CONFIG_SET', + + 'GOOGLE_CLIENT_ID', + 'GOOGLE_CLIENT_SECRET', + 'GOOGLE_APPLICATION_CREDENTIALS', + 'GOOGLE_TOPIC', + 'GOOGLE_SUBSCRIPTION_NAME', + 'GOOGLE_PROJECT_ID', + 'GOOGLE_CLOUD_STORAGE_BUCKET', + + 'UPLOAD_FILE_TYPES', + 'WIDGETS_UPLOAD_FILE_TYPES', +]; + +module.exports.up = async () => { + await connect(); + + for (const env of ENVS) { + try { + await Configs.create({ code: env, value: process.env[env] }); + } catch (e) { + console.log(e.message); + } + } +}; diff --git a/src/migrations/19051342284550-related-integration-ids.ts b/src/migrations/19051342284550-related-integration-ids.ts new file mode 100644 index 000000000..43094a994 --- /dev/null +++ b/src/migrations/19051342284550-related-integration-ids.ts @@ -0,0 +1,3 @@ +module.exports.up = async () => { + return Promise.resolve('Ignoring ........'); +}; diff --git a/src/migrations/19051342284560-customer-lead.ts b/src/migrations/19051342284560-customer-lead.ts new file mode 100644 index 000000000..32289a7fd --- /dev/null +++ b/src/migrations/19051342284560-customer-lead.ts @@ -0,0 +1,22 @@ +import { connect } from '../db/connection'; +import { Customers } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const response = await Customers.updateMany( + { + state: { $exists: false }, + $or: [ + { visitorContactInfo: { $exists: true } }, + { primaryEmail: { $exists: true } }, + { primaryPhone: { $exists: true } }, + ], + }, + { + $set: { state: 'lead' }, + }, + ); + + console.log(response); +}; diff --git a/src/migrations/19051342284570-customer-lead2.ts b/src/migrations/19051342284570-customer-lead2.ts new file mode 100644 index 000000000..1f5a48e49 --- /dev/null +++ b/src/migrations/19051342284570-customer-lead2.ts @@ -0,0 +1,18 @@ +import { connect } from '../db/connection'; +import { Customers } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const response = await Customers.updateMany( + { + state: { $nin: ['customer', 'lead'] }, + $or: [{ firstName: { $exists: true, $ne: '' } }, { lastName: { $exists: true, $ne: '' } }], + }, + { + $set: { state: 'lead' }, + }, + ); + + console.log(response); +}; diff --git a/src/migrations/19051342284580-customer-lead3.ts b/src/migrations/19051342284580-customer-lead3.ts new file mode 100644 index 000000000..2052d5b69 --- /dev/null +++ b/src/migrations/19051342284580-customer-lead3.ts @@ -0,0 +1,17 @@ +import { connect } from '../db/connection'; +import { Customers } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const response = await Customers.updateMany( + { + $or: [{ state: { $exists: false } }, { state: '' }], + }, + { + $set: { state: 'visitor' }, + }, + ); + + console.log(response); +}; diff --git a/src/migrations/19051342284590-custom-fields.ts b/src/migrations/19051342284590-custom-fields.ts new file mode 100644 index 000000000..d21c6ea43 --- /dev/null +++ b/src/migrations/19051342284590-custom-fields.ts @@ -0,0 +1,70 @@ +import * as mongoose from 'mongoose'; +import * as validator from 'validator'; +import { isValidDate, ITypedListItem } from '../db/models/Fields'; + +const generateTypedItem = (field: string, value: string): ITypedListItem => { + let stringValue; + let numberValue; + let dateValue; + + if (value) { + stringValue = value.toString(); + + // number + if (validator.isFloat(value.toString())) { + numberValue = value; + stringValue = null; + } + + if (isValidDate(value)) { + dateValue = value; + stringValue = null; + } + } + + return { field, value, stringValue, numberValue, dateValue }; +}; + +const generateTypedListFromMap = (data: { [key: string]: any }): ITypedListItem[] => { + const ids = Object.keys(data || {}); + return ids.map(_id => generateTypedItem(_id, data[_id])); +}; + +module.exports.up = async () => { + const { MONGO_URL = '' } = process.env; + + const mongoClient = await mongoose.createConnection(MONGO_URL, { + useNewUrlParser: true, + useCreateIndex: true, + }); + + const customersCollection = mongoClient.db.collection('customers'); + + await customersCollection + .find({ + customFieldsData: { $exists: true }, + }) + .forEach(async c => { + if (c.leadStatus === 'connected') { + c.leadStatus = 'attemptedToContact'; + } + + c.customFieldsDataBackup = c.customFieldsData; + + c.customFieldsData = generateTypedListFromMap(c.customFieldsData); + + await customersCollection.save(c); + }); + + await customersCollection + .find({ + trackedData: { $exists: true }, + }) + .forEach(async c => { + c.trackedDataBackup = c.trackedData; + + c.trackedData = generateTypedListFromMap(c.trackedData); + + await customersCollection.save(c); + }); +}; diff --git a/src/migrations/19051342284600-remove-duplicate-onboard-histories.ts b/src/migrations/19051342284600-remove-duplicate-onboard-histories.ts new file mode 100644 index 000000000..99704c4d0 --- /dev/null +++ b/src/migrations/19051342284600-remove-duplicate-onboard-histories.ts @@ -0,0 +1,27 @@ +import { connect } from '../db/connection'; +import { OnboardingHistories, Users } from '../db/models'; + +module.exports.up = async () => { + await connect(); + + const users = await Users.find({}); + + for (const user of users) { + const entries = (await OnboardingHistories.find({ userId: user._id })) || []; + const userInfo = user.username || user.email || user._id; + + console.log(`Found "${entries.length}" onboard history entries for user "${userInfo}"`); + + // if multiple entries are found leave only one + if (entries.length > 1) { + const completed = entries.find(item => item.isCompleted); + + if (completed) { + await OnboardingHistories.remove({ _id: { $ne: completed._id } }); + } else { + // leave the first entry + await OnboardingHistories.remove({ _id: { $ne: entries[0]._id } }); + } + } + } // end users loop +}; diff --git a/src/private/emailTemplates/base.html b/src/private/emailTemplates/base.html index a0dedf133..4ff14e395 100644 --- a/src/private/emailTemplates/base.html +++ b/src/private/emailTemplates/base.html @@ -105,59 +105,6 @@ - - - -

- - - - - - -
-
- - - - - - -
-
-

- {{{ signature }}} -

-
-
-
-
-
- + \ No newline at end of file diff --git a/src/private/emailTemplates/conversationCron.html b/src/private/emailTemplates/conversationCron.html index 7af9d406f..fe2a96cf7 100644 --- a/src/private/emailTemplates/conversationCron.html +++ b/src/private/emailTemplates/conversationCron.html @@ -184,7 +184,7 @@
- + @@ -226,7 +226,7 @@ @@ -234,7 +234,7 @@
{{createdAt}} - {{user.details.fullName}} + {{user.fullName}}
- +
@@ -263,10 +263,10 @@ target="_blank" style="font-family: Helvetica Neue; text-decoration: underline; - font-size: 12px; - color: #ccc; - padding: 0px 5px; - border-radius: 10px;" + font-size: 12px; + color: #ccc; + padding: 0px 5px; + border-radius: 10px;" href="https://erxes.io" >www.erxes.io @@ -351,7 +351,7 @@ font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved. diff --git a/src/private/emailTemplates/conversationDetail.html b/src/private/emailTemplates/conversationDetail.html deleted file mode 100644 index 257d88a72..000000000 --- a/src/private/emailTemplates/conversationDetail.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - -
-
-
- - - - - - - - -
- -
-
- -

- {{ conversationDetail.title }}
-

-
-

{{#each conversationDetail.messages}}

-

{{{ content }}}

- {{/each}} -
- -

{{ conversationDetail.date }}

-
-
- - - - - - - - - - - - - -
- -
- -
- Copyright © 2019 - erxes Inc. All rights - reserved. -
diff --git a/src/private/emailTemplates/invitation.html b/src/private/emailTemplates/invitation.html index 16ce9a469..8e66a3d57 100644 --- a/src/private/emailTemplates/invitation.html +++ b/src/private/emailTemplates/invitation.html @@ -143,7 +143,7 @@ font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved. diff --git a/src/private/emailTemplates/notification.html b/src/private/emailTemplates/notification.html index 4d340c919..eb19c2dad 100644 --- a/src/private/emailTemplates/notification.html +++ b/src/private/emailTemplates/notification.html @@ -2,7 +2,7 @@ role="presentation" cellpadding="0" cellspacing="0" - style=" max-width: 560px;font-size:0px;width:100%;background: #fff; padding-bottom: 30px;box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.2);border-radius: 10px;" + style="max-width: 560px;font-size:0px;width:100%;background: #fff; padding-bottom: 30px;box-shadow: 0px 0px 4px 0px rgba(0,0,0,0.2);border-radius: 10px;" align="center" border="0" > @@ -19,10 +19,10 @@ @@ -31,25 +31,30 @@ -

- {{ notification.title }}
+

+ {{ notification.title }}

-

{{{ notification.content }}}

-

{{ notification.date }}

+
+

{{{ userName }}} {{{ action }}}:

+
+ {{{ notification.content }}} +
+

{{ notification.date }}

+
-
+ + +
+ - +
- - - diff --git a/src/private/emailTemplates/resetPassword.html b/src/private/emailTemplates/resetPassword.html index cc39b81c3..a82d76cfa 100644 --- a/src/private/emailTemplates/resetPassword.html +++ b/src/private/emailTemplates/resetPassword.html @@ -170,7 +170,7 @@ font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved. diff --git a/src/private/emailTemplates/unsubscribe.html b/src/private/emailTemplates/unsubscribe.html index 187dcdb9a..4b403fa00 100644 --- a/src/private/emailTemplates/unsubscribe.html +++ b/src/private/emailTemplates/unsubscribe.html @@ -132,7 +132,7 @@

Unsubscribe Successful

font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved. diff --git a/src/private/emailTemplates/userInvitation.html b/src/private/emailTemplates/userInvitation.html index f8d298b26..b277697b3 100644 --- a/src/private/emailTemplates/userInvitation.html +++ b/src/private/emailTemplates/userInvitation.html @@ -253,7 +253,7 @@ font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved. diff --git a/src/private/importTemplates/board_item_template.xlsx b/src/private/importTemplates/board_item_template.xlsx new file mode 100644 index 000000000..04d2a431c Binary files /dev/null and b/src/private/importTemplates/board_item_template.xlsx differ diff --git a/src/private/importTemplates/company_template.xlsx b/src/private/importTemplates/company_template.xlsx new file mode 100644 index 000000000..f8f877e4b Binary files /dev/null and b/src/private/importTemplates/company_template.xlsx differ diff --git a/src/private/importTemplates/customer_template.xlsx b/src/private/importTemplates/customer_template.xlsx new file mode 100644 index 000000000..f4dd32d3f Binary files /dev/null and b/src/private/importTemplates/customer_template.xlsx differ diff --git a/src/private/importTemplates/product_template.xlsx b/src/private/importTemplates/product_template.xlsx new file mode 100644 index 000000000..60d8ecd4b Binary files /dev/null and b/src/private/importTemplates/product_template.xlsx differ diff --git a/src/private/version.json b/src/private/version.json new file mode 100644 index 000000000..35c229057 --- /dev/null +++ b/src/private/version.json @@ -0,0 +1 @@ +{"packageVersion":"0.17.6","sha":"b91386fe3b15d1fbffdc06e265b41875af3c3831","abbreviatedSha":"b91386fe3b","branch":"develop","tag":null,"committer":"Munkh-Orgil ","committerDate":"2020-08-28T07:46:23.000Z","author":"Munkh-Orgil ","authorDate":"2020-08-28T07:46:23.000Z","commitMessage":"erxes/erxes#2253","root":"/home/munkhjin/Documents/workspace/erxes/erxes-api","commonGitDir":"/home/munkhjin/Documents/workspace/erxes/erxes-api/.git","worktreeGitDir":"/home/munkhjin/Documents/workspace/erxes/erxes-api/.git","lastTag":null,"commitsSinceLastTag":null,"parents":["026ff2dd577d52ab010c5044ea88e022a347ec81"]} \ No newline at end of file diff --git a/src/pubsub.ts b/src/pubsub.ts index 9ae6b5ee2..6f5378e2d 100644 --- a/src/pubsub.ts +++ b/src/pubsub.ts @@ -1,87 +1,21 @@ import * as dotenv from 'dotenv'; -import * as fs from 'fs'; +import { redisOptions } from 'erxes-inmemory-storage'; import { RedisPubSub } from 'graphql-redis-subscriptions'; +import { PubSub } from 'graphql-subscriptions'; import * as Redis from 'ioredis'; -import * as path from 'path'; -import { ActivityLogs, Conversations } from './db/models'; -import { get, redisOptions, set } from './redisClient'; // load environment variables dotenv.config(); -interface IPubSub { - asyncIterator: (trigger: string, options?: any) => AsyncIterator; - publish(trigger: string, payload: any, options?: any): any; -} +const { REDIS_HOST, REDIS_PORT, REDIS_PASSWORD } = process.env; -interface IPubsubMessage { - action: string; - data: { - trigger: string; - type: string; - payload: any; - }; -} +const createPubsubInstance = () => { + if (REDIS_HOST) { + redisOptions.host = REDIS_HOST; + redisOptions.port = REDIS_PORT; + redisOptions.password = REDIS_PASSWORD; -interface IGoogleOptions { - projectId: string; - credentials: { - client_email: string; - private_key: string; - }; -} - -const { PUBSUB_TYPE, NODE_ENV, PROCESS_NAME } = process.env; - -// Google pubsub message handler -const commonMessageHandler = payload => { - return convertPubSubBuffer(payload.data); -}; - -const configGooglePubsub = (): IGoogleOptions => { - const checkHasConfigFile = fs.existsSync(path.join(__dirname, '..', '/google_cred.json')); - - if (!checkHasConfigFile) { - throw new Error('Google credentials file not found!'); - } - - const serviceAccount = require('../google_cred.json'); - - return { - projectId: serviceAccount.project_id, - credentials: { - client_email: serviceAccount.client_email, - private_key: serviceAccount.private_key, - }, - }; -}; - -const createPubsubInstance = (): IPubSub => { - let pubsub; - - if (NODE_ENV === 'test' || NODE_ENV === 'command' || PROCESS_NAME === 'crons') { - pubsub = { - asyncIterator: () => null, - publish: () => null, - }; - - return pubsub; - } - - if (PUBSUB_TYPE === 'GOOGLE') { - const googleOptions = configGooglePubsub(); - - const GooglePubSub = require('@axelspringer/graphql-google-pubsub').GooglePubSub; - - const googlePubsub = new GooglePubSub(googleOptions, undefined, commonMessageHandler); - - googlePubsub.subscribe('widgetNotification', message => { - publishMessage(message); - }); - - pubsub = googlePubsub; - } else { - const redisPubSub = new RedisPubSub({ + return new RedisPubSub({ connectionListener: error => { if (error) { console.error(error); @@ -90,65 +24,9 @@ const createPubsubInstance = (): IPubSub => { publisher: new Redis(redisOptions), subscriber: new Redis(redisOptions), }); - - redisPubSub.subscribe('widgetNotification', message => { - return publishMessage(message); - }); - - pubsub = redisPubSub; - } - - return pubsub; -}; - -const publishMessage = async ({ action, data }: IPubsubMessage) => { - if (NODE_ENV === 'test') { - return; - } - - if (action === 'callPublish') { - if (data.trigger === 'conversationMessageInserted') { - const { customerId, conversationId } = data.payload; - const conversation = await Conversations.findOne({ _id: conversationId }, { integrationId: 1 }); - const customerLastStatus = await get(`customer_last_status_${customerId}`); - - // if customer's last status is left then mark as joined when customer ask - if (conversation && customerLastStatus === 'left') { - set(`customer_last_status_${customerId}`, 'joined'); - - // customer has joined + time - const conversationMessages = await Conversations.changeCustomerStatus( - 'joined', - customerId, - conversation.integrationId, - ); - - for (const message of conversationMessages) { - graphqlPubsub.publish('conversationMessageInserted', { - conversationMessageInserted: message, - }); - } - - // notify as connected - graphqlPubsub.publish('customerConnectionChanged', { - customerConnectionChanged: { - _id: customerId, - status: 'connected', - }, - }); - } - } - - graphqlPubsub.publish(data.trigger, { [data.trigger]: data.payload }); - } - - if (action === 'activityLog') { - ActivityLogs.createLogFromWidget(data.type, data.payload); } -}; -const convertPubSubBuffer = (data: Buffer) => { - return JSON.parse(data.toString()); + return new PubSub(); }; export const graphqlPubsub = createPubsubInstance(); diff --git a/src/startup.ts b/src/startup.ts index 17ba85508..198c1b139 100644 --- a/src/startup.ts +++ b/src/startup.ts @@ -1,5 +1,15 @@ -import { trackEngages } from './trackers/engageTracker'; +import * as fs from 'fs'; -export const init = async app => { - trackEngages(app); +export const makeDirs = () => { + const dir = `${__dirname}/private/xlsTemplateOutputs`; + + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir); + } +}; + +const init = async () => { + makeDirs(); }; + +export default init; diff --git a/src/trackers/engageTracker.ts b/src/trackers/engageTracker.ts deleted file mode 100644 index c806b092e..000000000 --- a/src/trackers/engageTracker.ts +++ /dev/null @@ -1,88 +0,0 @@ -import * as AWS from 'aws-sdk'; -import { getEnv } from '../data/utils'; -import { EngageMessages } from '../db/models'; - -export const getApi = (type: string): any => { - const AWS_SES_ACCESS_KEY_ID = getEnv({ name: 'AWS_SES_ACCESS_KEY_ID' }); - const AWS_SES_SECRET_ACCESS_KEY = getEnv({ name: 'AWS_SES_SECRET_ACCESS_KEY' }); - const AWS_REGION = getEnv({ name: 'AWS_REGION' }); - - AWS.config.update({ - accessKeyId: AWS_SES_ACCESS_KEY_ID, - secretAccessKey: AWS_SES_SECRET_ACCESS_KEY, - region: AWS_REGION, - }); - - if (type === 'ses') { - return new AWS.SES(); - } - - return new AWS.SNS(); -}; - -/* - * Receives notification from amazon simple notification service - * And updates engage message status and stats - */ -const handleMessage = async message => { - const obj = JSON.parse(message); - - const { eventType, mail } = obj; - const { headers } = mail; - - const engageMessageId = headers.find(header => header.name === 'Engagemessageid'); - - const mailId = headers.find(header => header.name === 'Mailmessageid'); - - const customerId = headers.find(header => header.name === 'Customerid'); - - const mailHeaders = { - engageMessageId: engageMessageId.value, - mailId: mailId.value, - customerId: customerId.value, - }; - - const type = eventType.toLowerCase(); - - await EngageMessages.updateStats(engageMessageId.value, type); - - await EngageMessages.changeDeliveryReportStatus(mailHeaders, type); -}; - -export const trackEngages = expressApp => { - expressApp.post(`/service/engage/tracker`, (req, res) => { - const chunks: any = []; - - req.setEncoding('utf8'); - - req.on('data', chunk => { - chunks.push(chunk); - }); - - req.on('end', async () => { - const message = JSON.parse(chunks.join('')); - - const { Type = '', Message = {}, Token = '', TopicArn = '' } = message; - - if (Type === 'SubscriptionConfirmation') { - await getApi('sns') - .confirmSubscription({ Token, TopicArn }) - .promise(); - - return res.end('success'); - } - - handleMessage(Message); - }); - - return res.end('success'); - }); -}; - -export const awsRequests = { - getVerifiedEmails() { - return getApi('ses') - .listVerifiedEmailAddresses() - .promise(); - }, -}; diff --git a/src/workers/bulkInsert.ts b/src/workers/bulkInsert.ts deleted file mode 100644 index 57011d82b..000000000 --- a/src/workers/bulkInsert.ts +++ /dev/null @@ -1,113 +0,0 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import * as xlsxPopulate from 'xlsx-populate'; -import { can } from '../data/permissions/utils'; -import { ImportHistory } from '../db/models'; -import { IUserDocument } from '../db/models/definitions/users'; -import { checkFieldNames } from '../db/models/utils'; -import { createWorkers, splitToCore } from './utils'; - -export const intervals: any[] = []; - -/** - * Receives and saves xls file in private/xlsImports folder - * and imports customers to the database - */ -export const importXlsFile = async (file: any, type: string, { user }: { user: IUserDocument }) => { - return new Promise(async (resolve, reject) => { - if (!(await can('importXlsFile', user))) { - return reject(new Error('Permission denied!')); - } - - const versionNumber = process.version - .toString() - .slice(1) - .split('.')[0]; - - if (Number(versionNumber) < 10) { - return reject(new Error('Please upgrade node version above 10.5.0 support worker_threads!')); - } - - const readStream = fs.createReadStream(file.path); - - // Directory to save file - const downloadDir = `${__dirname}/../private/xlsTemplateOutputs/${file.name}`; - - // Converting pipe into promise - const pipe = stream => - new Promise((resolver, rejecter) => { - stream.on('finish', resolver); - stream.on('error', rejecter); - }); - - // Creating streams - const writeStream = fs.createWriteStream(downloadDir); - const streamObj = readStream.pipe(writeStream); - - pipe(streamObj) - .then(async () => { - // After finished saving instantly create and load workbook from xls - const workbook = await xlsxPopulate.fromFileAsync(downloadDir); - - // Deleting file after read - fs.unlink(downloadDir, () => { - return true; - }); - - const usedRange = workbook.sheet(0).usedRange(); - - if (!usedRange) { - return reject(new Error('Invalid file')); - } - - const usedSheets = usedRange.value(); - - // Getting columns - const fieldNames = usedSheets[0]; - - // Removing column - usedSheets.shift(); - - if (usedSheets.length === 0) { - return reject(new Error('Please import more at least one row of data')); - } - - const properties = await checkFieldNames(type, fieldNames); - - const importHistory = await ImportHistory.create({ - contentType: type, - total: usedSheets.length, - userId: user._id, - date: Date.now(), - }); - - const results: string[] = splitToCore(usedSheets); - - const workerFile = - process.env.NODE_ENV === 'production' - ? `./dist/workers/bulkInsert.worker.js` - : './src/workers/bulkInsert.worker.import.js'; - - const workerPath = path.resolve(workerFile); - - const percentagePerData = Number(((1 / usedSheets.length) * 100).toFixed(3)); - - const workerData = { - contentType: type, - user, - properties, - importHistoryId: importHistory._id, - percentagePerData, - }; - - await createWorkers(workerPath, workerData, results).catch(e => { - return reject(e); - }); - - return resolve({ id: importHistory.id }); - }) - .catch(e => { - return reject(e); - }); - }); -}; diff --git a/src/workers/bulkInsert.worker.import.js b/src/workers/bulkInsert.worker.import.js index c8658fc45..218716454 100644 --- a/src/workers/bulkInsert.worker.import.js +++ b/src/workers/bulkInsert.worker.import.js @@ -5,7 +5,6 @@ try { compilerOptions: { experimentalDecorators: false, }, - files: ['./bulkerInsert.worker.ts'], transpileOnly: true, }); } catch (e) { diff --git a/src/workers/bulkInsert.worker.ts b/src/workers/bulkInsert.worker.ts index 42aee11df..e50ab5f94 100644 --- a/src/workers/bulkInsert.worker.ts +++ b/src/workers/bulkInsert.worker.ts @@ -1,7 +1,20 @@ import * as mongoose from 'mongoose'; -import { Companies, Customers, ImportHistory } from '../db/models'; -import { graphqlPubsub } from '../pubsub'; -import { connect } from './utils'; +import { + Boards, + Companies, + Conformities, + Customers, + Deals, + ImportHistory, + Pipelines, + Products, + Stages, + Tags, + Tasks, + Tickets, + Users, +} from '../db/models'; +import { clearEmptyValues, connect, generatePronoun, updateDuplicatedValue } from './utils'; // tslint:disable-next-line const { parentPort, workerData } = require('worker_threads'); @@ -20,13 +33,45 @@ connect().then(async () => { return; } - const { result, contentType, properties, user, importHistoryId, percentagePerData } = workerData; - - let percentage = '0'; - let create: any = Customers.createCustomer; + const { user, scopeBrandIds, result, contentType, properties, importHistoryId, percentagePerData } = workerData; + + let create: any = null; + let model: any = null; + + const isBoardItem = (): boolean => contentType === 'deal' || contentType === 'task' || contentType === 'ticket'; + + switch (contentType) { + case 'company': + create = Companies.createCompany; + model = Companies; + break; + case 'customer': + create = Customers.createCustomer; + model = Customers; + break; + case 'lead': + create = Customers.createCustomer; + model = Customers; + break; + case 'product': + create = Products.createProduct; + model = Products; + break; + case 'deal': + create = Deals.createDeal; + break; + case 'task': + create = Tasks.createTask; + break; + case 'ticket': + create = Tickets.createTicket; + break; + default: + break; + } - if (contentType === 'company') { - create = Companies.createCompany; + if (!create) { + throw new Error(`Unsupported content type "${contentType}"`); } // Iterating field values @@ -45,70 +90,216 @@ connect().then(async () => { // Collecting errors const errorMsgs: string[] = []; - // Customer or company object to import - const coc: any = { - customFieldsData: {}, + const doc: any = { + scopeBrandIds, + customFieldsData: [], }; - let colIndex = 0; + let colIndex: number = 0; + let boardName: string = ''; + let pipelineName: string = ''; + let stageName: string = ''; // Iterating through detailed properties for (const property of properties) { - const value = fieldValue[colIndex] || ''; + const value = (fieldValue[colIndex] || '').toString(); + + if (contentType === 'customer') { + doc.state = 'customer'; + } + if (contentType === 'lead') { + doc.state = 'lead'; + } switch (property.type) { case 'customProperty': { - coc.customFieldsData[property.id] = fieldValue[colIndex]; + doc.customFieldsData.push({ + field: property.id, + value: fieldValue[colIndex], + }); } break; case 'customData': { - coc[property.name] = value.toString(); + doc[property.name] = value; + } + break; + + case 'ownerEmail': + { + const userEmail = value; + + const owner = await Users.findOne({ email: userEmail }).lean(); + + doc[property.name] = owner ? owner._id : ''; + } + break; + + case 'pronoun': + { + doc.sex = generatePronoun(value); + } + break; + + case 'companiesPrimaryNames': + { + doc.companiesPrimaryNames = value.split(','); + } + break; + + case 'customersPrimaryEmails': + doc.customersPrimaryEmails = value.split(','); + break; + + case 'boardName': + boardName = value; + break; + + case 'pipelineName': + pipelineName = value; + break; + + case 'stageName': + stageName = value; + break; + + case 'tag': + { + const tagName = value; + + const tag = await Tags.findOne({ name: new RegExp(`.*${tagName}.*`, 'i') }).lean(); + + doc[property.name] = tag ? [tag._id] : []; } break; case 'basic': { - coc[property.name] = value.toString(); + doc[property.name] = value; + + if (property.name === 'primaryName' && value) { + doc.names = [value]; + } if (property.name === 'primaryEmail' && value) { - coc.emails = [value]; + doc.emails = [value]; } if (property.name === 'primaryPhone' && value) { - coc.phones = [value]; + doc.phones = [value]; + } + + if (property.name === 'phones' && value) { + doc.phones = value.split(','); + } + + if (property.name === 'emails' && value) { + doc.emails = value.split(','); + } + + if (property.name === 'names' && value) { + doc.names = value.split(','); + } + + if (property.name === 'isComplete') { + doc.isComplete = Boolean(value); } } break; - } + } // end property.type switch colIndex++; + } // end properties for loop + + if ((contentType === 'customer' || contentType === 'lead') && !doc.emailValidationStatus) { + doc.emailValidationStatus = 'unknown'; } - // Creating coc - await create(coc, user) + if ((contentType === 'customer' || contentType === 'lead') && !doc.phoneValidationStatus) { + doc.phoneValidationStatus = 'unknown'; + } + + // set board item created user + if (isBoardItem()) { + doc.userId = user._id; + + if (boardName && pipelineName && stageName) { + const board = await Boards.findOne({ name: boardName, type: contentType }); + const pipeline = await Pipelines.findOne({ boardId: board && board._id, name: pipelineName }); + const stage = await Stages.findOne({ pipelineId: pipeline && pipeline._id, name: stageName }); + + doc.stageId = stage && stage._id; + } + } + + await create(doc, user) .then(async cocObj => { + if (doc.companiesPrimaryNames && doc.companiesPrimaryNames.length > 0 && contentType !== 'company') { + const companyIds: string[] = []; + + for (const primaryName of doc.companiesPrimaryNames) { + let company = await Companies.findOne({ primaryName }).lean(); + + if (company) { + companyIds.push(company._id); + } else { + company = await Companies.createCompany({ primaryName }); + + companyIds.push(company._id); + } + } + + for (const _id of companyIds) { + await Conformities.addConformity({ + mainType: contentType === 'lead' ? 'customer' : contentType, + mainTypeId: cocObj._id, + relType: 'company', + relTypeId: _id, + }); + } + } + + if (doc.customersPrimaryEmails && doc.customersPrimaryEmails.length > 0 && contentType !== 'customer') { + const customers = await Customers.find({ primaryEmail: { $in: doc.customersPrimaryEmails } }, { _id: 1 }); + const customerIds = customers.map(customer => customer._id); + + for (const _id of customerIds) { + await Conformities.addConformity({ + mainType: contentType === 'lead' ? 'customer' : contentType, + mainTypeId: cocObj._id, + relType: 'customer', + relTypeId: _id, + }); + } + } + await ImportHistory.updateOne({ _id: importHistoryId }, { $push: { ids: [cocObj._id] } }); + // Increasing success count inc.success++; }) - .catch((e: Error) => { - inc.failed++; + .catch(async (e: Error) => { + const updatedDoc = clearEmptyValues(doc); + // Increasing failed count and pushing into error message switch (e.message) { case 'Duplicated email': - errorMsgs.push(`Duplicated email ${coc.primaryEmail}`); + inc.success++; + await updateDuplicatedValue(model, 'primaryEmail', updatedDoc); break; case 'Duplicated phone': - errorMsgs.push(`Duplicated phone ${coc.primaryPhone}`); + inc.success++; + await updateDuplicatedValue(model, 'primaryPhone', updatedDoc); break; case 'Duplicated name': - errorMsgs.push(`Duplicated name ${coc.primaryName}`); + inc.success++; + await updateDuplicatedValue(model, 'primaryName', updatedDoc); break; default: + inc.failed++; errorMsgs.push(e.message); break; } @@ -131,20 +322,6 @@ connect().then(async () => { if (!importHistory) { throw new Error('Could not find import history'); } - - const fixedPercentage = (importHistory.percentage || 0).toFixed(0); - - if (fixedPercentage !== percentage) { - percentage = fixedPercentage; - - graphqlPubsub.publish('importHistoryChanged', { - importHistoryChanged: { - _id: importHistory._id, - status: importHistory.status, - percentage, - }, - }); - } } mongoose.connection.close(); diff --git a/src/workers/importHistoryRemove.worker.import.js b/src/workers/importHistoryRemove.worker.import.js index 81d5a3075..5f59f994d 100644 --- a/src/workers/importHistoryRemove.worker.import.js +++ b/src/workers/importHistoryRemove.worker.import.js @@ -5,7 +5,6 @@ try { compilerOptions: { experimentalDecorators: false, }, - files: ['./importHistoryRemove.worker.ts'], transpileOnly: true, }); } catch (e) { diff --git a/src/workers/importHistoryRemove.worker.ts b/src/workers/importHistoryRemove.worker.ts index c569d9137..9e37f458d 100644 --- a/src/workers/importHistoryRemove.worker.ts +++ b/src/workers/importHistoryRemove.worker.ts @@ -1,38 +1,54 @@ import * as mongoose from 'mongoose'; -import { Companies, Customers, ImportHistory } from '../db/models'; -import { graphqlPubsub } from '../pubsub'; +import { Companies, Customers, Deals, ImportHistory, Products, Tasks, Tickets } from '../db/models'; import { connect } from './utils'; // tslint:disable-next-line const { parentPort, workerData } = require('worker_threads'); -connect().then(async () => { - const { result, contentType, importHistoryId } = workerData; - - let collection: any = Companies; - - if (contentType === 'customer') { - collection = Customers; - } - - await collection.deleteMany({ _id: { $in: result } }); - await ImportHistory.updateOne({ _id: importHistoryId }, { $pull: { ids: { $in: result } } }); - - const historyObj = await ImportHistory.findOne({ _id: importHistoryId }); - - if (historyObj && (historyObj.ids || []).length === 0) { - graphqlPubsub.publish('importHistoryChanged', { - importHistoryChanged: { - _id: historyObj._id, - status: 'Removed', - percentage: 100, - }, - }); - - await ImportHistory.deleteOne({ _id: importHistoryId }); - } - - mongoose.connection.close(); - - parentPort.postMessage('Successfully finished job'); -}); +connect() + .then(async () => { + const { result, contentType, importHistoryId } = workerData; + + switch (contentType) { + case 'company': + await Companies.removeCompanies(result); + break; + case 'customer': + await Customers.removeCustomers(result); + break; + case 'lead': + await Customers.removeCustomers(result); + break; + case 'product': + await Products.removeProducts(result); + break; + case 'deal': + await Deals.removeDeals(result); + break; + case 'task': + await Tasks.removeTasks(result); + break; + case 'ticket': + await Tickets.removeTickets(result); + break; + default: + break; + } + + await ImportHistory.updateOne({ _id: importHistoryId }, { $pull: { ids: { $in: result } } }); + + const historyObj = await ImportHistory.findOne({ _id: importHistoryId }); + + if (historyObj && (historyObj.ids || []).length === 0) { + await ImportHistory.deleteOne({ _id: importHistoryId }); + } + + mongoose.connection.close(); + + parentPort.postMessage('Successfully finished job'); + }) + .catch(e => { + mongoose.connection.close(); + + parentPort.postMessage(`Finished job with error ${e.message}`); + }); diff --git a/src/workers/index.ts b/src/workers/index.ts index 3775b74a6..16aa68f78 100644 --- a/src/workers/index.ts +++ b/src/workers/index.ts @@ -1,16 +1,12 @@ -import * as bodyParser from 'body-parser'; import * as cookieParser from 'cookie-parser'; import * as dotenv from 'dotenv'; import * as express from 'express'; -import * as formidable from 'formidable'; -import * as path from 'path'; -import { checkFile } from '../data/utils'; +import { filterXSS } from 'xss'; import { connect } from '../db/connection'; -import { debugRequest, debugResponse, debugWorkers } from '../debuggers'; +import { debugWorkers } from '../debuggers'; +import { initMemoryStorage } from '../inmemoryStorage'; import userMiddleware from '../middlewares/userMiddleware'; -import { importXlsFile } from './bulkInsert'; -import { init } from './startup'; -import { clearIntervals, createWorkers, removeWorkers, splitToCore } from './utils'; +import { initBroker } from './messageBroker'; // load environment variables dotenv.config(); @@ -19,95 +15,34 @@ dotenv.config(); connect(); const app = express(); +app.disable('x-powered-by'); // for health check -app.get('/status', async (_req, res) => { +app.get('/health', async (_req, res) => { res.end('ok'); }); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({ extended: true })); +app.use(express.urlencoded()); +app.use(express.json()); app.use(cookieParser()); app.use(userMiddleware); -app.post('/import-remove', async (req: any, res) => { - debugRequest(debugWorkers, req); - - const { targetIds, contentType, importHistoryId } = req.body; - - const results = splitToCore(JSON.parse(targetIds)); - - const workerFile = - process.env.NODE_ENV === 'production' - ? `./dist/workers/importHistoryRemove.worker.js` - : './src/workers/importHistoryRemove.worker.import.js'; - - const workerPath = path.resolve(workerFile); - - const workerData = { - contentType, - importHistoryId, - }; - - await createWorkers(workerPath, workerData, results); - - debugResponse(debugWorkers, req); - - return res.json({ status: 'started removing' }); -}); - -app.post('/import-cancel', async (req: any, res) => { - debugRequest(debugWorkers, req); - - clearIntervals(); - - removeWorkers(); - - return res.json({ status: 'ok' }); -}); - -app.post('/import-file', async (req: any, res) => { - const form = new formidable.IncomingForm(); - - debugRequest(debugWorkers, req); - - form.parse(req, async (_err, fields: any, response) => { - let status = ''; - - try { - status = await checkFile(response.file); - } catch (e) { - return res.json({ status: e.message }); - } - - // if file is not ok then send error - if (status !== 'ok') { - return res.json(status); - } - - importXlsFile(response.file, fields.type, { user: req.user }) - .then(result => { - debugResponse(debugWorkers, req); - return res.json(result); - }) - .catch(e => { - debugWorkers(`Error occured while importing ${e.message}`); - return res.json({ status: 'error', message: e.message }); - }); - }); -}); - // Error handling middleware app.use((error, _req, res, _next) => { console.error(error.stack); - res.status(500).send(error.message); + + res.status(500).send(filterXSS(error.message)); }); -const { PORT_WORKERS } = process.env; +const { PORT_WORKERS = 3700 } = process.env; app.listen(PORT_WORKERS, () => { - init(); + initMemoryStorage(); + + initBroker(app).catch(e => { + debugWorkers(`Error ocurred during message broker init ${e.message}`); + }); debugWorkers(`Workers server is now running on ${PORT_WORKERS}`); }); diff --git a/src/workers/messageBroker.ts b/src/workers/messageBroker.ts new file mode 100644 index 000000000..d765828cc --- /dev/null +++ b/src/workers/messageBroker.ts @@ -0,0 +1,43 @@ +import * as dotenv from 'dotenv'; +import messageBroker from 'erxes-message-broker'; +import { RABBITMQ_QUEUES } from '../data/constants'; +import { receiveImportCancel, receiveImportCreate, receiveImportRemove } from './utils'; + +dotenv.config(); + +let client; + +export const initBroker = async server => { + client = await messageBroker({ + name: 'workers', + server, + envs: process.env, + }); + + const { consumeQueue, consumeRPCQueue } = client; + + // listen for rpc queue ========= + consumeRPCQueue(RABBITMQ_QUEUES.RPC_API_TO_WORKERS, async content => { + const response = { status: 'success', data: {}, errorMessage: '' }; + + try { + response.data = + content.action === 'removeImport' ? await receiveImportRemove(content) : await receiveImportCreate(content); + } catch (e) { + response.status = 'error'; + response.errorMessage = e.message; + } + + return response; + }); + + consumeQueue(RABBITMQ_QUEUES.WORKERS, async content => { + if (content.type === 'cancelImport') { + receiveImportCancel(); + } + }); +}; + +export default function() { + return client; +} diff --git a/src/workers/startup.ts b/src/workers/startup.ts deleted file mode 100644 index 510c9f8d2..000000000 --- a/src/workers/startup.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as fs from 'fs'; - -export const init = async () => { - const makeDirs = () => { - const dir = `${__dirname}/../private/xlsTemplateOutputs`; - - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); - } - }; - - makeDirs(); -}; diff --git a/src/workers/utils.ts b/src/workers/utils.ts index 4230ea28c..cc37ac056 100644 --- a/src/workers/utils.ts +++ b/src/workers/utils.ts @@ -1,7 +1,20 @@ +import * as csv from 'csvtojson'; import * as dotenv from 'dotenv'; +import * as fs from 'fs'; import * as mongoose from 'mongoose'; import * as os from 'os'; -import { debugImport } from '../debuggers'; +import * as path from 'path'; +import * as XlsxStreamReader from 'xlsx-stream-reader'; +import { checkFieldNames } from '../data/modules/fields/utils'; +import { deleteFile, s3Stream, uploadsFolderPath } from '../data/utils'; +import { ImportHistory } from '../db/models'; +import { CUSTOMER_SELECT_OPTIONS } from '../db/models/definitions/constants'; +import ImportHistories from '../db/models/ImportHistory'; +import { debugImport, debugWorkers } from '../debuggers'; + +const { MONGO_URL = '' } = process.env; + +export const connect = () => mongoose.connect(MONGO_URL, { useNewUrlParser: true, useCreateIndex: true }); dotenv.config(); @@ -13,8 +26,8 @@ export const createWorkers = (workerPath: string, workerData: any, results: stri // tslint:disable-next-line const Worker = require('worker_threads').Worker; - if (workers.length > 0) { - return reject(new Error('Workers are busy')); + if (workers && workers.length > 0) { + return reject(new Error('Workers are busy or not working')); } const interval = setImmediate(() => { @@ -95,10 +108,266 @@ export const clearIntervals = () => { intervals = []; }; -const { MONGO_URL = '' } = process.env; +export const clearEmptyValues = (obj: any) => { + Object.keys(obj).forEach(key => { + if (obj[key] === '' || obj[key] === 'unknown') { + delete obj[key]; + } + + if (Array.isArray(obj[key]) && obj[key].length === 0) { + delete obj[key]; + } + }); + + return obj; +}; + +export const updateDuplicatedValue = async (model: any, field: string, doc: any) => { + return model.updateOne({ [field]: doc[field] }, { $set: { ...doc, modifiedAt: new Date() } }); +}; + +// xls file import, cancel, removal +export const receiveImportRemove = async (content: any) => { + const { contentType, importHistoryId } = content; + + const importHistory = await ImportHistories.getImportHistory(importHistoryId); + + const results = splitToCore(importHistory.ids || []); + + const workerFile = + process.env.NODE_ENV === 'production' + ? `./dist/workers/importHistoryRemove.worker.js` + : './src/workers/importHistoryRemove.worker.import.js'; + + const workerPath = path.resolve(workerFile); + + await createWorkers(workerPath, { contentType, importHistoryId }, results); + + return { status: 'ok' }; +}; + +export const receiveImportCancel = () => { + clearIntervals(); + + removeWorkers(); + + return { status: 'ok' }; +}; + +const readXlsFile = async (fileName: string, uploadType: string): Promise<{ fieldNames: string[]; datas: any[] }> => { + return new Promise(async (resolve, reject) => { + let rowCount = 0; + + const usedSheets: any[] = []; + + const xlsxReader = XlsxStreamReader(); + + const errorCallback = error => { + reject(new Error(error.code)); + }; + + try { + const stream = + uploadType === 'local' + ? fs.createReadStream(`${uploadsFolderPath}/${fileName}`) + : await s3Stream(fileName, errorCallback); + + stream.pipe(xlsxReader); + + xlsxReader.on('worksheet', workSheetReader => { + if (workSheetReader > 1) { + return workSheetReader.skip(); + } + + workSheetReader.on('row', row => { + if (rowCount > 100000) { + return reject(new Error('You can only import 100000 rows one at a time')); + } + + if (row.values.length > 0) { + usedSheets.push(row.values); + rowCount++; + } + }); + + workSheetReader.process(); + }); + + xlsxReader.on('end', () => { + const compactedRows: any = []; + + for (const row of usedSheets) { + if (row.length > 0) { + row.shift(); + + compactedRows.push(row); + } + } + + const fieldNames = usedSheets[0]; + + // Removing column + compactedRows.shift(); + + return resolve({ fieldNames, datas: compactedRows }); + }); + + xlsxReader.on('error', error => { + return reject(error); + }); + } catch (e) { + reject(e); + } + }); +}; + +const readCsvFile = async (fileName: string, uploadType: string): Promise<{ fieldNames: string[]; datas: any[] }> => { + return new Promise(async (resolve, reject) => { + const errorCallback = error => { + reject(new Error(error.code)); + }; + + const mainDatas: any[] = []; + + try { + const stream = + uploadType === 'local' + ? fs.createReadStream(`${uploadsFolderPath}/${fileName}`) + : await s3Stream(fileName, errorCallback); + + const results = await csv().fromStream(stream); + + if (!results || results.length === 0) { + return reject(new Error('Please import at least one row of data')); + } + + if (results && results.length > 100000) { + return reject(new Error('You can only import 100000 rows one at a time')); + } + + const fieldNames: string[] = []; + + for (const [key, value] of Object.entries(results[0])) { + if (value && typeof value === 'object') { + const subFields = Object.keys(value || {}); + + for (const subField of subFields) { + fieldNames.push(`${key}.${subField}`); + } + } else { + fieldNames.push(key); + } + } + + for (const result of results) { + let data: any[] = []; + + for (const mainValue of Object.values(result)) { + if (mainValue) { + if (typeof mainValue !== 'object') { + data.push(mainValue || ''); + } else if (typeof mainValue === 'object') { + const subFieldValues = Object.values(mainValue || {}); + subFieldValues.forEach(subFieldValue => { + data.push(subFieldValue || ''); + }); + } + } + } + + if (data.length > 1) { + mainDatas.push(data); + } + + data = []; + } + + return resolve({ fieldNames, datas: mainDatas }); + } catch (e) { + return resolve(); + } + }); +}; + +export const receiveImportCreate = async (content: any) => { + try { + const { fileName, type, scopeBrandIds, user, uploadType, fileType } = content; + let fieldNames: string[] = []; + let datas: string[] = []; + let result: any = {}; + + switch (fileType) { + case 'csv': + result = await readCsvFile(fileName, uploadType); -export const connect = () => - mongoose.connect( - MONGO_URL, - { useNewUrlParser: true, useCreateIndex: true }, + fieldNames = result.fieldNames; + datas = result.datas; + + break; + + case 'xlsx': + result = await readXlsFile(fileName, uploadType); + + fieldNames = result.fieldNames; + datas = result.datas; + + break; + } + + if (datas.length === 0) { + throw new Error('Please import at least one row of data'); + } + + const properties = await checkFieldNames(type, fieldNames); + + const importHistory = await ImportHistory.create({ + contentType: type, + total: datas.length, + userId: user._id, + date: Date.now(), + }); + + const results: string[] = splitToCore(datas); + + const workerFile = + process.env.NODE_ENV === 'production' + ? `./dist/workers/bulkInsert.worker.js` + : './src/workers/bulkInsert.worker.import.js'; + + const workerPath = path.resolve(workerFile); + + const percentagePerData = Number(((1 / datas.length) * 100).toFixed(3)); + + const workerData = { + scopeBrandIds, + user, + contentType: type, + properties, + importHistoryId: importHistory._id, + percentagePerData, + }; + + await createWorkers(workerPath, workerData, results); + + await deleteFile(fileName); + + return { id: importHistory.id }; + } catch (e) { + debugWorkers(e.message); + throw e; + } +}; + +export const generateUid = () => { + return ( + '_' + + Math.random() + .toString(36) + .substr(2, 9) ); +}; +export const generatePronoun = value => { + const pronoun = CUSTOMER_SELECT_OPTIONS.SEX.find(sex => sex.label.toUpperCase() === value.toUpperCase()); + + return pronoun ? pronoun.value : ''; +}; diff --git a/tsconfig.json b/tsconfig.json index 4082b6b15..572ab60fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "outDir": "./dist", "allowJs": true, - "target": "es5", + "target": "es6", "moduleResolution": "node", "module": "commonjs", "lib": ["es2015", "es6", "es7", "esnext.asynciterable"], diff --git a/tsconfig.prod.json b/tsconfig.prod.json index 8caa09e0b..79c2328f8 100644 --- a/tsconfig.prod.json +++ b/tsconfig.prod.json @@ -1,8 +1,10 @@ { + "extends": "./tsconfig.json", "compilerOptions": { "sourceMap": false, + "inlineSourceMap": false, + "inlineSources": false }, - "extends": "./tsconfig.json", "include": ["./src/**/*.ts"], - "exclude": ["node_modules", "dist", "src/__tests__", "src/commands"] + "exclude": ["node_modules", "dist", "src/__tests__"] } diff --git a/wait-for.sh b/wait-for.sh index 071c2bee3..92cbdbb3c 100755 --- a/wait-for.sh +++ b/wait-for.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Use this script to test if a given TCP host/port are available +# Use this script to test if a given TCP host/port are available WAITFORIT_cmdname=${0##*/} @@ -141,16 +141,20 @@ WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} -# check to see if timeout is from busybox? +# Check to see if timeout is from busybox? WAITFORIT_TIMEOUT_PATH=$(type -p timeout) WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then - WAITFORIT_ISBUSY=1 + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then WAITFORIT_BUSYTIMEFLAG="-t" - + fi else - WAITFORIT_ISBUSY=0 - WAITFORIT_BUSYTIMEFLAG="" + WAITFORIT_ISBUSY=0 fi if [[ $WAITFORIT_CHILD -gt 0 ]]; then @@ -175,4 +179,4 @@ if [[ $WAITFORIT_CLI != "" ]]; then exec "${WAITFORIT_CLI[@]}" else exit $WAITFORIT_RESULT -fi +fi \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 0f02de9f1..8ccfaa281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,228 +2,387 @@ # yarn lockfile v1 -"@apollographql/apollo-tools@^0.2.6": - version "0.2.9" - resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.2.9.tgz#1e20999d11728ef47f8f812f2be0426b5dde1a51" +"@apollo/protobufjs@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@apollo/protobufjs/-/protobufjs-1.0.3.tgz#02c655aedd4ba7c7f64cbc3d2b1dd9a000a391ba" + integrity sha512-gqeT810Ect9WIqsrgfUvr+ljSB5m1PyBae9HGdrRyQ3HjHjTcjVvxpsMYXlUk4rUHnrfUqyoGvLSy2yLlRGEOw== dependencies: - apollo-env "0.2.5" + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.0" + "@types/node" "^10.1.0" + long "^4.0.0" -"@apollographql/graphql-playground-html@^1.6.6": - version "1.6.6" - resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.6.tgz#022209e28a2b547dcde15b219f0c50f47aa5beb3" +"@apollographql/apollo-tools@^0.4.3": + version "0.4.3" + resolved "https://registry.yarnpkg.com/@apollographql/apollo-tools/-/apollo-tools-0.4.3.tgz#938a50aea0935973a75155a73417f2f6fc7ac2ef" + integrity sha512-CtC1bmohB1owdGMT2ZZKacI94LcPAZDN2WvCe+4ZXT5d7xO5PHOAb70EP/LcFbvnS8QI+pkYRSCGFQnUcv9efg== + dependencies: + apollo-env "^0.6.1" + +"@apollographql/graphql-playground-html@1.6.24": + version "1.6.24" + resolved "https://registry.yarnpkg.com/@apollographql/graphql-playground-html/-/graphql-playground-html-1.6.24.tgz#3ce939cb127fb8aaa3ffc1e90dff9b8af9f2e3dc" + integrity sha512-8GqG48m1XqyXh4mIZrtB5xOhUwSsh1WsrrsaZQOEYYql3YN9DEu9OOSg0ILzXHZo/h2Q74777YE4YzlArQzQEQ== + +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35", "@babel/code-frame@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" + integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== + dependencies: + "@babel/highlight" "^7.8.3" + +"@babel/code-frame@^7.10.3": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a" + integrity sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg== + dependencies: + "@babel/highlight" "^7.10.4" + +"@babel/core@^7.1.0": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.8.6.tgz#27d7df9258a45c2e686b6f18b6c659e563aa4636" + integrity sha512-Sheg7yEJD51YHAvLEV/7Uvw95AeWqYPL3Vk3zGujJKIhJ+8oLw2ALaf3hbucILhKsgSoADOvtKRJuNVdcJkOrg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.6" + "@babel/helpers" "^7.8.4" + "@babel/parser" "^7.8.6" + "@babel/template" "^7.8.6" + "@babel/traverse" "^7.8.6" + "@babel/types" "^7.8.6" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.1" + json5 "^2.1.0" + lodash "^4.17.13" + resolve "^1.3.2" + semver "^5.4.1" + source-map "^0.5.0" + +"@babel/generator@^7.4.0", "@babel/generator@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.8.6.tgz#57adf96d370c9a63c241cd719f9111468578537a" + integrity sha512-4bpOR5ZBz+wWcMeVtcf7FbjcFzCp+817z2/gHNncIRcM9MmKzUhtWCYAq27RAfUrAFwb+OCG1s9WEaVxfi6cjg== + dependencies: + "@babel/types" "^7.8.6" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + +"@babel/helper-function-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.8.3.tgz#eeeb665a01b1f11068e9fb86ad56a1cb1a824cca" + integrity sha512-BCxgX1BC2hD/oBlIFUgOCQDOPV8nSINxCwM3o93xP4P9Fq6aV5sgv2cOOITDMtCfQ+3PvHp3l689XZvAM9QyOA== + dependencies: + "@babel/helper-get-function-arity" "^7.8.3" + "@babel/template" "^7.8.3" + "@babel/types" "^7.8.3" + +"@babel/helper-get-function-arity@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.8.3.tgz#b894b947bd004381ce63ea1db9f08547e920abd5" + integrity sha512-FVDR+Gd9iLjUMY1fzE2SR0IuaJToR4RkCDARVfsBBPSP53GEqSFjD8gNyxg246VUyc/ALRxFaAK8rVG7UT7xRA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" + integrity sha512-j+fq49Xds2smCUNYmEHF9kGNkhbet6yVIBp4e6oeQpH1RUs/Ir06xUKzDjDkGcaaokPiTNs2JBWHjaE4csUkZQ== + +"@babel/helper-split-export-declaration@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.8.3.tgz#31a9f30070f91368a7182cf05f831781065fc7a9" + integrity sha512-3x3yOeyBhW851hroze7ElzdkeRXQYQbFIb7gLK1WQYsw2GWDay5gAJNw1sWJ0VFP6z5J1whqeXH/WCdCjZv6dA== + dependencies: + "@babel/types" "^7.8.3" + +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + +"@babel/helpers@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.8.4.tgz#754eb3ee727c165e0a240d6c207de7c455f36f73" + integrity sha512-VPbe7wcQ4chu4TDQjimHv/5tj73qz88o12EPkO2ValS2QiQS/1F2SsjyIGNnAD0vF/nZS6Cf9i+vW6HIlnaR8w== + dependencies: + "@babel/template" "^7.8.3" + "@babel/traverse" "^7.8.4" + "@babel/types" "^7.8.3" + +"@babel/highlight@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.10.4.tgz#7d1bdfd65753538fabe6c38596cdb76d9ac60143" + integrity sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + chalk "^2.0.0" + js-tokens "^4.0.0" -"@axelspringer/graphql-google-pubsub@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@axelspringer/graphql-google-pubsub/-/graphql-google-pubsub-1.2.1.tgz#1b8f9570029d8f16e507f61b2aa7e94f97c138f8" - integrity sha512-NxZAFllK2gh2tdu0XAMKtR4CjZlrmIbf/31vIDwMRs/6axuU94T7w4lqLircbEG8V/Je6l930kU14h2dupQrXg== +"@babel/highlight@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.8.3.tgz#28f173d04223eaaa59bc1d439a3836e6d1265797" + integrity sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg== dependencies: - "@google-cloud/pubsub" "^0.28.1" - graphql-subscriptions "^1.1.0" - iterall "^1.2.2" + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" -"@babel/code-frame@^7.0.0-beta.35": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" +"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.8.6.tgz#ba5c9910cddb77685a008e3c587af8d27b67962c" + integrity sha512-trGNYSfwq5s0SgM1BMEB8hX3NDmO7EP2wsDGDexiaKMB92BaRpS+qZfpkMqUBhcsOTBwNy9B/jieo4ad/t/z2g== + +"@babel/plugin-syntax-object-rest-spread@^7.0.0": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/runtime@^7.10.3": + version "7.10.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.5.tgz#303d8bd440ecd5a491eae6117fd3367698674c5c" + integrity sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/runtime@^7.11.2": + version "7.11.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736" + integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw== + dependencies: + regenerator-runtime "^0.13.4" + +"@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" + integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + +"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.4", "@babel/traverse@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.8.6.tgz#acfe0c64e1cd991b3e32eae813a6eb564954b5ff" + integrity sha512-2B8l0db/DPi8iinITKuo7cbPznLCEk0kCxDoB9/N6gGNg/gxOXiR/IcymAFPiBwk5w6TtQ27w4wpElgp9btR9A== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.8.6" + "@babel/helper-function-name" "^7.8.3" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.8.6" + "@babel/types" "^7.8.6" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + +"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.8.3", "@babel/types@^7.8.6": + version "7.8.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.8.6.tgz#629ecc33c2557fcde7126e58053127afdb3e6d01" + integrity sha512-wqz7pgWMIrht3gquyEFPVXeXCti72Rm8ep9b5tQKz9Yg9LzJA3HxosF1SB3Kc81KD1A3XBkkVYtJvCKS2Z/QrA== dependencies: - "@babel/highlight" "^7.0.0" + esutils "^2.0.2" + lodash "^4.17.13" + to-fast-properties "^2.0.0" -"@babel/highlight@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" +"@cnakazawa/watch@^1.0.3": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" + integrity sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ== dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" + exec-sh "^0.3.2" + minimist "^1.2.0" -"@firebase/app-types@0.3.7": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.3.7.tgz#9b00609ca7a992de77a62254296111ae05ee0128" +"@dashersw/axon@2.0.5": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@dashersw/axon/-/axon-2.0.5.tgz#708b8cd21a5c803de8dd517a9252828b007d77bb" + integrity sha512-e7az6UOh/1JqLvzg2GPhP3n47QMQal3Qg2a2497JwY7dlbSKUg4dQmnRyKWNjFz0FHjranUjKvX6J6NAV3Sm/Q== + dependencies: + amp "~0.3.1" + amp-message "~0.1.1" + configurable "0.0.1" + debug "*" + escape-regexp "0.0.1" -"@firebase/app@^0.3.4": - version "0.3.14" - resolved "https://registry.yarnpkg.com/@firebase/app/-/app-0.3.14.tgz#26537d25d87647af530d72967d159544e4093ffa" +"@dashersw/node-discover@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@dashersw/node-discover/-/node-discover-1.0.4.tgz#3fd2aad22228e0ecf72bb069e9f0e06ef4bd5b82" + integrity sha512-OblARM345ECaTSSFQcuWUl+7/uhOjhKBIA0G0CbOPbUzwF3cqBbl2R0E9tulnsLk3XB6Zpmja0TZIU5ClKF6LA== dependencies: - "@firebase/app-types" "0.3.7" - "@firebase/util" "0.2.11" - dom-storage "2.1.0" - tslib "1.9.3" - xmlhttprequest "1.8.0" + redis "^2.7.1" + uuid "^3.3.2" -"@firebase/database-types@0.3.8": - version "0.3.8" - resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.3.8.tgz#d35b2fb09fa0ba1f542c16b6fa5a7471f85fc2a4" +"@firebase/app-types@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@firebase/app-types/-/app-types-0.5.2.tgz#3d9e86283b6b37d9384e42eecd04df9fae384466" + integrity sha512-k3zRi9gXyWrymu8OL6DA1Pz7eo+sKVBopX5ouOjQwozAZ55WhelifPC99WHmLWo8sAokNM0XDyzM7loOA5yliQ== -"@firebase/database@^0.3.6": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.3.17.tgz#12c2ec7bb528ffb97efebfd15e2157114164fd27" +"@firebase/auth-interop-types@0.1.3": + version "0.1.3" + resolved "https://registry.yarnpkg.com/@firebase/auth-interop-types/-/auth-interop-types-0.1.3.tgz#ee28e96c795bde1d92670af2c26d6c32d468ffdc" + integrity sha512-Fd0MJ8hHw/MasNTJz7vl5jnMMs71X6pY/VqN0V6lqdP5HKTuyPVnffJ1d2Vb6uCLZ1D7nXAer4YWj9cOrNLPAQ== + +"@firebase/component@0.1.6": + version "0.1.6" + resolved "https://registry.yarnpkg.com/@firebase/component/-/component-0.1.6.tgz#e983f0630ae3f01003ead85330c1fe3cd2623c1f" + integrity sha512-dm5pVhm+sU8ag1M3hY6vleA/H7Ed8sKRxbm4TAKhtjGHDejPXxnK0meTNydJ3MwisHWlwzGuzIEhb223K7FFxA== dependencies: - "@firebase/database-types" "0.3.8" - "@firebase/logger" "0.1.10" - "@firebase/util" "0.2.11" - faye-websocket "0.11.1" - tslib "1.9.3" + "@firebase/util" "0.2.41" + tslib "1.10.0" -"@firebase/logger@0.1.10": - version "0.1.10" - resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.10.tgz#b71549c86166d0932e857eda81ab20028bf22610" +"@firebase/database-types@0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@firebase/database-types/-/database-types-0.4.12.tgz#f6946e8605260f1c38a7db1b858d3d42e30bcbf4" + integrity sha512-PVCTQRG9fnN1cam3Qr91+WzsCf9tO+lmUcPEb0uvafSFVhvx2U9OZOlYDdM5hS0MMHTNXI7Ywmc33EheIlLmMw== + dependencies: + "@firebase/app-types" "0.5.2" -"@firebase/util@0.2.11": - version "0.2.11" - resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.11.tgz#f29235a89a6fb52b273cb9151210461853b7e2b1" +"@firebase/database@^0.5.17": + version "0.5.22" + resolved "https://registry.yarnpkg.com/@firebase/database/-/database-0.5.22.tgz#347e8f5d0fae2b7d50dbc2a39ee6fb35dfa37fc9" + integrity sha512-3CVsmLFscFIAFOjjVhlT6HzFOhS0TKVbjhixp64oVZMOshp9qPHtHIytf6QXRAypbtZMPFAMGnhNu0pmPW/vtg== dependencies: - tslib "1.9.3" + "@firebase/auth-interop-types" "0.1.3" + "@firebase/component" "0.1.6" + "@firebase/database-types" "0.4.12" + "@firebase/logger" "0.1.36" + "@firebase/util" "0.2.41" + faye-websocket "0.11.3" + tslib "1.10.0" -"@google-cloud/common@^0.16.1": - version "0.16.2" - resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-0.16.2.tgz#029b3c7c4a425f1374045ba8f6a878bd50e4761c" +"@firebase/logger@0.1.36": + version "0.1.36" + resolved "https://registry.yarnpkg.com/@firebase/logger/-/logger-0.1.36.tgz#e8c634008d382169e30e944a9bf0ee02cdd88490" + integrity sha512-5Z0ryTtzRk7kjUb0/18r10oXYu8mSPAjgdbLowRBP6HdSJB7BDiUIRS7iATSmUBZLTArdroSiFJ29m7YDfm/cw== + +"@firebase/util@0.2.41": + version "0.2.41" + resolved "https://registry.yarnpkg.com/@firebase/util/-/util-0.2.41.tgz#36a0e0deb05a67b8ca79ec559d7a9859a46da519" + integrity sha512-QRu3wjU5I0ZBWrf4wgrEBYu5K5tkHjETMDPMY8WYCeekKB13k2MuJzHBjQVuStEOU7j6ygTAA0B8vXI/6B5D0g== dependencies: - array-uniq "^1.0.3" - arrify "^1.0.1" - concat-stream "^1.6.0" - create-error-class "^3.0.2" - duplexify "^3.5.0" - ent "^2.2.0" - extend "^3.0.1" - google-auto-auth "^0.9.0" - is "^3.2.0" - log-driver "1.2.7" - methmeth "^1.1.0" - modelo "^4.2.0" - request "^2.79.0" - retry-request "^3.0.0" - split-array-stream "^1.0.0" - stream-events "^1.0.1" - string-format-obj "^1.1.0" - through2 "^2.0.3" + tslib "1.10.0" -"@google-cloud/common@^0.32.0": - version "0.32.0" - resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-0.32.0.tgz#c7cfbf763f6018e14ad116da2c620a266efc0e59" +"@google-cloud/common@^2.1.1": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@google-cloud/common/-/common-2.4.0.tgz#2783b7de8435024a31453510f2dab5a6a91a4c82" + integrity sha512-zWFjBS35eI9leAHhjfeOYlK5Plcuj/77EzstnrJIZbKgF/nkqjcQuGiMCpzCwOfPyUbz8ZaEOYgbHa759AKbjg== dependencies: - "@google-cloud/projectify" "^0.3.3" - "@google-cloud/promisify" "^0.4.0" - "@types/request" "^2.48.1" - arrify "^1.0.1" + "@google-cloud/projectify" "^1.0.0" + "@google-cloud/promisify" "^1.0.0" + arrify "^2.0.0" duplexify "^3.6.0" ent "^2.2.0" extend "^3.0.2" - google-auth-library "^3.1.1" - pify "^4.0.1" + google-auth-library "^5.5.0" retry-request "^4.0.0" + teeny-request "^6.0.0" -"@google-cloud/firestore@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-1.2.0.tgz#ac81f84f7e55c5a25956011ebab628e1bbf4b2b0" +"@google-cloud/firestore@^3.0.0": + version "3.5.1" + resolved "https://registry.yarnpkg.com/@google-cloud/firestore/-/firestore-3.5.1.tgz#3084f57c56970e4fd928f821c9071124c0cf21cf" + integrity sha512-cTPKg0Yh2cSwde5tlGLHccCrhSpSMSBB0SwWm1bQwTyp4I7T8USp/mEyppd6zP2u8oQaHSPcP+lHdg/aHmL4tA== dependencies: - bun "^0.0.12" - deep-equal "^1.0.1" + deep-equal "^2.0.0" functional-red-black-tree "^1.0.1" - google-gax "^0.25.0" - lodash.merge "^4.6.1" + google-gax "^1.13.0" + readable-stream "^3.4.0" through2 "^3.0.0" -"@google-cloud/paginator@^0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-0.2.0.tgz#eab2e6aa4b81df7418f6c51e2071f64dab2c2fa5" +"@google-cloud/paginator@^2.0.0": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@google-cloud/paginator/-/paginator-2.0.3.tgz#c7987ad05d1c3ebcef554381be80e9e8da4e4882" + integrity sha512-kp/pkb2p/p0d8/SKUu4mOq8+HGwF8NPzHWkj+VKrIPQPyMRw8deZtrO/OcSiy9C/7bpfU5Txah5ltUNfPkgEXg== dependencies: - arrify "^1.0.1" - extend "^3.0.1" - split-array-stream "^2.0.0" - stream-events "^1.0.4" - -"@google-cloud/precise-date@^0.1.0": - version "0.1.0" - resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-0.1.0.tgz#02ccda04b4413fa64f098fc93db51e95af5c855a" - integrity sha512-nXt4AskYjmDLRIO+nquVVppjiLE5ficFRP3WF1JYtPnSRFRpuMusa1kysPsD/yOxt5NMmvlkUCkaFI4rHYeckQ== + arrify "^2.0.0" + extend "^3.0.2" -"@google-cloud/projectify@^0.3.0", "@google-cloud/projectify@^0.3.3": - version "0.3.3" - resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-0.3.3.tgz#bde9103d50b20a3ea3337df8c6783a766e70d41d" +"@google-cloud/precise-date@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@google-cloud/precise-date/-/precise-date-1.0.3.tgz#39c600ed52213f4158692a72c90d13b2162a93d2" + integrity sha512-wWnDGh9y3cJHLuVEY8t6un78vizzMWsS7oIWKeFtPj+Ndy+dXvHW0HTx29ZUhen+tswSlQYlwFubvuRP5kKdzQ== -"@google-cloud/promisify@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-0.4.0.tgz#4fbfcf4d85bb6a2e4ccf05aa63d2b10d6c9aad9b" +"@google-cloud/projectify@^1.0.0": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-1.0.4.tgz#28daabebba6579ed998edcadf1a8f3be17f3b5f0" + integrity sha512-ZdzQUN02eRsmTKfBj9FDL0KNDIFNjBn/d6tHQmA/+FImH5DO6ZV8E7FzxMgAUiVAUq41RFAkb25p1oHOZ8psfg== -"@google-cloud/pubsub@0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-0.18.0.tgz#d48dd78531b28383d73980b3c1e2365e04c76432" - dependencies: - "@google-cloud/common" "^0.16.1" - arrify "^1.0.0" - async-each "^1.0.1" - delay "^2.0.0" - duplexify "^3.5.4" - extend "^3.0.1" - google-auto-auth "^0.9.0" - google-gax "^0.16.0" - google-proto-files "^0.15.0" - is "^3.0.1" - lodash.chunk "^4.2.0" - lodash.merge "^4.6.0" - lodash.snakecase "^4.1.1" - protobufjs "^6.8.1" - through2 "^2.0.3" - uuid "^3.1.0" +"@google-cloud/promisify@^1.0.0": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-1.0.4.tgz#ce86ffa94f9cfafa2e68f7b3e4a7fad194189723" + integrity sha512-VccZDcOql77obTnFh0TbNED/6ZbbmHDf8UMNnzO1d5g9V0Htfm4k5cllY8P1tJsRKC3zWYGRLaViiupcgVjBoQ== -"@google-cloud/pubsub@^0.28.1": - version "0.28.1" - resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-0.28.1.tgz#8d0605e155f5a8c36f7b51363c1e139f534b5fd8" - integrity sha512-ukvR2S6DgerEJ5T0e9G2XTyk83Ajjfhy2GdNHR3qOIkFZTn1VjqnMbGK8oWtnYm4+hZ9PHPiZY4LnxvapmwaRA== +"@google-cloud/pubsub@^1.1.5": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@google-cloud/pubsub/-/pubsub-1.5.0.tgz#0ce5a60a90c87af9e9dcd28433a0da74c223007b" + integrity sha512-SCuNClo/xDGjYmciExxVjX78WPY1Ul6MK5Qn5eX2tsILqxXpf5Lan+XU/jnL53pirAIFgcxt8I2CWibSdSqRww== dependencies: - "@google-cloud/paginator" "^0.2.0" - "@google-cloud/precise-date" "^0.1.0" - "@google-cloud/projectify" "^0.3.0" - "@google-cloud/promisify" "^0.4.0" - "@sindresorhus/is" "^0.15.0" + "@google-cloud/paginator" "^2.0.0" + "@google-cloud/precise-date" "^1.0.0" + "@google-cloud/projectify" "^1.0.0" + "@google-cloud/promisify" "^1.0.0" "@types/duplexify" "^3.6.0" "@types/long" "^4.0.0" - "@types/p-defer" "^1.0.3" - arrify "^1.0.0" + arrify "^2.0.0" async-each "^1.0.1" - extend "^3.0.1" - google-auth-library "^3.0.0" - google-gax "^0.25.0" + extend "^3.0.2" + google-auth-library "^5.5.0" + google-gax "^1.7.5" is-stream-ended "^0.1.4" - lodash.merge "^4.6.0" lodash.snakecase "^4.1.1" - p-defer "^1.0.0" + p-defer "^3.0.0" protobufjs "^6.8.1" -"@google-cloud/storage@^2.3.0", "@google-cloud/storage@^2.5.0": - version "2.5.0" - resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-2.5.0.tgz#9dd3566d8155cf5ba0c212208f69f9ecd47fbd7e" +"@google-cloud/storage@^4.0.0", "@google-cloud/storage@^4.1.2": + version "4.3.1" + resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-4.3.1.tgz#fce4c4ab74d3524ab040e0e15b9b50e6df13fd39" + integrity sha512-/i7tAcUZDQNDs8/+oN+U2mOXdWdP2eld0pFKLkpthmWmaD89JQlrgHAFL7uvlgCSbaD7YxgbSyJebgd6YBgMgQ== dependencies: - "@google-cloud/common" "^0.32.0" - "@google-cloud/paginator" "^0.2.0" - "@google-cloud/promisify" "^0.4.0" - arrify "^1.0.0" - async "^2.0.1" + "@google-cloud/common" "^2.1.1" + "@google-cloud/paginator" "^2.0.0" + "@google-cloud/promisify" "^1.0.0" + arrify "^2.0.0" compressible "^2.0.12" concat-stream "^2.0.0" - date-and-time "^0.6.3" + date-and-time "^0.12.0" duplexify "^3.5.0" - extend "^3.0.0" - gcs-resumable-upload "^1.0.0" - hash-stream-validation "^0.2.1" + extend "^3.0.2" + gaxios "^2.0.1" + gcs-resumable-upload "^2.2.4" + hash-stream-validation "^0.2.2" mime "^2.2.0" mime-types "^2.0.8" onetime "^5.1.0" - pumpify "^1.5.1" + p-limit "^2.2.0" + pumpify "^2.0.0" + readable-stream "^3.4.0" snakeize "^0.1.0" stream-events "^1.0.1" - teeny-request "^3.11.3" through2 "^3.0.0" - xdg-basedir "^3.0.0" + xdg-basedir "^4.0.0" -"@grpc/grpc-js@^0.3.0": - version "0.3.6" - resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.3.6.tgz#d9b52043907170d38e06711d9477fde29ab46fa8" +"@grpc/grpc-js@^0.6.18": + version "0.6.18" + resolved "https://registry.yarnpkg.com/@grpc/grpc-js/-/grpc-js-0.6.18.tgz#ba3b3dfef869533161d192a385412a4abd0db127" + integrity sha512-uAzv/tM8qpbf1vpx1xPMfcUMzbfdqJtdCYAqY/LsLeQQlnTb4vApylojr+wlCyr7bZeg3AFfHvtihnNOQQt/nA== dependencies: - semver "^5.5.0" + semver "^6.2.0" -"@grpc/proto-loader@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.4.0.tgz#a823a51eb2fde58369bef1deb5445fd808d70901" +"@grpc/proto-loader@^0.5.1": + version "0.5.3" + resolved "https://registry.yarnpkg.com/@grpc/proto-loader/-/proto-loader-0.5.3.tgz#a233070720bf7560c4d70e29e7950c72549a132c" + integrity sha512-8qvUtGg77G2ZT2HqdqYoM/OY97gQd/0crSG34xNmZ4ZOsv3aQT/FQV9QfZPazTGna6MIoyUd+u6AxsoZjJ/VMQ== dependencies: lodash.camelcase "^4.3.0" protobufjs "^6.8.6" @@ -233,76 +392,287 @@ resolved "https://registry.yarnpkg.com/@iarna/toml/-/toml-2.2.3.tgz#f060bf6eaafae4d56a7dac618980838b0696e2ab" integrity sha512-FmuxfCuolpLl0AnQ2NHSzoUKWEJDFl63qXjzdoWBVyFCXzMGm1spBzk7LeHNoVCiWCF7mRVms9e6jEV9+MoPbg== -"@mrmlnc/readdir-enhanced@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" +"@jest/console@^24.7.1", "@jest/console@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0" + integrity sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ== dependencies: - call-me-maybe "^1.0.1" - glob-to-regexp "^0.3.0" + "@jest/source-map" "^24.9.0" + chalk "^2.0.1" + slash "^2.0.0" -"@nodelib/fs.stat@^1.1.2": - version "1.1.3" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" +"@jest/core@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-24.9.0.tgz#2ceccd0b93181f9c4850e74f2a9ad43d351369c4" + integrity sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A== + dependencies: + "@jest/console" "^24.7.1" + "@jest/reporters" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-changed-files "^24.9.0" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-resolve-dependencies "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + jest-watcher "^24.9.0" + micromatch "^3.1.10" + p-each-series "^1.0.0" + realpath-native "^1.1.0" + rimraf "^2.5.4" + slash "^2.0.0" + strip-ansi "^5.0.0" + +"@jest/environment@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-24.9.0.tgz#21e3afa2d65c0586cbd6cbefe208bafade44ab18" + integrity sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ== + dependencies: + "@jest/fake-timers" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + +"@jest/fake-timers@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-24.9.0.tgz#ba3e6bf0eecd09a636049896434d306636540c93" + integrity sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A== + dependencies: + "@jest/types" "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + +"@jest/reporters@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-24.9.0.tgz#86660eff8e2b9661d042a8e98a028b8d631a5b43" + integrity sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + exit "^0.1.2" + glob "^7.1.2" + istanbul-lib-coverage "^2.0.2" + istanbul-lib-instrument "^3.0.1" + istanbul-lib-report "^2.0.4" + istanbul-lib-source-maps "^3.0.1" + istanbul-reports "^2.2.6" + jest-haste-map "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + node-notifier "^5.4.2" + slash "^2.0.0" + source-map "^0.6.0" + string-length "^2.0.0" -"@octokit/endpoint@^4.0.0": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-4.2.2.tgz#4ff11382bad89c7e01030a1e62d5e9d13c2402b0" - integrity sha512-5IZjkUNhx5q0IRN7Juwf5A+Lu2qAso7ULST7C1P2mbGHePuCOk936Stcl/5GdJpB3ovD8M6/Lv3xra6Mn0IKNQ== +"@jest/source-map@^24.3.0", "@jest/source-map@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-24.9.0.tgz#0e263a94430be4b41da683ccc1e6bffe2a191714" + integrity sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg== dependencies: - deepmerge "3.2.0" + callsites "^3.0.0" + graceful-fs "^4.1.15" + source-map "^0.6.0" + +"@jest/test-result@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-24.9.0.tgz#11796e8aa9dbf88ea025757b3152595ad06ba0ca" + integrity sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA== + dependencies: + "@jest/console" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/istanbul-lib-coverage" "^2.0.0" + +"@jest/test-sequencer@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz#f8f334f35b625a4f2f355f2fe7e6036dad2e6b31" + integrity sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A== + dependencies: + "@jest/test-result" "^24.9.0" + jest-haste-map "^24.9.0" + jest-runner "^24.9.0" + jest-runtime "^24.9.0" + +"@jest/transform@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-24.9.0.tgz#4ae2768b296553fadab09e9ec119543c90b16c56" + integrity sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ== + dependencies: + "@babel/core" "^7.1.0" + "@jest/types" "^24.9.0" + babel-plugin-istanbul "^5.1.0" + chalk "^2.0.1" + convert-source-map "^1.4.0" + fast-json-stable-stringify "^2.0.0" + graceful-fs "^4.1.15" + jest-haste-map "^24.9.0" + jest-regex-util "^24.9.0" + jest-util "^24.9.0" + micromatch "^3.1.10" + pirates "^4.0.1" + realpath-native "^1.1.0" + slash "^2.0.0" + source-map "^0.6.1" + write-file-atomic "2.4.1" + +"@jest/types@^24.9.0": + version "24.9.0" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-24.9.0.tgz#63cb26cb7500d069e5a389441a7c6ab5e909fc59" + integrity sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw== + dependencies: + "@types/istanbul-lib-coverage" "^2.0.0" + "@types/istanbul-reports" "^1.1.1" + "@types/yargs" "^13.0.0" + +"@nodelib/fs.scandir@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.3.tgz#3a582bdb53804c6ba6d146579c46e52130cf4a3b" + integrity sha512-eGmwYQn3gxo4r7jdQnkrrN6bY478C3P+a/y72IJukF8LjB6ZHeB3c+Ehacj3sYeSmUXGlnA67/PmbM9CVwL7Dw== + dependencies: + "@nodelib/fs.stat" "2.0.3" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.3", "@nodelib/fs.stat@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.3.tgz#34dc5f4cabbc720f4e60f75a747e7ecd6c175bd3" + integrity sha512-bQBFruR2TAwoevBEd/NWMoAAtNGzTRgdrqnYCc7dhzfoNvqPzLyqlEQnzZ3kVnNrSp25iyxE00/3h2fqGAGArA== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.4.tgz#011b9202a70a6366e436ca5c065844528ab04976" + integrity sha512-1V9XOY4rDW0rehzbrcqAmHnz8e7SKvX27gh8Gt2WgB0+pdzdiLV83p72kZPU+jvMbS1qU5mauP2iOvO8rhmurQ== + dependencies: + "@nodelib/fs.scandir" "2.1.3" + fastq "^1.6.0" + +"@octokit/auth-token@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-2.4.0.tgz#b64178975218b99e4dfe948253f0673cbbb59d9f" + integrity sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg== + dependencies: + "@octokit/types" "^2.0.0" + +"@octokit/endpoint@^5.5.0": + version "5.5.3" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-5.5.3.tgz#0397d1baaca687a4c8454ba424a627699d97c978" + integrity sha512-EzKwkwcxeegYYah5ukEeAI/gYRLv2Y9U5PpIsseGSFDk+G3RbipQGBs8GuYS1TLCtQaqoO66+aQGtITPalxsNQ== + dependencies: + "@octokit/types" "^2.0.0" is-plain-object "^3.0.0" - universal-user-agent "^2.0.1" - url-template "^2.0.8" + universal-user-agent "^5.0.0" -"@octokit/request@3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-3.0.0.tgz#304a279036b2dc89e7fba7cb30c9e6a9b1f4d2df" - integrity sha512-DZqmbm66tq+a9FtcKrn0sjrUpi0UaZ9QPUCxxyk/4CJ2rseTMpAWRf6gCwOSUCzZcx/4XVIsDk+kz5BVdaeenA== +"@octokit/plugin-paginate-rest@^1.1.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" + integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== dependencies: - "@octokit/endpoint" "^4.0.0" - deprecation "^1.0.1" - is-plain-object "^2.0.4" - node-fetch "^2.3.0" + "@octokit/types" "^2.0.1" + +"@octokit/plugin-request-log@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-1.0.0.tgz#eef87a431300f6148c39a7f75f8cfeb218b2547e" + integrity sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw== + +"@octokit/plugin-rest-endpoint-methods@2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" + integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== + dependencies: + "@octokit/types" "^2.0.1" + deprecation "^2.3.1" + +"@octokit/request-error@^1.0.1", "@octokit/request-error@^1.0.2": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" + integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== + dependencies: + "@octokit/types" "^2.0.0" + deprecation "^2.0.0" once "^1.4.0" - universal-user-agent "^2.0.1" -"@octokit/rest@16.25.0": - version "16.25.0" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.25.0.tgz#1111dc2b2058bc77442fd7fbd295dab3991b62bf" - integrity sha512-QKIzP0gNYjyIGmY3Gpm3beof0WFwxFR+HhRZ+Wi0fYYhkEUvkJiXqKF56Pf5glzzfhEwOrggfluEld5F/ZxsKw== +"@octokit/request@^5.2.0": + version "5.3.2" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.3.2.tgz#1ca8b90a407772a1ee1ab758e7e0aced213b9883" + integrity sha512-7NPJpg19wVQy1cs2xqXjjRq/RmtSomja/VSWnptfYwuBxLdbYh2UjhGi0Wx7B1v5Iw5GKhfFDQL7jM7SSp7K2g== dependencies: - "@octokit/request" "3.0.0" + "@octokit/endpoint" "^5.5.0" + "@octokit/request-error" "^1.0.1" + "@octokit/types" "^2.0.0" + deprecation "^2.0.0" + is-plain-object "^3.0.0" + node-fetch "^2.3.0" + once "^1.4.0" + universal-user-agent "^5.0.0" + +"@octokit/rest@16.43.1": + version "16.43.1" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b" + integrity sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw== + dependencies: + "@octokit/auth-token" "^2.4.0" + "@octokit/plugin-paginate-rest" "^1.1.1" + "@octokit/plugin-request-log" "^1.0.0" + "@octokit/plugin-rest-endpoint-methods" "2.4.0" + "@octokit/request" "^5.2.0" + "@octokit/request-error" "^1.0.2" atob-lite "^2.0.0" - before-after-hook "^1.4.0" + before-after-hook "^2.0.0" btoa-lite "^1.0.0" - deprecation "^1.0.1" + deprecation "^2.0.0" lodash.get "^4.4.2" lodash.set "^4.3.2" lodash.uniq "^4.5.0" octokit-pagination-methods "^1.1.0" once "^1.4.0" - universal-user-agent "^2.0.0" - url-template "^2.0.8" + universal-user-agent "^4.0.0" + +"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": + version "2.3.1" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-2.3.1.tgz#40cd61c125a6161cfb3bfabc75805ac7a54213b4" + integrity sha512-rvJP1Y9A/+Cky2C3var1vsw3Lf5Rjn/0sojNl2AjCX+WbpIHYccaJ46abrZoIxMYnOToul6S9tPytUVkFI7CXQ== + dependencies: + "@types/node" ">= 8" "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha1-m4sMxmPWaafY9vXQiToU00jzD78= "@protobufjs/base64@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== "@protobufjs/codegen@^2.0.4": version "2.0.4" resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== "@protobufjs/eventemitter@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha1-NVy8mLr61ZePntCV85diHx0Ga3A= "@protobufjs/fetch@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU= dependencies: "@protobufjs/aspromise" "^1.1.1" "@protobufjs/inquire" "^1.1.0" @@ -310,87 +680,71 @@ "@protobufjs/float@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E= "@protobufjs/inquire@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik= "@protobufjs/path@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha1-bMKyDFya1q0NzP0hynZz2Nf79o0= "@protobufjs/pool@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q= "@protobufjs/utf8@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@release-it/conventional-changelog@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@release-it/conventional-changelog/-/conventional-changelog-1.1.0.tgz#42e33ceff37d6ae4ad1667b4a1b1586b75e58fca" - integrity sha512-3uyHg2hgCEJMFjIbRTnGgqU//PK9u89dBlKRUmbSNALARCXiKN4XvJOWni54TsFeDSWmM3IUh9bgoGvj55ia4A== +"@release-it/conventional-changelog@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@release-it/conventional-changelog/-/conventional-changelog-2.0.0.tgz#7101d5174c1d99adf5769561bd4c0741ffa754a9" + integrity sha512-S6mm01dtRsG0ouzcxQoTF2R9q4uhJyGV9NyDmt5Yamd/zbLAPhRmtwdekjKwEkAHQMvIcM1WfbEY/YoPVW0Jbw== dependencies: concat-stream "^2.0.0" - conventional-changelog "^3.1.8" - conventional-recommended-bump "^5.0.0" - prepend-file "^1.3.1" - release-it "^12.2.1" + conventional-changelog "^3.1.23" + conventional-recommended-bump "^6.0.10" + prepend-file "^2.0.0" "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" integrity sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ== -"@sindresorhus/is@^0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.15.0.tgz#96915baa05e6a6a1d137badf4984d3fc05820bb6" - integrity sha512-lu8BpxjAtRCAo5ifytTpCPCj99LF7o/2Myn+NXyNCBqvPYn7Pjd76AMmUB5l7XF1U6t0hcWrlEM5ESufW7wAeA== - -"@sinonjs/commons@^1.0.2", "@sinonjs/commons@^1.2.0": - version "1.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.3.0.tgz#50a2754016b6f30a994ceda6d9a0a8c36adda849" +"@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.4.0", "@sinonjs/commons@^1.7.0": + version "1.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.1.tgz#da5fd19a5f71177a53778073978873964f49acf1" + integrity sha512-Debi3Baff1Qu1Unc3mjJ96MgpbwTn43S1+9yJ0llWygPwDNu2aaWBD6yc9y/Z8XDRNhx7U+u2UDg2OGQXkclUQ== dependencies: type-detect "4.0.8" -"@sinonjs/formatio@^3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.1.0.tgz#6ac9d1eb1821984d84c4996726e45d1646d8cce5" +"@sinonjs/formatio@^3.2.1": + version "3.2.2" + resolved "https://registry.yarnpkg.com/@sinonjs/formatio/-/formatio-3.2.2.tgz#771c60dfa75ea7f2d68e3b94c7e888a78781372c" + integrity sha512-B8SEsgd8gArBLMD6zpRw3juQ2FVSsmdd7qlevyDqzS9WTCtvF55/gAL+h6gue8ZvPYcdiPdvueM/qm//9XzyTQ== dependencies: - "@sinonjs/samsam" "^2 || ^3" + "@sinonjs/commons" "^1" + "@sinonjs/samsam" "^3.1.0" -"@sinonjs/samsam@^2 || ^3", "@sinonjs/samsam@^3.0.2": - version "3.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.0.2.tgz#304fb33bd5585a0b2df8a4c801fcb47fa84d8e43" +"@sinonjs/samsam@^3.1.0", "@sinonjs/samsam@^3.3.3": + version "3.3.3" + resolved "https://registry.yarnpkg.com/@sinonjs/samsam/-/samsam-3.3.3.tgz#46682efd9967b259b81136b9f120fd54585feb4a" + integrity sha512-bKCMKZvWIjYD0BLGnNrxVuw4dkWCYsLqFOUWw8VgKF/+5Y+mE7LfHWPIYoDXowH+3a9LsWDMo0uAP8YDosPvHQ== dependencies: - "@sinonjs/commons" "^1.0.2" + "@sinonjs/commons" "^1.3.0" array-from "^2.1.1" - lodash.get "^4.4.2" - -"@snyk/composer-lockfile-parser@1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@snyk/composer-lockfile-parser/-/composer-lockfile-parser-1.0.2.tgz#d748e56076bc1c25b130c1f13ed705fa285a1994" - integrity sha512-kFzMajJLgWYsRTD+j1B79RckP1nYolM3UU9wJAo6VjvaBJ1R8E6IXmz0lEJBwK2zXM4EPrgk41ZqmoQS3hselQ== - dependencies: - lodash "4.17.11" + lodash "^4.17.15" -"@snyk/dep-graph@1.8.1": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@snyk/dep-graph/-/dep-graph-1.8.1.tgz#4286dc42f691e826c4779a77722e7ac7fa692420" - integrity sha512-cWqJwuiU1+9hL0Fd/qgq0DYeWM/6mqPIa/B0yoEsHD8nR/IPFgalVvMbOSdPKeApvi/AxDzcRxr8tfqHJ7aq2w== - dependencies: - graphlib "^2.1.5" - lodash "^4" - object-hash "^1.3.1" - semver "^6.0.0" - source-map-support "^0.5.11" - tslib "^1.9.3" - -"@snyk/gemfile@1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@snyk/gemfile/-/gemfile-1.2.0.tgz#919857944973cce74c650e5428aaf11bcd5c0457" - integrity sha512-nI7ELxukf7pT4/VraL4iabtNNMz8mUo7EXlqCFld8O5z6mIMLX9llps24iPpaIZOwArkY3FWA+4t+ixyvtTSIA== +"@sinonjs/text-encoding@^0.7.1": + version "0.7.1" + resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" + integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== "@szmarczak/http-timer@^1.1.2": version "1.1.2" @@ -399,61 +753,141 @@ dependencies: defer-to-connect "^1.0.1" -"@types/accepts@^1.3.5": +"@tootallnate/once@1": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.0.0.tgz#9c13c2574c92d4503b005feca8f2e16cc1611506" + integrity sha512-KYyTT/T6ALPkIRd2Ge080X/BsXvy9O0hcWTtMWkPvwAwF99+vn6Dv4GzrFT/Nn1LePr+FFDbRXXlqmsy9lw2zA== + +"@types/accepts@*", "@types/accepts@^1.3.5": version "1.3.5" resolved "https://registry.yarnpkg.com/@types/accepts/-/accepts-1.3.5.tgz#c34bec115cfc746e04fe5a059df4ce7e7b391575" + integrity sha512-jOdnI/3qTpHABjM5cx1Hc0sKsPoYCp+DP/GJRGtDlPd7fiV9oXGGIcjW/ZOxLIvjGz8MA+uMZI9metHlgqbgwQ== dependencies: "@types/node" "*" -"@types/agent-base@^4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@types/agent-base/-/agent-base-4.2.0.tgz#00644e8b395b40e1bf50aaf1d22cabc1200d5051" - integrity sha512-8mrhPstU+ZX0Ugya8tl5DsDZ1I5ZwQzbL/8PA0z8Gj0k9nql7nkaMzmPVLj+l/nixWaliXi+EBiLA8bptw3z7Q== +"@types/babel__core@^7.1.0": + version "7.1.6" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.6.tgz#16ff42a5ae203c9af1c6e190ed1f30f83207b610" + integrity sha512-tTnhWszAqvXnhW7m5jQU9PomXSiKXk2sFxpahXvI20SZKu9ylPi8WtIxueZ6ehDWikPT0jeFujMj3X4ZHuf3Tg== dependencies: - "@types/events" "*" - "@types/node" "*" + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.1" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.1.tgz#4901767b397e8711aeb99df8d396d7ba7b7f0e04" + integrity sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.0.2.tgz#4ff63d6b52eddac1de7b975a5223ed32ecea9307" + integrity sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.9.tgz#be82fab304b141c3eee81a4ce3b034d0eba1590a" + integrity sha512-jEFQ8L1tuvPjOI8lnpaf73oCJe+aoxL6ygqSy6c8LcW98zaC+4mzWuQIRCEvKeCOu+lbqdXcg4Uqmm1S8AP1tw== + dependencies: + "@babel/types" "^7.3.0" "@types/bluebird@*": - version "3.5.25" - resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.25.tgz#59188b871208092e37767e4b3d80c3b3eaae43bd" + version "3.5.29" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.29.tgz#7cd933c902c4fc83046517a1bef973886d00bdb6" + integrity sha512-kmVtnxTuUuhCET669irqQmPAez4KFnFVKvpleVRyfC3g+SHD1hIkFZcWLim9BVcwUBLO59o8VZE4yGCmTif8Yw== -"@types/body-parser@*", "@types/body-parser@1.17.0", "@types/body-parser@^1.17.0": - version "1.17.0" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c" +"@types/body-parser@*": + version "1.19.0" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.0.tgz#0685b3c47eb3006ffed117cdd55164b61f80538f" + integrity sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/body-parser@1.17.1": + version "1.17.1" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.1.tgz#18fcf61768fb5c30ccc508c21d6fd2e8b3bf7897" + integrity sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w== dependencies: "@types/connect" "*" "@types/node" "*" "@types/bson@*": - version "1.0.11" - resolved "https://registry.yarnpkg.com/@types/bson/-/bson-1.0.11.tgz#c95ad69bb0b3f5c33b4bb6cc86d86cafb273335c" + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/bson/-/bson-4.0.1.tgz#2bfc80819e7055b76d5496d5344ed23e5d12bbb2" + integrity sha512-K6VAEdLVJFBxKp8m5cRTbUfeZpuSvOuLKJLrgw9ANIXo00RiyGzgH4BKWWR4F520gV4tWmxG7q9sKQRVDuzrBw== dependencies: "@types/node" "*" "@types/caseless@*": - version "0.12.1" - resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.1.tgz#9794c69c8385d0192acc471a540d1f8e0d16218a" + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + +"@types/color-name@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" + integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== "@types/connect@*": - version "3.4.32" - resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28" + version "3.4.33" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.33.tgz#31610c901eca573b8713c3330abc6e6b9f588546" + integrity sha512-2+FrkXY4zllzTNfJth7jOqEHC+enpLeGslEhpnTAkg21GkRrWV4SsAtqchtT4YS9/nODBU2/ZfsBY2X4J/dX7A== dependencies: "@types/node" "*" +"@types/cookies@*": + version "0.7.4" + resolved "https://registry.yarnpkg.com/@types/cookies/-/cookies-0.7.4.tgz#26dedf791701abc0e36b5b79a5722f40e455f87b" + integrity sha512-oTGtMzZZAVuEjTwCjIh8T8FrC8n/uwy+PG0yTvQcdZ7etoel7C7/3MSd7qrukENTgQtotG7gvBlBojuVs7X5rw== + dependencies: + "@types/connect" "*" + "@types/express" "*" + "@types/keygrip" "*" + "@types/node" "*" + "@types/cors@^2.8.4": - version "2.8.4" - resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.4.tgz#50991a759a29c0b89492751008c6af7a7c8267b0" + version "2.8.6" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.6.tgz#cfaab33c49c15b1ded32f235111ce9123009bd02" + integrity sha512-invOmosX0DqbpA+cE2yoHGUlF/blyf7nB0OGYBBiH27crcVm5NmFaZkLP4Ta1hGaesckCi5lVLlydNJCxkTOSg== dependencies: "@types/express" "*" -"@types/debug@^4.1.4": - version "4.1.4" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.4.tgz#56eec47706f0fd0b7c694eae2f3172e6b0b769da" - integrity sha512-D9MyoQFI7iP5VdpEyPZyjjqIJ8Y8EDNQFIFVLOmeg1rI1xiHOChyUPMPRUVfqFCerxfE+yS3vMyj37F6IdtOoQ== +"@types/cross-spawn@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.1.tgz#60fa0c87046347c17d9735e5289e72b804ca9b63" + integrity sha512-MtN1pDYdI6D6QFDzy39Q+6c9rl2o/xN7aWGe6oZuzqq5N6+YuwFsWiEAv3dNzvzN9YzU+itpN8lBzFpphQKLAw== + dependencies: + "@types/node" "*" + +"@types/debug@^4.1.5": + version "4.1.5" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" + integrity sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ== + +"@types/decompress@^4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@types/decompress/-/decompress-4.2.3.tgz#98eed48af80001038aa05690b2094915f296fe65" + integrity sha512-W24e3Ycz1UZPgr1ZEDHlK4XnvOr+CpJH3qNsFeqXwwlW/9END9gxn3oJSsp7gYdiQxrXUHwUUd3xuzVz37MrZQ== + dependencies: + "@types/node" "*" + +"@types/dedent@^0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/dedent/-/dedent-0.7.0.tgz#155f339ca404e6dd90b9ce46a3f78fd69ca9b050" + integrity sha512-EGlKlgMhnLt/cM4DbUSafFdrkeJoC9Mvnj0PUCU7tFmTjMjNRT957kXCx0wYm3JuEq4o4ZsS5vG+NlkM2DMd2A== "@types/dotenv@^4.0.3": version "4.0.3" resolved "https://registry.yarnpkg.com/@types/dotenv/-/dotenv-4.0.3.tgz#ebcfc40da7bc0728b705945b7db48485ec5b4b67" + integrity sha512-mmhpINC/HcLGQK5ikFJlLXINVvcxhlrV+ZOUJSN7/ottYl+8X4oSXzS9lBtDkmWAl96EGyGyLrNvk9zqdSH8Fw== dependencies: "@types/node" "*" @@ -465,38 +899,78 @@ "@types/node" "*" "@types/events@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== "@types/express-serve-static-core@*": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.0.tgz#fdfe777594ddc1fe8eb8eccce52e261b496e43e7" + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.2.tgz#f6f41fa35d42e79dbf6610eccbb2637e6008a0cf" + integrity sha512-El9yMpctM6tORDAiBwZVLMcxoTMcqqRO9dVyYcn7ycLWbvR8klrDn8CAOwRfZujZtWD7yS/mshTdz43jMOejbg== dependencies: - "@types/events" "*" "@types/node" "*" "@types/range-parser" "*" -"@types/express@*", "@types/express@4.16.0", "@types/express@^4.16.0": - version "4.16.0" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.16.0.tgz#6d8bc42ccaa6f35cf29a2b7c3333cb47b5a32a19" +"@types/express@*", "@types/express@4.17.2": + version "4.17.2" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c" + integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA== dependencies: "@types/body-parser" "*" "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/form-data@*": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/form-data/-/form-data-2.2.1.tgz#ee2b3b8eaa11c0938289953606b745b738c54b1e" +"@types/express@^4.17.6": + version "4.17.6" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.6.tgz#6bce49e49570507b86ea1b07b806f04697fac45e" + integrity sha512-n/mr9tZI83kd4azlPG5y997C/M4DNABK9yErhFM6hKdym4kkmd9j0vtsJyjFIwfRBxtrxZtAfGZCNRIBMFLK5w== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/find-cache-dir@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/find-cache-dir/-/find-cache-dir-2.0.1.tgz#7d44d34f6fcde70989517cb4f66042a9bf83f5e3" + integrity sha512-L+8XrNQEa8EL8C9rwWxbC2R+K7tWVg+Ib5zTp0GmFajjUvTBGDqaY0WAdDBGSXdO0eEG3O0FCjdIHNfjyluF1Q== + +"@types/find-package-json@^1.1.0": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/find-package-json/-/find-package-json-1.1.1.tgz#c0d296ac74fe3309ed0fe75a9c3edb42a776d30c" + integrity sha512-XMCocYkg6VUpkbOQMKa3M5cgc3MvU/LJKQwd3VUJrWZbLr2ARUggupsCAF8DxjEEIuSO6HlnH+vl+XV4bgVeEQ== dependencies: "@types/node" "*" "@types/formidable@^1.0.31": version "1.0.31" resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-1.0.31.tgz#274f9dc2d0a1a9ce1feef48c24ca0859e7ec947b" + integrity sha512-dIhM5t8lRP0oWe2HF8MuPvdd1TpPTjhDMAqemcq6oIZQCBQTovhBAdTQ5L5veJB4pdQChadmHuxtB0YzqvfU3Q== dependencies: "@types/events" "*" "@types/node" "*" +"@types/fs-capacitor@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/fs-capacitor/-/fs-capacitor-2.0.0.tgz#17113e25817f584f58100fb7a08eed288b81956e" + integrity sha512-FKVPOCFbhCvZxpVAMhdBdTfVfXUpsh15wFHgqOKxh9N9vzWZVuWCSijZ5T4U34XYNnuj2oduh6xcs1i+LPI+BQ== + dependencies: + "@types/node" "*" + +"@types/fs-extra@^8.0.1": + version "8.1.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-8.1.0.tgz#1114834b53c3914806cd03b3304b37b3bd221a4d" + integrity sha512-UoOfVEzAUpeSPmjm7h1uk5MH6KZma2z2O7a75onTGjnNvAvMVrPzPL/vBbT65iIGHWj6rokwfmYcmxmlSf2uwg== + dependencies: + "@types/node" "*" + +"@types/get-port@^4.0.1": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@types/get-port/-/get-port-4.2.0.tgz#4fc44616c737d37d3ee7926d86fa975d0afba5e4" + integrity sha512-Iv2FAb5RnIk/eFO2CTu8k+0VMmIR15pKbcqRWi+s3ydW+aKXlN2yemP92SrO++ERyJx+p6Ie1ggbLBMbU1SjiQ== + dependencies: + get-port "*" + "@types/glob@^7.1.1": version "7.1.1" resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575" @@ -506,131 +980,264 @@ "@types/minimatch" "*" "@types/node" "*" -"@types/google-cloud__pubsub@^0.18.1": - version "0.18.1" - resolved "https://registry.yarnpkg.com/@types/google-cloud__pubsub/-/google-cloud__pubsub-0.18.1.tgz#82df118603394b637f39892a63ad977be95e58c4" +"@types/graphql-upload@^8.0.0": + version "8.0.3" + resolved "https://registry.yarnpkg.com/@types/graphql-upload/-/graphql-upload-8.0.3.tgz#b371edb5f305a2a1f7b7843a890a2a7adc55c3ec" + integrity sha512-hmLg9pCU/GmxBscg8GCr1vmSoEmbItNNxdD5YH2TJkXm//8atjwuprB+xJBK714JG1dkxbbhp5RHX+Pz1KsCMA== dependencies: - "@types/events" "*" - "@types/node" "*" + "@types/express" "*" + "@types/fs-capacitor" "*" + "@types/koa" "*" + graphql "^14.5.3" "@types/graphql@^14.0.3": - version "14.0.3" - resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.0.3.tgz#389e2e5b83ecdb376d9f98fae2094297bc112c1c" + version "14.5.0" + resolved "https://registry.yarnpkg.com/@types/graphql/-/graphql-14.5.0.tgz#a545fb3bc8013a3547cf2f07f5e13a33642b75d6" + integrity sha512-MOkzsEp1Jk5bXuAsHsUi6BVv0zCO+7/2PTiZMXWDSsMXvNU6w/PLMQT2vHn8hy2i0JqojPz1Sz6rsFjHtsU0lA== + dependencies: + graphql "*" + +"@types/http-assert@*": + version "1.5.1" + resolved "https://registry.yarnpkg.com/@types/http-assert/-/http-assert-1.5.1.tgz#d775e93630c2469c2f980fc27e3143240335db3b" + integrity sha512-PGAK759pxyfXE78NbKxyfRcWYA/KwW17X290cNev/qAsn9eQIxkH4shoNBafH37wewhDG/0p1cHPbK6+SzZjWQ== "@types/ioredis@^3.2.15": - version "3.2.17" - resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-3.2.17.tgz#c0037731eeee9d9669bdc7a9b9dcc014e14139ab" + version "3.2.23" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-3.2.23.tgz#c5ef1972da46dc477fb719389ee54cb2de9d6466" + integrity sha512-KZQHgEQ10LK8o/gHn4ZTsa7Ni9oaTizAw3MN15asNO7bzIeRbb7PjRDSGSBe+2jCYBUNAIbxin1jmqu5npxOLA== dependencies: "@types/bluebird" "*" "@types/node" "*" -"@types/jest@^23.3.0": - version "23.3.10" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-23.3.10.tgz#4897974cc317bf99d4fe6af1efa15957fa9c94de" +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" + integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + +"@types/istanbul-lib-report@*": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" + integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== + dependencies: + "@types/istanbul-lib-coverage" "*" + +"@types/istanbul-reports@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" + integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + dependencies: + "@types/istanbul-lib-coverage" "*" + "@types/istanbul-lib-report" "*" + +"@types/jest@^24.0.21": + version "24.9.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-24.9.1.tgz#02baf9573c78f1b9974a5f36778b366aa77bd534" + integrity sha512-Fb38HkXSVA4L8fGKEZ6le5bB8r6MRWlOCZbVuWZcmOMSCd2wCYOwN1ibj8daIoV9naq7aaOZjrLCoCMptKU/4Q== + dependencies: + jest-diff "^24.3.0" + +"@types/json2csv@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@types/json2csv/-/json2csv-5.0.1.tgz#576d38515dfedeabf46eb85e790894b8df72ab40" + integrity sha512-1r5GCTyFtdQ53CRSIctzWZCmtDXvxtzM77SzOqPB4woMeGcc3rhUMzPqEQH3rokG1k/QLzlC5Qe5Ih8NuFN70Q== + dependencies: + "@types/node" "*" + +"@types/keygrip@*": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@types/keygrip/-/keygrip-1.0.2.tgz#513abfd256d7ad0bf1ee1873606317b33b1b2a72" + integrity sha512-GJhpTepz2udxGexqos8wgaBx4I/zWIDPh/KOGEwAqtuGDkOUJu5eFvwmdBX4AmB8Odsr+9pHCQqiAqDL/yKMKw== + +"@types/koa-compose@*": + version "3.2.5" + resolved "https://registry.yarnpkg.com/@types/koa-compose/-/koa-compose-3.2.5.tgz#85eb2e80ac50be95f37ccf8c407c09bbe3468e9d" + integrity sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ== + dependencies: + "@types/koa" "*" + +"@types/koa@*": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@types/koa/-/koa-2.11.2.tgz#0595656a59ff13ca97edf6dde7da1e5319651f9b" + integrity sha512-2UPelagNNW6bnc1I5kIzluCaheXRA9S+NyOdXEFFj9Az7jc15ek5V03kb8OTbb3tdZ5i2BIJObe86PhHvpMolg== + dependencies: + "@types/accepts" "*" + "@types/cookies" "*" + "@types/http-assert" "*" + "@types/keygrip" "*" + "@types/koa-compose" "*" + "@types/node" "*" + +"@types/lockfile@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/lockfile/-/lockfile-1.0.1.tgz#434a3455e89843312f01976e010c60f1bcbd56f7" + integrity sha512-65WZedEm4AnOsBDdsapJJG42MhROu3n4aSSiu87JXF/pSdlubxZxp3S1yz3kTfkJ2KBPud4CpjoHVAptOm9Zmw== "@types/long@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.0.tgz#719551d2352d301ac8b81db732acb6bdc28dbdef" + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.1.tgz#459c65fa1867dafe6a8f322c4c51695663cc55e9" + integrity sha512-5tXH6Bx/kNGd3MgffdmP4dy2Z+G4eaXw0SE81Tq3BNadtnMR5/ySMzX4SLEzHJzSmPNn4HIdpQsBvXMUykr58w== + +"@types/md5-file@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/md5-file/-/md5-file-4.0.1.tgz#5e6cfb7949dc375049b8f6fd8f91adacfc176c63" + integrity sha512-uK6vlo/LJp6iNWinpSzZwMe8Auzs0UYxesm7OGfQS3oz6PJciHtrKcqVOGk4wjYKawrl234vwNWvHyXH1ZzRyQ== "@types/mime@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" + version "2.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d" + integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw== "@types/minimatch@*": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/minimist@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6" + integrity sha1-aaI6OtKcrwCX8G7aWbNh7i8GOfY= + +"@types/mkdirp@^0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" + integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== + dependencies: + "@types/node" "*" + "@types/mongodb@*", "@types/mongodb@^3.1.2": - version "3.1.18" - resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.1.18.tgz#24e847c15155226895ec9c79660ec0907005deff" + version "3.3.16" + resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.3.16.tgz#3b125aa0e31ef0f6733fcfcf507cce1324823a26" + integrity sha512-i1Ov36BXdp+urtPsaSvrNfCdsaVn4Ukq1m1kGyzdWB1+eg3gJ68unXU5LNmnF4EAMRFY6FXZzA7/W3NST40b8g== dependencies: "@types/bson" "*" "@types/node" "*" -"@types/mongoose@^5.2.1": - version "5.3.5" - resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.3.5.tgz#9e84a5ddebfb404adc3b5afa90b41e7474397870" +"@types/mongoose@^5.5.32": + version "5.7.3" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.7.3.tgz#2fc7f26cb0fda43d8ff46b5ab6ad13068370f268" + integrity sha512-kZR/hBOft/Nm6aFP/1k0aBrfaYZQBM8I7eynpiOdgON2GqzSTd0S1kSGLUkeDLrm5NLcJ6wbXyrbYRm/nWZvlA== dependencies: "@types/mongodb" "*" "@types/node" "*" -"@types/node@*", "@types/node@^10.1.0": - version "10.12.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" +"@types/node-fetch@2.5.4": + version "2.5.4" + resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.5.4.tgz#5245b6d8841fc3a6208b82291119bc11c4e0ce44" + integrity sha512-Oz6id++2qAOFuOlE1j0ouk1dzl3mmI1+qINPNBhi9nt/gVOz0G+13Ao6qjhdF0Ys+eOkhu6JnFmt38bR3H0POQ== + dependencies: + "@types/node" "*" -"@types/node@^8.0.53": - version "8.10.45" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.45.tgz#4c49ba34106bc7dced77ff6bae8eb6543cde8351" +"@types/node@*", "@types/node@>= 8": + version "13.7.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.7.tgz#1628e6461ba8cc9b53196dfeaeec7b07fa6eea99" + integrity sha512-Uo4chgKbnPNlxQwoFmYIwctkQVkMMmsAoGGU4JKwLuvBefF0pCq4FybNSnfkfRCpC7ZW7kttcC/TrRtAJsvGtg== -"@types/node@^8.0.7": - version "8.10.50" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.50.tgz#f3d68482b1f54b5f4fba8daaac385db12bb6a706" - integrity sha512-+ZbcUwJdaBgOZpwXeT0v+gHC/jQbEfzoc9s4d0rN0JIKeQbuTrT+A2n1aQY6LpZjrLXJT7avVUqiCecCJeeZxA== +"@types/node@^10.1.0": + version "10.17.17" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.17.tgz#7a183163a9e6ff720d86502db23ba4aade5999b8" + integrity sha512-gpNnRnZP3VWzzj5k3qrpRC6Rk3H/uclhAVo1aIvwzK5p5cOrs9yEyQ8H/HBsBY0u5rrWxXEiVPQ0dEB6pkjE8Q== -"@types/p-defer@^1.0.3": - version "1.0.3" - resolved "https://registry.yarnpkg.com/@types/p-defer/-/p-defer-1.0.3.tgz#786ce79c86f779fcd9e9bec4f1fbd1167aeac064" - integrity sha512-0CK39nXek0mSZL/lnGYjhcR1QLAxg9N0/5S1BvU+MQwjlP4Jd2ebbEkJ/bEUqYMAvKLMZcGd4sJE13dnUKlDnQ== +"@types/node@^8.10.59": + version "8.10.59" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.10.59.tgz#9e34261f30183f9777017a13d185dfac6b899e04" + integrity sha512-8RkBivJrDCyPpBXhVZcjh7cQxVBSmRk9QM7hOketZzp6Tg79c0N8kkpAIito9bnJ3HCVCHVYz+KHTEbfQNfeVQ== -"@types/q@^1.5.0": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.1.tgz#48fd98c1561fe718b61733daed46ff115b496e18" +"@types/normalize-package-data@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" + integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA== + +"@types/qs@*": + version "6.9.1" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.1.tgz#937fab3194766256ee09fcd40b781740758617e7" + integrity sha512-lhbQXx9HKZAPgBkISrBcmAcMpZsmpe/Cd/hY7LGZS5OfkySUBItnPZHgQPssWYUET8elF+yCFBbP1Q0RZPTdaw== "@types/range-parser@*": version "1.2.3" resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.3.tgz#7ee330ba7caafb98090bece86a5ee44115904c2c" + integrity sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA== -"@types/redis@^2.8.13": - version "2.8.13" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.13.tgz#fdd76d9a8b7c36ff24d8506a94ef2a890c87f498" - integrity sha512-p86cm5P6DMotUqCS6odQRz0JJwc5QXZw9eyH0ALVIqmq12yqtex5ighWyGFHKxak9vaA/GF/Ilu0KZ0MuXXUbg== - dependencies: - "@types/node" "*" - -"@types/request@^2.47.1", "@types/request@^2.48.1": - version "2.48.1" - resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.1.tgz#e402d691aa6670fbbff1957b15f1270230ab42fa" +"@types/request@^2.47.1": + version "2.48.4" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.4.tgz#df3d43d7b9ed3550feaa1286c6eabf0738e6cf7e" + integrity sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw== dependencies: "@types/caseless" "*" - "@types/form-data" "*" "@types/node" "*" "@types/tough-cookie" "*" + form-data "^2.5.0" "@types/serve-static@*": - version "1.13.2" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.2.tgz#f5ac4d7a6420a99a6a45af4719f4dcd8cd907a48" + version "1.13.3" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.3.tgz#eb7e1c41c4468272557e897e9171ded5e2ded9d1" + integrity sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g== dependencies: "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/stack-utils@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" + integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== + "@types/strip-bom@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2" + integrity sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I= "@types/strip-json-comments@0.0.30": version "0.0.30" resolved "https://registry.yarnpkg.com/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz#9aa30c04db212a9a0649d6ae6fd50accc40748a1" + integrity sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ== + +"@types/tmp@0.1.0": + version "0.1.0" + resolved "https://registry.yarnpkg.com/@types/tmp/-/tmp-0.1.0.tgz#19cf73a7bcf641965485119726397a096f0049bd" + integrity sha512-6IwZ9HzWbCq6XoQWhxLpDjuADodH/MKXRUIDFudvgjcVdjFknvmR+DNsoUeer4XPrEnrZs04Jj+kfV9pFsrhmA== "@types/tough-cookie@*": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.4.tgz#821878b81bfab971b93a265a561d54ea61f9059f" + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" + integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== "@types/underscore@^1.8.9": - version "1.8.9" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.8.9.tgz#fef41f800cd23db1b4f262ddefe49cd952d82323" + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.9.4.tgz#22d1a3e6b494608e430221ec085fa0b7ccee7f33" + integrity sha512-CjHWEMECc2/UxOZh0kpiz3lEyX2Px3rQS9HzD20lxMvx571ivOBQKeLnqEjxUY0BMgp6WJWo/pQLRBwMW5v4WQ== + +"@types/uuid@3.4.6": + version "3.4.6" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.6.tgz#d2c4c48eb85a757bf2927f75f939942d521e3016" + integrity sha512-cCdlC/1kGEZdEglzOieLDYBxHsvEOIg7kp/2FYyVR9Pxakq+Qf/inL3RKQ+PA8gOlI/NnL+fXmQH12nwcGzsHw== + dependencies: + "@types/node" "*" "@types/ws@^6.0.0": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.1.tgz#ca7a3f3756aa12f62a0a62145ed14c6db25d5a28" + version "6.0.4" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1" + integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg== dependencies: - "@types/events" "*" "@types/node" "*" -"@yarnpkg/lockfile@^1.0.2": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" - integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== +"@types/yargs-parser@*": + version "15.0.0" + resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-15.0.0.tgz#cb3f9f741869e20cce330ffbeb9271590483882d" + integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== + +"@types/yargs@^13.0.0": + version "13.0.8" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.8.tgz#a38c22def2f1c2068f8971acb3ea734eb3c64a99" + integrity sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA== + dependencies: + "@types/yargs-parser" "*" + +"@wry/equality@^0.1.2": + version "0.1.9" + resolved "https://registry.yarnpkg.com/@wry/equality/-/equality-0.1.9.tgz#b13e18b7a8053c6858aa6c85b54911fb31e3a909" + integrity sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ== + dependencies: + tslib "^1.9.3" JSONStream@^1.0.4: version "1.3.5" @@ -640,155 +1247,200 @@ JSONStream@^1.0.4: jsonparse "^1.2.0" through ">=2.2.7 <3" -abab@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" - abab@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.0.tgz#aba0ab4c5eee2d4c79d3487d85450fb2376ebb0f" - -abbrev@1, abbrev@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - -abort-controller@^2.0.2: version "2.0.3" - resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-2.0.3.tgz#b174827a732efadff81227ed4b8d1cc569baf20a" - dependencies: - event-target-shim "^5.0.0" + resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.3.tgz#623e2075e02eb2d3f2475e49f99c91846467907a" + integrity sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg== abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== dependencies: event-target-shim "^5.0.0" -accepts@^1.3.5, accepts@~1.3.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" - dependencies: - mime-types "~2.1.18" - negotiator "0.6.1" - -acorn-es7-plugin@^1.0.12: - version "1.1.7" - resolved "https://registry.yarnpkg.com/acorn-es7-plugin/-/acorn-es7-plugin-1.1.7.tgz#f2ee1f3228a90eead1245f9ab1922eb2e71d336b" - -acorn-globals@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf" +accepts@^1.3.5, accepts@~1.3.4, accepts@~1.3.7: + version "1.3.7" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" + integrity sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA== dependencies: - acorn "^4.0.4" + mime-types "~2.1.24" + negotiator "0.6.2" acorn-globals@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.0.tgz#e3b6f8da3c1552a95ae627571f7dd6923bb54103" + version "4.3.4" + resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.4.tgz#9fa1926addc11c97308c4e66d7add0d40c3272e7" + integrity sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A== dependencies: acorn "^6.0.1" acorn-walk "^6.0.1" acorn-walk@^6.0.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.1.1.tgz#d363b66f5fac5f018ff9c3a1e7b6f8e310cc3913" - -acorn@^4.0.4: - version "4.0.13" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787" + version "6.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" + integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== -acorn@^5.0.0, acorn@^5.5.3: +acorn@^5.5.3: version "5.7.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== acorn@^6.0.1: - version "6.0.4" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.0.4.tgz#77377e7353b72ec5104550aa2d2097a2fd40b754" + version "6.4.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.4.0.tgz#b659d2ffbafa24baf5db1cdbb2c94a983ecd2784" + integrity sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw== + +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= adler-32@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/adler-32/-/adler-32-1.2.0.tgz#6a3e6bf0a63900ba15652808cb15c6813d1a5f25" + integrity sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU= dependencies: exit-on-epipe "~1.0.1" printj "~1.1.0" -agent-base@4, agent-base@^4.2.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee" - integrity sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg== +after@0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" + integrity sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8= + +agent-base@5: + version "5.1.1" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" + integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== + +agent-base@6: + version "6.0.0" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" + integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== dependencies: - es6-promisify "^5.0.0" + debug "4" -agent-base@^4.1.0, agent-base@~4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" +agentkeepalive@^3.4.1: + version "3.5.2" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" + integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== dependencies: - es6-promisify "^5.0.0" + humanize-ms "^1.2.1" ajv@^6.5.5: - version "6.6.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.6.2.tgz#caceccf474bf3fc3ce3b147443711a24063cc30d" + version "6.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" + integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== dependencies: - fast-deep-equal "^2.0.1" + fast-deep-equal "^3.1.1" fast-json-stable-stringify "^2.0.0" json-schema-traverse "^0.4.1" uri-js "^4.2.2" -ansi-align@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-2.0.0.tgz#c36aeccba563b89ceb556f3690f0b1d9e3547f7f" - integrity sha1-w2rsy6VjuJzrVW82kPCx2eNUf38= +amp-message@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/amp-message/-/amp-message-0.1.2.tgz#a78f1c98995087ad36192a41298e4db49e3dfc45" + integrity sha1-p48cmJlQh602GSpBKY5NtJ49/EU= dependencies: - string-width "^2.0.0" + amp "0.3.1" + +amp@0.3.1, amp@~0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/amp/-/amp-0.3.1.tgz#6adf8d58a74f361e82c1fa8d389c079e139fc47d" + integrity sha1-at+NWKdPNh6CwfqNOJwHnhOfxH0= + +amqplib@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.5.3.tgz#7ccfc85d12ee7cd3c6dc861bb07f0648ec3d7193" + integrity sha512-ZOdUhMxcF+u62rPI+hMtU1NBXSDFQ3eCJJrenamtdQ7YYwh7RZJHOIM1gonVbZ5PyVdYH4xqBPje9OYqk7fnqw== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +amqplib@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.6.0.tgz#87857c7c95d56d22438ced4cf1f7e5f0dc43b309" + integrity sha512-zXCh4jQ77TBZe1YtvZ1n7sUxnTjnNagpy8MVi2yc1ive239pS3iLwm4e4d5o4XZGx1BdTKQ/U0ZmaDU3c8MxYQ== + dependencies: + bitsyntax "~0.1.0" + bluebird "^3.5.2" + buffer-more-ints "~1.0.0" + readable-stream "1.x >=1.1.9" + safe-buffer "~5.1.2" + url-parse "~1.4.3" + +ansi-align@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-align/-/ansi-align-3.0.0.tgz#b536b371cf687caaef236c18d3e21fe3797467cb" + integrity sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw== + dependencies: + string-width "^3.0.0" ansi-escapes@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= ansi-escapes@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" - -ansi-escapes@^3.1.0, ansi-escapes@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== -ansi-escapes@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.0.tgz#c38600259cefba178ee3f7166c5ea3a5dd2e88fc" - integrity sha512-0+VX4uhi8m3aNbzoqKmkAVOEj6uQzcUHXoFPkKjhZPTpGRUBqVh930KbB6PS4zIyDZccphlLIYlu8nsjFzkXwg== +ansi-escapes@^4.2.1: + version "4.3.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.1.tgz#a5c47cc43181f1f38ffd7076837700d395522a61" + integrity sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA== dependencies: - type-fest "^0.5.2" + type-fest "^0.11.0" ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= ansi-regex@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= -ansi-regex@^4.1.0: +ansi-regex@^4.0.0, ansi-regex@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== +ansi-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" + integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== + ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + integrity sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4= ansi-styles@^3.2.0, ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== dependencies: color-convert "^1.9.0" -ansicolors@^0.3.2, ansicolors@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" +ansi-styles@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" + integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== + dependencies: + "@types/color-name" "^1.1.1" + color-convert "^2.0.1" anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== dependencies: micromatch "^2.1.5" normalize-path "^2.0.0" @@ -796,362 +1448,411 @@ anymatch@^1.3.0: anymatch@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-2.0.0.tgz#bcb24b4f37934d9aa7ac17b4adaf89e7c76ef2eb" + integrity sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw== dependencies: micromatch "^3.1.4" normalize-path "^2.1.1" -apollo-cache-control@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.4.0.tgz#fec343e6ec95aa4f1b88e07e62f067bee0c48397" +apollo-cache-control@^0.8.11: + version "0.8.11" + resolved "https://registry.yarnpkg.com/apollo-cache-control/-/apollo-cache-control-0.8.11.tgz#726e4e3c5685bacbf26c8fbba1f41b4e6252c597" + integrity sha512-8yz4qbRBIFDWRHdT8uPh0HHh+VbQXxoFGJQRAG8hyMRvR+EuURXX1ltXYkn5J3YJ3MKEqgsvwGaq60dFZq63UQ== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.4.0" + apollo-server-env "^2.4.3" + graphql-extensions "^0.10.10" -apollo-datasource@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.2.1.tgz#3ecef4efe64f7a04a43862f32027d38ac09e142c" +apollo-datasource-rest@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/apollo-datasource-rest/-/apollo-datasource-rest-0.5.1.tgz#bebbe38bdeb6c43ef00bf22dae0b932742178968" + integrity sha512-ySgAnP252L0ibJXLzDwF44HBqTV60cL8kZeK0DJgsGtv+SeynXv4GIcqlthbU5h7vzGCaZ/6ijmG4Fx5snkRpw== dependencies: - apollo-server-caching "0.2.1" - apollo-server-env "2.2.0" + apollo-datasource "0.5.0" + apollo-server-caching "0.4.0" + apollo-server-env "2.4.0" + apollo-server-errors "2.3.1" + http-cache-semantics "^4.0.0" -apollo-engine-reporting-protobuf@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.2.0.tgz#2aaf4d2eddefe7924d469cf1135267bc0deadf73" +apollo-datasource@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.5.0.tgz#7a8c97e23da7b9c15cb65103d63178ab19eca5e9" + integrity sha512-SVXxJyKlWguuDjxkY/WGlC/ykdsTmPxSF0z8FenagcQ91aPURXzXP1ZDz5PbamY+0iiCRubazkxtTQw4GWTFPg== dependencies: - protobufjs "^6.8.6" + apollo-server-caching "0.4.0" + apollo-server-env "2.4.0" -apollo-engine-reporting@0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-0.2.0.tgz#e71816b1f46e782f8538c5a118148d4c0e628e25" +apollo-datasource@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/apollo-datasource/-/apollo-datasource-0.7.0.tgz#2a6d82edb2eba21b4ddf21877009ba39ff821945" + integrity sha512-Yja12BgNQhzuFGG/5Nw2MQe0hkuQy2+9er09HxeEyAf2rUDIPnhPrn1MDoZTB8MU7UGfjwITC+1ofzKkkrZobA== dependencies: - apollo-engine-reporting-protobuf "0.2.0" - apollo-server-env "2.2.0" + apollo-server-caching "^0.5.1" + apollo-server-env "^2.4.3" + +apollo-engine-reporting-protobuf@^0.4.4: + version "0.4.4" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.4.4.tgz#73a064f8c9f2d6605192d1673729c66ec47d9cb7" + integrity sha512-SGrIkUR7Q/VjU8YG98xcvo340C4DaNUhg/TXOtGsMlfiJDzHwVau/Bv6zifAzBafp2lj0XND6Daj5kyT/eSI/w== + dependencies: + "@apollo/protobufjs" "^1.0.3" + +apollo-engine-reporting@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/apollo-engine-reporting/-/apollo-engine-reporting-1.6.0.tgz#a5073a2e350ea4c8ce6adb5a5b536028ed165390" + integrity sha512-prA17Tp/WYBJdCd4ey1CnGX8d4Xis1n9PsFmT7x8PV/oNpxG21/x3yNw5kPBZuKAoKz8yEggYtHhkYie1ZBjPQ== + dependencies: + apollo-engine-reporting-protobuf "^0.4.4" + apollo-graphql "^0.4.0" + apollo-server-caching "^0.5.1" + apollo-server-env "^2.4.3" + apollo-server-errors "^2.3.4" + apollo-server-types "^0.2.10" async-retry "^1.2.1" - graphql-extensions "0.4.0" - lodash "^4.17.10" + graphql-extensions "^0.10.10" -apollo-env@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.2.5.tgz#162c785bccd2aea69350a7600fab4b7147fc9da5" +apollo-env@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/apollo-env/-/apollo-env-0.6.1.tgz#12cc869c4276a5f794edf5e5f243676038d4fb07" + integrity sha512-B9BgpQGR1ndeDtb4Gtor0J4CITQ+OPACZrVW6lgStnljKEe9ZB76DZ1dAd3OCeizAswW6Lo9uvfK8jhVS5nBhQ== dependencies: - core-js "^3.0.0-beta.3" + "@types/node-fetch" "2.5.4" + core-js "^3.0.1" node-fetch "^2.2.0" + sha.js "^2.4.11" + +apollo-graphql@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/apollo-graphql/-/apollo-graphql-0.4.0.tgz#dd0afe31a6241b8e2ded20b906c9ee8dfbe03497" + integrity sha512-abCHcKln1EGbzSItW087EjBI5wnluikyUqEn4VsdeWHCtdENWpHCn/MnM0+jJa1prNasxN7tCukp4nMpJYYVqg== + dependencies: + apollo-env "^0.6.1" + lodash.sortby "^4.7.0" apollo-link@^1.2.3: - version "1.2.6" - resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.6.tgz#d9b5676d79c01eb4e424b95c7171697f6ad2b8da" + version "1.2.13" + resolved "https://registry.yarnpkg.com/apollo-link/-/apollo-link-1.2.13.tgz#dff00fbf19dfcd90fddbc14b6a3f9a771acac6c4" + integrity sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw== dependencies: - apollo-utilities "^1.0.0" - zen-observable-ts "^0.8.13" + apollo-utilities "^1.3.0" + ts-invariant "^0.4.0" + tslib "^1.9.3" + zen-observable-ts "^0.8.20" -apollo-server-caching@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.2.1.tgz#7e67f8c8cac829e622b394f0fb82579cabbeadfd" +apollo-server-caching@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.4.0.tgz#e82917590d723c0adc1fa52900e79e93ad65e4d9" + integrity sha512-GTOZdbLhrSOKYNWMYgaqX5cVNSMT0bGUTZKV8/tYlyYmsB6ey7l6iId3Q7UpHS6F6OR2lstz5XaKZ+T3fDfPzQ== dependencies: lru-cache "^5.0.0" -apollo-server-core@2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.3.1.tgz#cbdc0020a0dfecf2220cf5062dbb304fdf56edf2" +apollo-server-caching@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz#5cd0536ad5473abb667cc82b59bc56b96fb35db6" + integrity sha512-L7LHZ3k9Ao5OSf2WStvQhxdsNVplRQi7kCAPfqf9Z3GBEnQ2uaL0EgO0hSmtVHfXTbk5CTRziMT1Pe87bXrFIw== + dependencies: + lru-cache "^5.0.0" + +apollo-server-core@^2.10.1: + version "2.10.1" + resolved "https://registry.yarnpkg.com/apollo-server-core/-/apollo-server-core-2.10.1.tgz#5fa4ce7992d0bf1cce616dedf1a22a41c7589c7c" + integrity sha512-BVITSJRMnj+CWFkjt7FMcaoqg/Ni9gfyVE9iu8bUc1IebBfFDcQj652Iolr7dTqyUziN2jbf0wfcybKYJLQHQQ== dependencies: - "@apollographql/apollo-tools" "^0.2.6" - "@apollographql/graphql-playground-html" "^1.6.6" + "@apollographql/apollo-tools" "^0.4.3" + "@apollographql/graphql-playground-html" "1.6.24" + "@types/graphql-upload" "^8.0.0" "@types/ws" "^6.0.0" - apollo-cache-control "0.4.0" - apollo-datasource "0.2.1" - apollo-engine-reporting "0.2.0" - apollo-server-caching "0.2.1" - apollo-server-env "2.2.0" - apollo-server-errors "2.2.0" - apollo-server-plugin-base "0.2.1" - apollo-tracing "0.4.0" - graphql-extensions "0.4.1" - graphql-subscriptions "^1.0.0" + apollo-cache-control "^0.8.11" + apollo-datasource "^0.7.0" + apollo-engine-reporting "^1.6.0" + apollo-server-caching "^0.5.1" + apollo-server-env "^2.4.3" + apollo-server-errors "^2.3.4" + apollo-server-plugin-base "^0.6.10" + apollo-server-types "^0.2.10" + apollo-tracing "^0.8.11" + fast-json-stable-stringify "^2.0.0" + graphql-extensions "^0.10.10" graphql-tag "^2.9.2" graphql-tools "^4.0.0" graphql-upload "^8.0.2" - json-stable-stringify "^1.0.1" - lodash "^4.17.10" + sha.js "^2.4.11" subscriptions-transport-ws "^0.9.11" ws "^6.0.0" -apollo-server-env@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.2.0.tgz#5eec5dbf46581f663fd6692b2e05c7e8ae6d6034" +apollo-server-env@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.0.tgz#6611556c6b627a1636eed31317d4f7ea30705872" + integrity sha512-7ispR68lv92viFeu5zsRUVGP+oxsVI3WeeBNniM22Cx619maBUwcYTIC3+Y3LpXILhLZCzA1FASZwusgSlyN9w== dependencies: node-fetch "^2.1.2" util.promisify "^1.0.0" -apollo-server-errors@2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.2.0.tgz#5b452a1d6ff76440eb0f127511dc58031a8f3cb5" +apollo-server-env@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/apollo-server-env/-/apollo-server-env-2.4.3.tgz#9bceedaae07eafb96becdfd478f8d92617d825d2" + integrity sha512-23R5Xo9OMYX0iyTu2/qT0EUb+AULCBriA9w8HDfMoChB8M+lFClqUkYtaTTHDfp6eoARLW8kDBhPOBavsvKAjA== + dependencies: + node-fetch "^2.1.2" + util.promisify "^1.0.0" -apollo-server-express@^2.3.1: +apollo-server-errors@2.3.1: version "2.3.1" - resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.3.1.tgz#0598e2fa0a0d9e6eb570c0bb6ce65c31810a9c09" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.1.tgz#033cf331463ebb99a563f8354180b41ac6714eb6" + integrity sha512-errZvnh0vUQChecT7M4A/h94dnBSRL213dNxpM5ueMypaLYgnp4hiCTWIEaooo9E4yMGd1qA6WaNbLDG2+bjcg== + +apollo-server-errors@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/apollo-server-errors/-/apollo-server-errors-2.3.4.tgz#b70ef01322f616cbcd876f3e0168a1a86b82db34" + integrity sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA== + +apollo-server-express@^2.9.7: + version "2.10.1" + resolved "https://registry.yarnpkg.com/apollo-server-express/-/apollo-server-express-2.10.1.tgz#f48b3c59ebb904d1048c80d2bc23ad8878579457" + integrity sha512-NkuWGBOCTiju/aDjfvDImm+4yzfrM0dwiRxu9fKwwh2h1oYBUKJNqjQ1mzJRi0ks6Sn1egwl/fQkTBTkWwGx7Q== dependencies: - "@apollographql/graphql-playground-html" "^1.6.6" + "@apollographql/graphql-playground-html" "1.6.24" "@types/accepts" "^1.3.5" - "@types/body-parser" "1.17.0" + "@types/body-parser" "1.17.1" "@types/cors" "^2.8.4" - "@types/express" "4.16.0" + "@types/express" "4.17.2" accepts "^1.3.5" - apollo-server-core "2.3.1" + apollo-server-core "^2.10.1" + apollo-server-types "^0.2.10" body-parser "^1.18.3" cors "^2.8.4" + express "^4.17.1" graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" + parseurl "^1.3.2" + subscriptions-transport-ws "^0.9.16" type-is "^1.6.16" -apollo-server-plugin-base@0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.2.1.tgz#d08c9576f7f11ab6e212f352d482faaa4059a31e" - -apollo-tracing@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.4.0.tgz#4b939063f4292422ac5a3564b76d1d88dec0a916" +apollo-server-plugin-base@^0.6.10: + version "0.6.10" + resolved "https://registry.yarnpkg.com/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.10.tgz#33d3e2bb82fca22a00b6648a2f1c6b2cc032a8a0" + integrity sha512-/xT7UT/tbCDIoTQ4lcEQsJ0ACh7h7QG0BDmeSlDXjwDuENRI50bQ2QoluCMPitZXGe+FCQfLhvzFgzbsZGT0IA== dependencies: - apollo-server-env "2.2.0" - graphql-extensions "0.4.0" + apollo-server-types "^0.2.10" -apollo-utilities@^1.0.0, apollo-utilities@^1.0.1: - version "1.0.27" - resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.0.27.tgz#77c550f9086552376eca3a48e234a1466b5b057e" +apollo-server-types@^0.2.10: + version "0.2.10" + resolved "https://registry.yarnpkg.com/apollo-server-types/-/apollo-server-types-0.2.10.tgz#017ee0c812e70b0846826834eb2c9eda036c1c7a" + integrity sha512-ke9ViPEWfW+2XLe66CaKGVZdS7duSLbamSKSprmmeMBd8s6tmjf0FumUVxV7X4quxPZi0OPo8x0LoLU7GWsmaA== dependencies: - fast-json-stable-stringify "^2.0.0" + apollo-engine-reporting-protobuf "^0.4.4" + apollo-server-caching "^0.5.1" + apollo-server-env "^2.4.3" -app-root-path@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" - -append-transform@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" +apollo-tracing@^0.8.11: + version "0.8.11" + resolved "https://registry.yarnpkg.com/apollo-tracing/-/apollo-tracing-0.8.11.tgz#55822aac7381da77c703b52d35c4dab9393ec33c" + integrity sha512-Z0wDZ5QOBmpGoajB74ZKGTM7GzG6rqZRzAph4kxud6axcyNqUDKiKZ3Eere+NSLwvvt8M3qnPW4UJSUy/wwOXg== dependencies: - default-require-extensions "^1.0.0" - -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + apollo-server-env "^2.4.3" + graphql-extensions "^0.10.10" -archy@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" - integrity sha1-+cjBN1fMHde8N5rHeyxipcKGjEA= - -are-we-there-yet@~1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" +apollo-utilities@^1.0.1, apollo-utilities@^1.3.0: + version "1.3.3" + resolved "https://registry.yarnpkg.com/apollo-utilities/-/apollo-utilities-1.3.3.tgz#f1854715a7be80cd810bc3ac95df085815c0787c" + integrity sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw== dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" + "@wry/equality" "^0.1.2" + fast-json-stable-stringify "^2.0.0" + ts-invariant "^0.4.0" + tslib "^1.10.0" + +app-root-path@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" + integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== arg@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0" + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== dependencies: sprintf-js "~1.0.2" arr-diff@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= dependencies: arr-flatten "^1.0.1" arr-diff@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= arr-flatten@^1.0.1, arr-flatten@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== arr-union@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= array-equal@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93" - -array-filter@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" - -array-filter@~0.0.0: - version "0.0.1" - resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec" + integrity sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM= array-find-index@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" + integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= array-from@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" + integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= array-ify@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/array-ify/-/array-ify-1.0.0.tgz#9e528762b4a9066ad163a6962a364418e9626ece" integrity sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4= -array-map@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662" - -array-reduce@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b" - -array-union@^1.0.1, array-union@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" - dependencies: - array-uniq "^1.0.1" - -array-uniq@^1.0.1, array-uniq@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== array-unique@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= array-unique@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +arraybuffer.slice@~0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz#3bbc4275dd584cc1b10809b89d4e8b63a69e7675" + integrity sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog== -arrify@^1.0.0, arrify@^1.0.1: +arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" + integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - -ascli@~1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ascli/-/ascli-1.0.1.tgz#bcfa5974a62f18e81cabaeb49732ab4a88f906bc" - dependencies: - colour "~0.7.1" - optjs "~3.2.2" +arrify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa" + integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug== asn1@~0.2.3: version "0.2.4" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== dependencies: safer-buffer "~2.1.0" assert-plus@1.0.0, assert-plus@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= assign-symbols@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367" - -ast-types@0.x.x: - version "0.13.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.13.2.tgz#df39b677a911a83f3a049644fb74fdded23cea48" - integrity sha512-uWMHxJxtfj/1oZClOxDEV1sQ1HCDkA4MG8Gr69KKeBjEVH0R84WlejZ0y2DcwyBlpAEMltmVYkVgqfLFb2oyiA== + integrity sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c= astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== async-each@^1.0.0, async-each@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + version "1.0.3" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.3.tgz#b727dbf87d7651602f06f4d4ac387f47d91b0cbf" + integrity sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ== async-limiter@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8" + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd" + integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ== -async-retry@1.2.3, async-retry@^1.2.1: - version "1.2.3" - resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.2.3.tgz#a6521f338358d322b1a0012b79030c6f411d1ce0" +async-retry@1.3.1, async-retry@^1.2.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/async-retry/-/async-retry-1.3.1.tgz#139f31f8ddce50c0870b0ba558a6079684aaed55" + integrity sha512-aiieFW/7h3hY0Bq5d+ktDBejxuwR78vRu9hDUdR8rNhSaQ29VzPL4AoIRG7D/c7tdenwOcKvgPM6tIxB3cB6HA== dependencies: retry "0.12.0" -async@2.6.1, async@^2.1.4, async@^2.3.0, async@^2.4.0, async@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610" - dependencies: - lodash "^4.17.10" - -async@^1.4.0: - version "1.5.2" - resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" - integrity sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo= - -async@^2.0.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/async/-/async-2.6.2.tgz#18330ea7e6e313887f5d2f2a904bac6fe4dd5381" +async@^2.6.2: + version "2.6.3" + resolved "https://registry.yarnpkg.com/async/-/async-2.6.3.tgz#d72625e2344a3656e3a3ad4fa749fa83299d82ff" + integrity sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg== dependencies: - lodash "^4.17.11" + lodash "^4.17.14" asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= atob-lite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= -atob@^2.1.1: +atob@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" + integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg== aws-sdk@^2.151.0: - version "2.382.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.382.0.tgz#9212bc7aced9c051973578d06ea1978a57ccbd5f" + version "2.630.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.630.0.tgz#108ccd50b585226db8d1798def73f7b873a2efa6" + integrity sha512-7BDPUIqmMZfZf+KN2Z3RpGDYGkEucQORLM2EqXuE91ETW5ySvoNd771+EaE3OS+FUx3JejfcVk8Rr2ZFU38RjA== dependencies: buffer "4.9.1" events "1.1.1" - ieee754 "1.1.8" + ieee754 "1.1.13" jmespath "0.15.0" querystring "0.2.0" sax "1.2.1" url "0.10.3" - uuid "3.1.0" + uuid "3.3.2" xml2js "0.4.19" aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f" - -axios@^0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102" - dependencies: - follow-redirects "^1.3.0" - is-buffer "^1.1.5" + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== -babel-code-frame@^6.22.0, babel-code-frame@^6.26.0: +babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + integrity sha1-Y/1D99weO7fONZR9uP42mj9Yx0s= dependencies: chalk "^1.1.3" esutils "^2.0.2" js-tokens "^3.0.2" -babel-core@^6.0.0, babel-core@^6.26.0, babel-core@^6.26.3: +babel-core@^6.26.0, babel-core@^6.26.3: version "6.26.3" resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + integrity sha512-6jyFLuDmeidKmUEb3NM+/yawG0M2bDZ9Z1qbZP59cyHLz8kYGKYwpJP0UwUKKUiTRNvxfLesJnTedqczP7cTDA== dependencies: babel-code-frame "^6.26.0" babel-generator "^6.26.0" @@ -1176,6 +1877,7 @@ babel-core@^6.0.0, babel-core@^6.26.0, babel-core@^6.26.3: babel-generator@^6.18.0, babel-generator@^6.26.0: version "6.26.1" resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + integrity sha512-HyfwY6ApZj7BYTcJURpM5tznulaBvyio7/0d4zFOeMPUmfxkCjHocCuoLa2SAGzBI8AREcH3eP3758F672DppA== dependencies: babel-messages "^6.23.0" babel-runtime "^6.26.0" @@ -1189,47 +1891,72 @@ babel-generator@^6.18.0, babel-generator@^6.26.0: babel-helpers@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + integrity sha1-NHHenK7DiOXIUOWX5Yom3fN2ArI= dependencies: babel-runtime "^6.22.0" babel-template "^6.24.1" -babel-jest@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-21.2.0.tgz#2ce059519a9374a2c46f2455b6fbef5ad75d863e" +babel-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-24.9.0.tgz#3fc327cb8467b89d14d7bc70e315104a783ccd54" + integrity sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw== dependencies: - babel-plugin-istanbul "^4.0.0" - babel-preset-jest "^21.2.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/babel__core" "^7.1.0" + babel-plugin-istanbul "^5.1.0" + babel-preset-jest "^24.9.0" + chalk "^2.4.2" + slash "^2.0.0" babel-messages@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + integrity sha1-8830cDhYA1sqKVHG7F7fbGLyYw4= dependencies: babel-runtime "^6.22.0" -babel-plugin-istanbul@^4.0.0, babel-plugin-istanbul@^4.1.6: +babel-plugin-istanbul@^4.1.6: version "4.1.6" resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-4.1.6.tgz#36c59b2192efce81c5b378321b74175add1c9a45" + integrity sha512-PWP9FQ1AhZhS01T/4qLSKoHGY/xvkZdVBGlKM/HuxxS3+sC66HhTNR7+MpbO/so/cz/wY94MeSWJuP1hXIPfwQ== dependencies: babel-plugin-syntax-object-rest-spread "^6.13.0" find-up "^2.1.0" istanbul-lib-instrument "^1.10.1" test-exclude "^4.2.1" -babel-plugin-jest-hoist@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-21.2.0.tgz#2cef637259bd4b628a6cace039de5fcd14dbb006" +babel-plugin-istanbul@^5.1.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz#df4ade83d897a92df069c4d9a25cf2671293c854" + integrity sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + find-up "^3.0.0" + istanbul-lib-instrument "^3.3.0" + test-exclude "^5.2.3" babel-plugin-jest-hoist@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-22.4.4.tgz#b9851906eab34c7bf6f8c895a2b08bea1a844c0b" + integrity sha512-DUvGfYaAIlkdnygVIEl0O4Av69NtuQWcrjMOv6DODPuhuGLDnbsARz3AwiiI/EkIMMlxQDUcrZ9yoyJvTNjcVQ== + +babel-plugin-jest-hoist@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz#4f837091eb407e01447c8843cbec546d0002d756" + integrity sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw== + dependencies: + "@types/babel__traverse" "^7.0.6" babel-plugin-syntax-object-rest-spread@^6.13.0: version "6.13.0" resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5" + integrity sha1-/WU28rzhODb/o6VFjEkDpZe7O/U= babel-plugin-transform-es2015-modules-commonjs@^6.26.2: version "6.26.2" resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + integrity sha512-CV9ROOHEdrjcwhIaJNBGMBCodN+1cfkwtM1SbUHmvyy35KGT7fohbpOxkE2uLz1o6odKK2Ck/tz47z+VqQfi9Q== dependencies: babel-plugin-transform-strict-mode "^6.24.1" babel-runtime "^6.26.0" @@ -1239,27 +1966,31 @@ babel-plugin-transform-es2015-modules-commonjs@^6.26.2: babel-plugin-transform-strict-mode@^6.24.1: version "6.24.1" resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + integrity sha1-1fr3qleKZbvlkc9e2uBKDGcCB1g= dependencies: babel-runtime "^6.22.0" babel-types "^6.24.1" -babel-preset-jest@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-21.2.0.tgz#ff9d2bce08abd98e8a36d9a8a5189b9173b85638" - dependencies: - babel-plugin-jest-hoist "^21.2.0" - babel-plugin-syntax-object-rest-spread "^6.13.0" - babel-preset-jest@^22.4.3: version "22.4.4" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-22.4.4.tgz#ec9fbd8bcd7dfd24b8b5320e0e688013235b7c39" + integrity sha512-+dxMtOFwnSYWfum0NaEc0O03oSdwBsjx4tMSChRDPGwu/4wSY6Q6ANW3wkjKpJzzguaovRs/DODcT4hbSN8yiA== dependencies: babel-plugin-jest-hoist "^22.4.4" babel-plugin-syntax-object-rest-spread "^6.13.0" +babel-preset-jest@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz#192b521e2217fb1d1f67cf73f70c336650ad3cdc" + integrity sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg== + dependencies: + "@babel/plugin-syntax-object-rest-spread" "^7.0.0" + babel-plugin-jest-hoist "^24.9.0" + babel-register@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + integrity sha1-btAhFz4vy0htestFxgCahW9kcHE= dependencies: babel-core "^6.26.0" babel-runtime "^6.26.0" @@ -1272,6 +2003,7 @@ babel-register@^6.26.0: babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + integrity sha1-llxwWGaOgrVde/4E/yM3vItWR/4= dependencies: core-js "^2.4.0" regenerator-runtime "^0.11.0" @@ -1279,6 +2011,7 @@ babel-runtime@^6.22.0, babel-runtime@^6.26.0, babel-runtime@^6.9.2: babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + integrity sha1-3gPi0WOWsGn0bdn/+FIfsaDjXgI= dependencies: babel-runtime "^6.26.0" babel-traverse "^6.26.0" @@ -1289,6 +2022,7 @@ babel-template@^6.16.0, babel-template@^6.24.1, babel-template@^6.26.0: babel-traverse@^6.18.0, babel-traverse@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + integrity sha1-RqnL1+3MYsjlwGTi0tjQ9ANXZu4= dependencies: babel-code-frame "^6.26.0" babel-messages "^6.23.0" @@ -1303,6 +2037,7 @@ babel-traverse@^6.18.0, babel-traverse@^6.26.0: babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc= dependencies: babel-runtime "^6.26.0" esutils "^2.0.2" @@ -1312,22 +2047,37 @@ babel-types@^6.18.0, babel-types@^6.24.1, babel-types@^6.26.0: babylon@^6.18.0: version "6.18.0" resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + integrity sha512-q/UEjfGJ2Cm3oKV71DJz9d25TPnq5rhBVL2Q4fA5wcC3jcrdn7+SssEybFIxwAvvP+YCsCYNKughoF33GxgycQ== -backo2@^1.0.2: +backo2@1.0.2, backo2@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/backo2/-/backo2-1.0.2.tgz#31ab1ac8b129363463e35b3ebb69f4dfcfba7947" + integrity sha1-MasayLEpNjRj41s+u2n038+6eUc= balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +base64-arraybuffer@0.1.5: + version "0.1.5" + resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8" + integrity sha1-c5JncZI7Whl0etZmqlzUv5xunOg= base64-js@^1.0.2, base64-js@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3" + version "1.3.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" + integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== + +base64id@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/base64id/-/base64id-2.0.0.tgz#2770ac6bc47d312af97a8bf9a634342e0cd25cb6" + integrity sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog== base@^0.11.1: version "0.11.2" resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f" + integrity sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg== dependencies: cache-base "^1.0.1" class-utils "^0.3.5" @@ -1340,6 +2090,7 @@ base@^0.11.1: bcrypt-pbkdf@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= dependencies: tweetnacl "^0.14.3" @@ -1348,58 +2099,139 @@ bcryptjs@^2.4.3: resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb" integrity sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms= -before-after-hook@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d" - integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg== +before-after-hook@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" + integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== + +better-assert@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522" + integrity sha1-QIZrnhueC1W0gYlDEeaPr/rrxSI= + dependencies: + callsite "1.0.0" + +big-integer@^1.6.17: + version "1.6.48" + resolved "https://registry.yarnpkg.com/big-integer/-/big-integer-1.6.48.tgz#8fd88bd1632cba4a1c8c3e3d7159f08bb95b4b9e" + integrity sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w== bignumber.js@^7.0.0: version "7.2.1" resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-7.2.1.tgz#80c048759d826800807c4bfd521e50edbba57a5f" + integrity sha512-S4XzBk5sMB+Rcb/LNcpzXr57VRTxgAvaAEDAl1AwRx27j00hT84O6OkteE7u8UB3NuaaygCRrEpqox4uDOrbdQ== binary-extensions@^1.0.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14" + version "1.13.1" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" + integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== + +binary@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/binary/-/binary-0.3.0.tgz#9f60553bc5ce8c3386f3b553cff47462adecaa79" + integrity sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk= + dependencies: + buffers "~0.1.1" + chainsaw "~0.1.0" + +bindings@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" + integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== + dependencies: + file-uri-to-path "1.0.0" + +bitsyntax@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" + integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== + dependencies: + buffer-more-ints "~1.0.0" + debug "~2.6.9" + safe-buffer "~5.1.2" + +bl@^1.0.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c" + integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +bl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" + integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA== + dependencies: + readable-stream "^2.3.5" + safe-buffer "^5.1.1" + +blob@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" + integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== bluebird@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + integrity sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA== + +bluebird@^3.3.4, bluebird@^3.5.1, bluebird@^3.5.2: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bluebird@^3.1.5, bluebird@^3.3.4: - version "3.5.3" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.3.tgz#7d01c6f9616c9a51ab0f8c549a79dfe6ec33efa7" +bluebird@~3.4.1: + version "3.4.7" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3" + integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM= -body-parser@1.18.3, body-parser@^1.17.1, body-parser@^1.18.3: - version "1.18.3" - resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" +body-parser@1.19.0, body-parser@^1.18.3: + version "1.19.0" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" + integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== dependencies: - bytes "3.0.0" + bytes "3.1.0" content-type "~1.0.4" debug "2.6.9" depd "~1.1.2" - http-errors "~1.6.3" - iconv-lite "0.4.23" + http-errors "1.7.2" + iconv-lite "0.4.24" on-finished "~2.3.0" - qs "6.5.2" - raw-body "2.3.3" - type-is "~1.6.16" + qs "6.7.0" + raw-body "2.4.0" + type-is "~1.6.17" -boxen@^1.2.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b" - integrity sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw== +boom@7.x.x: + version "7.3.0" + resolved "https://registry.yarnpkg.com/boom/-/boom-7.3.0.tgz#733a6d956d33b0b1999da3fe6c12996950d017b9" + integrity sha512-Swpoyi2t5+GhOEGw8rEsKvTxFLIDiiKoUc2gsoV6Lyr43LHBIzch3k2MvYUs8RTROrIkVJ3Al0TkaOGjnb+B6A== dependencies: - ansi-align "^2.0.0" - camelcase "^4.0.0" - chalk "^2.0.1" - cli-boxes "^1.0.0" - string-width "^2.0.0" - term-size "^1.2.0" - widest-line "^2.0.0" + hoek "6.x.x" + +bowser@2.9.0: + version "2.9.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.9.0.tgz#3bed854233b419b9a7422d9ee3e85504373821c9" + integrity sha512-2ld76tuLBNFekRgmJfT2+3j5MIrP6bFict8WAIT3beq+srz1gcKNAdNKMqHqauQt63NmAa88HfP1/Ypa9Er3HA== + +boxen@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/boxen/-/boxen-4.2.0.tgz#e411b62357d6d6d36587c8ac3d5d974daa070e64" + integrity sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ== + dependencies: + ansi-align "^3.0.0" + camelcase "^5.3.1" + chalk "^3.0.0" + cli-boxes "^2.2.0" + string-width "^4.1.0" + term-size "^2.1.0" + type-fest "^0.8.1" + widest-line "^3.1.0" brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -1407,6 +2239,7 @@ brace-expansion@^1.1.7: braces@^1.8.2: version "1.8.5" resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + integrity sha1-uneWLhLf+WnWt2cR6RS3N4V79qc= dependencies: expand-range "^1.8.1" preserve "^0.2.0" @@ -1415,6 +2248,7 @@ braces@^1.8.2: braces@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729" + integrity sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w== dependencies: arr-flatten "^1.1.0" array-unique "^0.3.2" @@ -1427,73 +2261,118 @@ braces@^2.3.1: split-string "^3.0.2" to-regex "^3.0.1" +braces@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + browser-process-hrtime@^0.1.2: version "0.1.3" resolved "https://registry.yarnpkg.com/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz#616f00faef1df7ec1b5bf9cfe2bdc3170f26c7b4" + integrity sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw== -browser-resolve@^1.11.2: +browser-resolve@^1.11.2, browser-resolve@^1.11.3: version "1.11.3" resolved "https://registry.yarnpkg.com/browser-resolve/-/browser-resolve-1.11.3.tgz#9b7cbb3d0f510e4cb86bdbd796124d28b5890af6" + integrity sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ== dependencies: resolve "1.1.7" -bser@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.0.0.tgz#9ac78d3ed5d915804fd87acb158bc797147a1719" +bser@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" + integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== dependencies: node-int64 "^0.4.0" -bson@^1.1.0, bson@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.0.tgz#bee57d1fb6a87713471af4e32bcae36de814b5b0" +bson@^1.1.1, bson@~1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" + integrity sha512-TdiJxMVnodVS7r0BdL42y/pqC9cL2iKynVwA0Ho3qbsQYr428veL3l7BQyuqiw+Q5SqqoT0m4srSY/BlZ9AxXg== btoa-lite@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buffer-alloc-unsafe@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" + integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg== + +buffer-alloc@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec" + integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow== + dependencies: + buffer-alloc-unsafe "^1.1.0" + buffer-fill "^1.0.0" + +buffer-crc32@~0.2.3: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + buffer-equal-constant-time@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + +buffer-fill@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c" + integrity sha1-+PeLdniYiO858gXNY39o5wISKyw= buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +buffer-indexof-polyfill@~1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz#a9fb806ce8145d5428510ce72f278bb363a638bf" + integrity sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8= + +buffer-more-ints@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" + integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== buffer@4.9.1: version "4.9.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.1.tgz#6d1bb601b07a4efced97094132093027c95bc298" + integrity sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg= dependencies: base64-js "^1.0.2" ieee754 "^1.1.4" isarray "^1.0.0" -builtin-modules@^1.0.0, builtin-modules@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" - -bun@^0.0.12: - version "0.0.12" - resolved "https://registry.yarnpkg.com/bun/-/bun-0.0.12.tgz#d54fae69f895557f275423bc14b404030b20a5fc" +buffer@^5.2.1: + version "5.4.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" + integrity sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A== dependencies: - readable-stream "~1.0.32" + base64-js "^1.0.2" + ieee754 "^1.1.4" -busboy@^0.2.14: - version "0.2.14" - resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.2.14.tgz#6c2a622efcf47c57bbbe1e2a9c37ad36c7925453" - dependencies: - dicer "0.2.5" - readable-stream "1.1.x" +buffers@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/buffers/-/buffers-0.1.1.tgz#b24579c3bed4d6d396aeee6d9a8ae7f5482ab7bb" + integrity sha1-skV5w77U1tOWru5tmorn9Ugqt7s= -bytebuffer@~5: - version "5.0.1" - resolved "https://registry.yarnpkg.com/bytebuffer/-/bytebuffer-5.0.1.tgz#582eea4b1a873b6d020a48d58df85f0bba6cfddd" - dependencies: - long "~3" +builtin-modules@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f" + integrity sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8= -bytes@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" +busboy@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b" + integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw== + dependencies: + dicer "0.3.0" bytes@3.1.0: version "3.1.0" @@ -1503,6 +2382,7 @@ bytes@3.1.0: cache-base@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2" + integrity sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ== dependencies: collection-visit "^1.0.0" component-emitter "^1.2.1" @@ -1527,14 +2407,6 @@ cacheable-request@^6.0.0: normalize-url "^4.1.0" responselike "^1.0.2" -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - -call-signature@0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/call-signature/-/call-signature-0.0.2.tgz#a84abc825a55ef4cb2b028bd74e205a65b9a4996" - caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -1549,13 +2421,25 @@ caller-path@^2.0.0: dependencies: caller-callsite "^2.0.0" +callsite@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/callsite/-/callsite-1.0.0.tgz#280398e5d664bd74038b6f0905153e6e8af1bc20" + integrity sha1-KAOY5dZkvXQDi28JBRU+borxvCA= + callsites@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-2.0.0.tgz#06eb84f00eea413da86affefacbffb36093b3c50" + integrity sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA= + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== camelcase-keys@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" + integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= dependencies: camelcase "^2.0.0" map-obj "^1.0.0" @@ -1569,60 +2453,76 @@ camelcase-keys@^4.0.0: map-obj "^2.0.0" quick-lru "^1.0.0" -camelcase@^2.0.0, camelcase@^2.0.1: +camelcase-keys@^6.2.2: + version "6.2.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" + integrity sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg== + dependencies: + camelcase "^5.3.1" + map-obj "^4.0.0" + quick-lru "^4.0.1" + +camelcase@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" + integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^4.0.0, camelcase@^4.1.0: +camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" + integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= -camelcase@^5.0.0: +camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -capture-exit@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-1.2.0.tgz#1c5fcc489fd0ab00d4f1ac7ae1072e3173fbab6f" - dependencies: - rsvp "^3.3.3" - -capture-stack-trace@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz#a6c0bbe1f38f3aa0b92238ecb6ff42c344d4135d" +camelize@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/camelize/-/camelize-1.0.0.tgz#164a5483e630fa4321e5af07020e531831b2609b" + integrity sha1-FkpUg+Yw+kMh5a8HAg5TGDGyYJs= -cardinal@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cardinal/-/cardinal-2.1.1.tgz#7cc1055d822d212954d07b085dea251cc7bc5505" +capture-exit@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4" + integrity sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g== dependencies: - ansicolors "~0.3.2" - redeyed "~2.1.0" + rsvp "^4.8.4" caseless@~0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= -cfb@^1.0.7: - version "1.1.0" - resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.1.0.tgz#44fb1b30eee014fa5633a0ed5f26c87fd765799a" +cfb@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/cfb/-/cfb-1.1.3.tgz#05de6816259c8e8bc32713aba905608ee385df66" + integrity sha512-joXBW0nMuwV9no7UTMiyVJnQL6XIU3ThXVjFUDHgl9MpILPOomyfaGqC290VELZ48bbQKZXnQ81UT5HouTxHsw== dependencies: adler-32 "~1.2.0" commander "^2.16.0" crc-32 "~1.2.0" printj "~1.1.2" -chalk@2.4.2, chalk@^2.4.1, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" +chainsaw@~0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/chainsaw/-/chainsaw-0.1.0.tgz#5eab50b28afe58074d0d58291388828b5e5fbc98" + integrity sha1-XqtQsor+WAdNDVgpE4iCi15fvJg= dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" + traverse ">=0.3.0 <0.4" + +chalk@3.0.0, chalk@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" + integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + integrity sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg= dependencies: ansi-styles "^2.2.1" escape-string-regexp "^1.0.2" @@ -1630,9 +2530,10 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== dependencies: ansi-styles "^3.2.1" escape-string-regexp "^1.0.5" @@ -1648,14 +2549,17 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -child_process@1.0.2: +charm@1.0.2: version "1.0.2" - resolved "https://registry.yarnpkg.com/child_process/-/child_process-1.0.2.tgz#b1f7e7fc73d25e7fd1d455adc94e143830182b5a" - integrity sha1-sffn/HPSXn/R1FWtyU4UODAYK1o= + resolved "https://registry.yarnpkg.com/charm/-/charm-1.0.2.tgz#8add367153a6d9a581331052c4090991da995e35" + integrity sha1-it02cVOm2aWBMxBSxAkJkdqZXjU= + dependencies: + inherits "^2.0.1" chokidar@^1.6.0: version "1.7.0" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + integrity sha1-eY5ol3gVHIB2tLNg5e3SjNortGg= dependencies: anymatch "^1.3.0" async-each "^1.0.0" @@ -1668,36 +2572,35 @@ chokidar@^1.6.0: optionalDependencies: fsevents "^1.0.0" -chownr@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.1.tgz#54726b8b8fff4df053c42187e801fb4412df1494" +ci-info@2.0.0, ci-info@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" + integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== ci-info@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497" - -ci-info@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46" - integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== + integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== class-utils@^0.3.5: version "0.3.6" resolved "https://registry.yarnpkg.com/class-utils/-/class-utils-0.3.6.tgz#f93369ae8b9a7ce02fd41faad0ca83033190c463" + integrity sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg== dependencies: arr-union "^3.1.0" define-property "^0.2.5" isobject "^3.0.0" static-extend "^0.1.1" -cli-boxes@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-1.0.0.tgz#4fa917c3e59c94a004cd61f8ee509da651687143" - integrity sha1-T6kXw+WclKAEzWH47lCdplFocUM= +cli-boxes@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-boxes/-/cli-boxes-2.2.0.tgz#538ecae8f9c6ca508e3c3c95b453fe93cb4c168d" + integrity sha512-gpaBrMAizVEANOpfZp/EEUixTXDyGt7DFzdK5hU+UbWt/J0lB0w20ncZj59Z9a93xHb9u12zF5BS6i9RKbtg4w== cli-cursor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= dependencies: restore-cursor "^1.0.1" @@ -1708,65 +2611,53 @@ cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-spinners@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= -cli-spinners@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.1.0.tgz#22c34b4d51f573240885b201efda4e4ec9fff3c7" - integrity sha512-8B00fJOEh1HPrx4fo5eW16XmE1PcL1tGpGrxy63CXGP9nHdPBN63X75hA1zhvQuhVztJWLqV58Roj2qlNM7cAA== - -cli-table@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23" - dependencies: - colors "1.0.3" +cli-spinners@^2.0.0, cli-spinners@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77" + integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== cli-truncate@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + integrity sha1-nxXPuwcFAFNpIWxiasfQWrkN1XQ= dependencies: slice-ansi "0.0.4" string-width "^1.0.1" -cli-usage@^0.1.1: - version "0.1.8" - resolved "https://registry.yarnpkg.com/cli-usage/-/cli-usage-0.1.8.tgz#16479361f3a895a81062d02d9634827c713aaaf8" - dependencies: - marked "^0.5.0" - marked-terminal "^3.0.0" - cli-width@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= -cliui@^3.0.3, cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - cliui@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" + integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== dependencies: string-width "^2.1.1" strip-ansi "^4.0.0" wrap-ansi "^2.0.0" -clone-deep@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-0.3.0.tgz#348c61ae9cdbe0edfe053d91ff4cc521d790ede8" - integrity sha1-NIxhrpzb4O3+BT2R/0zFIdeQ7eg= +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== dependencies: - for-own "^1.0.0" - is-plain-object "^2.0.1" - kind-of "^3.2.2" - shallow-clone "^0.1.2" + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" clone-response@^1.0.2: version "1.0.2" @@ -1781,20 +2672,24 @@ clone@^1.0.2: integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= cluster-key-slot@^1.0.6: - version "1.0.12" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.0.12.tgz#d5deff2a520717bc98313979b687309b2d368e29" + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" + integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= collection-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collection-visit/-/collection-visit-1.0.0.tgz#4bc0373c164bc3291b4d368c829cf1a80a59dca0" + integrity sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA= dependencies: map-visit "^1.0.0" object-visit "^1.0.0" @@ -1802,289 +2697,327 @@ collection-visit@^1.0.0: color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== dependencies: color-name "1.1.3" +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + color-name@1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -colors@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b" +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colour@~0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/colour/-/colour-0.7.1.tgz#9cb169917ec5d12c0736d3e8685746df1cadf778" +colors@1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78" + integrity sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA== -combined-stream@^1.0.6, combined-stream@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828" +combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== dependencies: delayed-stream "~1.0.0" -commander@^2.12.1, commander@^2.16.0, commander@^2.9.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" +commander@^2.12.1, commander@^2.16.0, commander@^2.9.0, commander@~2.20.3: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" + integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@~2.17.1: - version "2.17.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" +commander@~2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4" + integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ= + dependencies: + graceful-readlink ">= 1.0.0" -compare-func@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" - integrity sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg= +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= + +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== dependencies: array-ify "^1.0.0" - dot-prop "^3.0.0" + dot-prop "^5.1.0" + +component-bind@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/component-bind/-/component-bind-1.0.0.tgz#00c608ab7dcd93897c0009651b1d3a8e1e73bbd1" + integrity sha1-AMYIq33Nk4l8AAllGx06jh5zu9E= -component-emitter@^1.2.1: +component-emitter@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" + integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY= + +component-emitter@^1.2.1, component-emitter@^1.3.0, component-emitter@~1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.3.0.tgz#16e4070fba8ae29b679f2215853ee181ab2eabc0" + integrity sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg== + +component-inherit@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/component-inherit/-/component-inherit-0.0.3.tgz#645fc4adf58b72b649d5cae65135619db26ff143" + integrity sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM= compressible@^2.0.12: - version "2.0.16" - resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.16.tgz#a49bf9858f3821b64ce1be0296afc7380466a77f" + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== dependencies: - mime-db ">= 1.38.0 < 2" + mime-db ">= 1.43.0 < 2" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - -concat-stream@^1.6.0: - version "1.6.2" - resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34" - dependencies: - buffer-from "^1.0.0" - inherits "^2.0.3" - readable-stream "^2.2.2" - typedarray "^0.0.6" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= concat-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== dependencies: buffer-from "^1.0.0" inherits "^2.0.3" readable-stream "^3.0.2" typedarray "^0.0.6" -configstore@^3.0.0, configstore@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-3.1.2.tgz#c6f25defaeef26df12dd33414b001fe81a543f8f" - integrity sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw== - dependencies: - dot-prop "^4.1.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - unique-string "^1.0.0" - write-file-atomic "^2.0.0" - xdg-basedir "^3.0.0" - -configstore@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/configstore/-/configstore-4.0.0.tgz#5933311e95d3687efb592c528b922d9262d227e7" +configstore@^5.0.0, configstore@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/configstore/-/configstore-5.0.1.tgz#d365021b5df4b98cdd187d6a3b0e3f6a7cc5ed96" + integrity sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA== dependencies: - dot-prop "^4.1.0" + dot-prop "^5.2.0" graceful-fs "^4.1.2" - make-dir "^1.0.0" - unique-string "^1.0.0" - write-file-atomic "^2.0.0" - xdg-basedir "^3.0.0" + make-dir "^3.0.0" + unique-string "^2.0.0" + write-file-atomic "^3.0.0" + xdg-basedir "^4.0.0" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" +configurable@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/configurable/-/configurable-0.0.1.tgz#47d75b727b51b4eb84c1dadafe3f8240313833b1" + integrity sha1-R9dbcntRtOuEwdra/j+CQDE4M7E= -content-disposition@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" +content-disposition@0.5.3: + version "0.5.3" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" + integrity sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g== + dependencies: + safe-buffer "5.1.2" -content-type-parser@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/content-type-parser/-/content-type-parser-1.0.2.tgz#caabe80623e63638b2502fd4c7f12ff4ce2352e7" +content-security-policy-builder@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz#0a2364d769a3d7014eec79ff7699804deb8cfcbb" + integrity sha512-/MtLWhJVvJNkA9dVLAp6fg9LxD2gfI6R2Fi1hPmfjYXSahJJzcfvoeDOxSyp4NvxMuwWv3WMssE9o31DoULHrQ== content-type@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -conventional-changelog-angular@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.3.tgz#299fdd43df5a1f095283ac16aeedfb0a682ecab0" - integrity sha512-YD1xzH7r9yXQte/HF9JBuEDfvjxxwDGGwZU1+ndanbY0oFgA+Po1T9JDSpPLdP0pZT6MhCAsdvFKC4TJ4MTJTA== +conventional-changelog-angular@^5.0.11: + version "5.0.11" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz#99a3ca16e4a5305e0c2c2fae3ef74fd7631fc3fb" + integrity sha512-nSLypht/1yEflhuTogC03i7DX7sOrXGsRn14g131Potqi6cbGbGEE9PSDEHKldabB6N76HiSyw9Ph+kLmC04Qw== dependencies: - compare-func "^1.3.1" + compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-atom@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.1.tgz#dc88ce650ffa9ceace805cbe70f88bfd0cb2c13a" - integrity sha512-9BniJa4gLwL20Sm7HWSNXd0gd9c5qo49gCi8nylLFpqAHhkFTj7NQfROq3f1VpffRtzfTQp4VKU5nxbe2v+eZQ== +conventional-changelog-atom@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.7.tgz#221575253a04f77a2fd273eb2bf29a138f710abf" + integrity sha512-7dOREZwzB+tCEMjRTDfen0OHwd7vPUdmU0llTy1eloZgtOP4iSLVzYIQqfmdRZEty+3w5Jz+AbhfTJKoKw1JeQ== dependencies: q "^1.5.1" -conventional-changelog-codemirror@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.1.tgz#acc046bc0971460939a0cc2d390e5eafc5eb30da" - integrity sha512-23kT5IZWa+oNoUaDUzVXMYn60MCdOygTA2I+UjnOMiYVhZgmVwNd6ri/yDlmQGXHqbKhNR5NoXdBzSOSGxsgIQ== +conventional-changelog-codemirror@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.7.tgz#d6b6a8ce2707710c5a036e305037547fb9e15bfb" + integrity sha512-Oralk1kiagn3Gb5cR5BffenWjVu59t/viE6UMD/mQa1hISMPkMYhJIqX+CMeA1zXgVBO+YHQhhokEj99GP5xcg== dependencies: q "^1.5.1" -conventional-changelog-conventionalcommits@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-3.0.2.tgz#3a380a14ecd6f5056da6d460e30dd6c0c9f1aebe" - integrity sha512-w1+fQSDnm/7+sPKIYC5nfRVYDszt+6HdWizrigSqWFVIiiBVzkHGeqDLMSHc+Qq9qssHVAxAak5206epZyK87A== +conventional-changelog-conventionalcommits@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.4.0.tgz#8d96687141c9bbd725a89b95c04966d364194cd4" + integrity sha512-ybvx76jTh08tpaYrYn/yd0uJNLt5yMrb1BphDe4WBredMlvPisvMghfpnJb6RmRNcqXeuhR6LfGZGewbkRm9yA== dependencies: - compare-func "^1.3.1" + compare-func "^2.0.0" + lodash "^4.17.15" q "^1.5.1" -conventional-changelog-core@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-3.2.2.tgz#de41e6b4a71011a18bcee58e744f6f8f0e7c29c0" - integrity sha512-cssjAKajxaOX5LNAJLB+UOcoWjAIBvXtDMedv/58G+YEmAXMNfC16mmPl0JDOuVJVfIqM0nqQiZ8UCm8IXbE0g== +conventional-changelog-core@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.0.tgz#d8befd1e1f5126bf35a17668276cc8c244650469" + integrity sha512-8+xMvN6JvdDtPbGBqA7oRNyZD4od1h/SIzrWqHcKZjitbVXrFpozEeyn4iI4af1UwdrabQpiZMaV07fPUTGd4w== dependencies: - conventional-changelog-writer "^4.0.5" - conventional-commits-parser "^3.0.2" + add-stream "^1.0.0" + conventional-changelog-writer "^4.0.17" + conventional-commits-parser "^3.1.0" dateformat "^3.0.0" get-pkg-repo "^1.0.0" git-raw-commits "2.0.0" git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.2" - lodash "^4.2.1" + git-semver-tags "^4.1.0" + lodash "^4.17.15" normalize-package-data "^2.3.5" q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" + shelljs "^0.8.3" through2 "^3.0.0" -conventional-changelog-ember@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.2.tgz#284ffdea8c83ea8c210b65c5b4eb3e5cc0f4f51a" - integrity sha512-qtZbA3XefO/n6DDmkYywDYi6wDKNNc98MMl2F9PKSaheJ25Trpi3336W8fDlBhq0X+EJRuseceAdKLEMmuX2tg== +conventional-changelog-ember@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.8.tgz#f0f04eb7ff3c885af97db100865ab95dcfa9917f" + integrity sha512-JEMEcUAMg4Q9yxD341OgWlESQ4gLqMWMXIWWUqoQU8yvTJlKnrvcui3wk9JvnZQyONwM2g1MKRZuAjKxr8hAXA== dependencies: q "^1.5.1" -conventional-changelog-eslint@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.2.tgz#e9eb088cda6be3e58b2de6a5aac63df0277f3cbe" - integrity sha512-Yi7tOnxjZLXlCYBHArbIAm8vZ68QUSygFS7PgumPRiEk+9NPUeucy5Wg9AAyKoBprSV3o6P7Oghh4IZSLtKCvQ== +conventional-changelog-eslint@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.8.tgz#f8b952b7ed7253ea0ac0b30720bb381f4921b46c" + integrity sha512-5rTRltgWG7TpU1PqgKHMA/2ivjhrB+E+S7OCTvj0zM/QGg4vmnVH67Vq/EzvSNYtejhWC+OwzvDrLk3tqPry8A== dependencies: q "^1.5.1" -conventional-changelog-express@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.1.tgz#fea2231d99a5381b4e6badb0c1c40a41fcacb755" - integrity sha512-G6uCuCaQhLxdb4eEfAIHpcfcJ2+ao3hJkbLrw/jSK/eROeNfnxCJasaWdDAfFkxsbpzvQT4W01iSynU3OoPLIw== +conventional-changelog-express@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.5.tgz#6e93705acdad374516ca125990012a48e710f8de" + integrity sha512-pW2hsjKG+xNx/Qjof8wYlAX/P61hT5gQ/2rZ2NsTpG+PgV7Rc8RCfITvC/zN9K8fj0QmV6dWmUefCteD9baEAw== dependencies: q "^1.5.1" -conventional-changelog-jquery@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.4.tgz#7eb598467b83db96742178e1e8d68598bffcd7ae" - integrity sha512-IVJGI3MseYoY6eybknnTf9WzeQIKZv7aNTm2KQsiFVJH21bfP2q7XVjfoMibdCg95GmgeFlaygMdeoDDa+ZbEQ== +conventional-changelog-jquery@^3.0.10: + version "3.0.10" + resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.10.tgz#fe8eb6aff322aa980af5eb68497622a5f6257ce7" + integrity sha512-QCW6wF8QgPkq2ruPaxc83jZxoWQxLkt/pNxIDn/oYjMiVgrtqNdd7lWe3vsl0hw5ENHNf/ejXuzDHk6suKsRpg== dependencies: q "^1.5.1" -conventional-changelog-jshint@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.1.tgz#11c0e8283abf156a4ff78e89be6fdedf9bd72202" - integrity sha512-kRFJsCOZzPFm2tzRHULWP4tauGMvccOlXYf3zGeuSW4U0mZhk5NsjnRZ7xFWrTFPlCLV+PNmHMuXp5atdoZmEg== +conventional-changelog-jshint@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.8.tgz#3fff4df8cb46037f77b9dc3f8e354c7f99332f13" + integrity sha512-hB/iI0IiZwnZ+seYI+qEQ4b+EMQSEC8jGIvhO2Vpz1E5p8FgLz75OX8oB1xJWl+s4xBMB6f8zJr0tC/BL7YOjw== dependencies: - compare-func "^1.3.1" + compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-preset-loader@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.1.1.tgz#65bb600547c56d5627d23135154bcd9a907668c4" - integrity sha512-K4avzGMLm5Xw0Ek/6eE3vdOXkqnpf9ydb68XYmCc16cJ99XMMbc2oaNMuPwAsxVK6CC1yA4/I90EhmWNj0Q6HA== +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.5: - version "4.0.6" - resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.6.tgz#24db578ac8e7c89a409ef9bba12cf3c095990148" - integrity sha512-ou/sbrplJMM6KQpR5rKFYNVQYesFjN7WpNGdudQSWNi6X+RgyFUcSv871YBYkrUYV9EX8ijMohYVzn9RUb+4ag== +conventional-changelog-writer@^4.0.17: + version "4.0.17" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.17.tgz#4753aaa138bf5aa59c0b274cb5937efcd2722e21" + integrity sha512-IKQuK3bib/n032KWaSb8YlBFds+aLmzENtnKtxJy3+HqDq5kohu3g/UdNbIHeJWygfnEbZjnCKFxAW0y7ArZAw== dependencies: - compare-func "^1.3.1" - conventional-commits-filter "^2.0.2" + compare-func "^2.0.0" + conventional-commits-filter "^2.0.6" dateformat "^3.0.0" - handlebars "^4.1.0" + handlebars "^4.7.6" json-stringify-safe "^5.0.1" - lodash "^4.2.1" - meow "^4.0.0" + lodash "^4.17.15" + meow "^7.0.0" semver "^6.0.0" split "^1.0.0" through2 "^3.0.0" -conventional-changelog@^3.1.8: - version "3.1.8" - resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.8.tgz#091382b5a0820bf8ec8e75ad2664a3688c31b07d" - integrity sha512-fb3/DOLLrQdNqN0yYn/lT6HcNsAa9A+VTDBqlZBMQcEPPIeJIMI+DBs3yu+eiYOLi22w9oShq3nn/zN6qm1Hmw== - dependencies: - conventional-changelog-angular "^5.0.3" - conventional-changelog-atom "^2.0.1" - conventional-changelog-codemirror "^2.0.1" - conventional-changelog-conventionalcommits "^3.0.2" - conventional-changelog-core "^3.2.2" - conventional-changelog-ember "^2.0.2" - conventional-changelog-eslint "^3.0.2" - conventional-changelog-express "^2.0.1" - conventional-changelog-jquery "^3.0.4" - conventional-changelog-jshint "^2.0.1" - conventional-changelog-preset-loader "^2.1.1" - -conventional-commits-filter@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz#f122f89fbcd5bb81e2af2fcac0254d062d1039c1" - integrity sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ== +conventional-changelog@^3.1.23: + version "3.1.23" + resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.23.tgz#d696408021b579a3814aba79b38729ed86478aea" + integrity sha512-sScUu2NHusjRC1dPc5p8/b3kT78OYr95/Bx7Vl8CPB8tF2mG1xei5iylDTRjONV5hTlzt+Cn/tBWrKdd299b7A== + dependencies: + conventional-changelog-angular "^5.0.11" + conventional-changelog-atom "^2.0.7" + conventional-changelog-codemirror "^2.0.7" + conventional-changelog-conventionalcommits "^4.4.0" + conventional-changelog-core "^4.2.0" + conventional-changelog-ember "^2.0.8" + conventional-changelog-eslint "^3.0.8" + conventional-changelog-express "^2.0.5" + conventional-changelog-jquery "^3.0.10" + conventional-changelog-jshint "^2.0.8" + conventional-changelog-preset-loader "^2.3.4" + +conventional-commits-filter@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.6.tgz#0935e1240c5ca7698329affee1b6a46d33324c4c" + integrity sha512-4g+sw8+KA50/Qwzfr0hL5k5NWxqtrOVw4DDk3/h6L85a9Gz0/Eqp3oP+CWCNfesBvZZZEFHF7OTEbRe+yYSyKw== dependencies: lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.0.3.tgz#c3f972fd4e056aa8b9b4f5f3d0e540da18bf396d" - integrity sha512-KaA/2EeUkO4bKjinNfGUyqPTX/6w9JGshuQRik4r/wJz7rUw3+D3fDG6sZSEqJvKILzKXFQuFkpPLclcsAuZcg== +conventional-commits-parser@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz#10140673d5e7ef5572633791456c5d03b69e8be4" + integrity sha512-RSo5S0WIwXZiRxUGTPuYFbqvrR4vpJ1BDdTlthFgvHt5kEdnd1+pdvwWphWn57/oIl4V72NMmOocFqqJ8mFFhA== dependencies: JSONStream "^1.0.4" - is-text-path "^2.0.0" - lodash "^4.2.1" - meow "^4.0.0" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^7.0.0" split2 "^2.0.0" through2 "^3.0.0" trim-off-newlines "^1.0.0" -conventional-recommended-bump@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-5.0.0.tgz#019d45a1f3d2cc14a26e9bad1992406ded5baa23" - integrity sha512-CsfdICpbUe0pmM4MTG90GPUqnFgB1SWIR2HAh+vS+JhhJdPWvc0brs8oadWoYGhFOQpQwe57JnvzWEWU0m2OSg== +conventional-recommended-bump@^6.0.10: + version "6.0.10" + resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.0.10.tgz#ac2fb3e31bad2aeda80086b345bf0c52edd1d1b3" + integrity sha512-2ibrqAFMN3ZA369JgVoSbajdD/BHN6zjY7DZFKTHzyzuQejDUCjQ85S5KHxCRxNwsbDJhTPD5hOKcis/jQhRgg== dependencies: concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.1.1" - conventional-commits-filter "^2.0.2" - conventional-commits-parser "^3.0.2" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.6" + conventional-commits-parser "^3.1.0" git-raw-commits "2.0.0" - git-semver-tags "^2.0.2" - meow "^4.0.0" + git-semver-tags "^4.1.0" + meow "^7.0.0" q "^1.5.1" convert-hex@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/convert-hex/-/convert-hex-0.1.0.tgz#08c04568922c27776b8a2e81a95d393362ea0b65" + integrity sha1-CMBFaJIsJ3drii6BqV05M2LqC2U= -convert-source-map@^1.4.0, convert-source-map@^1.5.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.6.0.tgz#51b537a8c43e0f04dec1993bffcdd504e758ac20" +convert-source-map@^1.4.0, convert-source-map@^1.5.1, convert-source-map@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442" + integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA== dependencies: safe-buffer "~5.1.1" convert-string@~0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/convert-string/-/convert-string-0.1.0.tgz#79ce41a9bb0d03bcf72cdc6a8f3c56fbbc64410a" + integrity sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo= cookie-parser@^1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" + version "1.4.4" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.4.tgz#e6363de4ea98c3def9697b93421c09f30cf5d188" + integrity sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw== dependencies: cookie "0.3.1" cookie-signature "1.0.6" @@ -2092,51 +3025,70 @@ cookie-parser@^1.4.3: cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= + +cookie@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" + integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== + +cookie@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + +cookiejar@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.2.tgz#dd8a235530752f988f9a0844f3fc589e3111125c" + integrity sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA== copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" + integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@^2.0.0, core-js@^2.4.0, core-js@^2.5.0: - version "2.6.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.1.tgz#87416ae817de957a3f249b3b5ca475d4aaed6042" - -core-js@^3.0.0-beta.3: - version "3.0.0-beta.6" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.0.0-beta.6.tgz#f1ee6c8bd9c1941f992fda01f886b3b40ceb1510" +core-js@^2.4.0, core-js@^2.5.0: + version "2.6.11" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c" + integrity sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg== -core-js@~2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.3.0.tgz#fab83fbb0b2d8dc85fa636c4b9d34c75420c6d65" +core-js@^3.0.1: + version "3.6.4" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.4.tgz#440a83536b458114b9cb2ac1580ba377dc470647" + integrity sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw== core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= cors@^2.8.1, cors@^2.8.4: version "2.8.5" resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== dependencies: object-assign "^4" vary "^1" -cosmiconfig@5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.0.tgz#45038e4d28a7fe787203aede9c25bca4a08b12c8" - integrity sha512-nxt+Nfc3JAqf4WIWd0jXLjTJZmsPLrA9DDc4nRw2KFJQJK7DNooqSXrNI7tzLG50CF8axczly5UV929tBmh/7g== +cosmiconfig@5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" + integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== dependencies: import-fresh "^2.0.0" is-directory "^0.3.1" - js-yaml "^3.13.0" + js-yaml "^3.13.1" parse-json "^4.0.0" cosmiconfig@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-1.1.0.tgz#0dea0f9804efdfb929fbb1b188e25553ea053d37" + integrity sha1-DeoPmATv37kp+7GxiOJVU+oFPTc= dependencies: graceful-fs "^4.1.2" js-yaml "^3.4.3" @@ -2147,9 +3099,25 @@ cosmiconfig@^1.1.0: pinkie-promise "^2.0.0" require-from-string "^1.1.0" +cote@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/cote/-/cote-1.0.0.tgz#ca1cf6ffc3064504e8605e1a933e779e12e1328e" + integrity sha512-O9L5bCnA556UHbdGS0O+D7hhf6yxRon6igbl18ADgMnsCd8P9pVfvuvgO8X/Uch5Mj+xXRoVtkWYgddwNc74eg== + dependencies: + "@dashersw/axon" "2.0.5" + "@dashersw/node-discover" "^1.0.4" + charm "1.0.2" + colors "1.4.0" + eventemitter2 "6.0.0" + lodash "^4.17.15" + portfinder "1.0.25" + socket.io "^2.3.0" + uuid "^3.3.3" + cpx@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/cpx/-/cpx-1.5.0.tgz#185be018511d87270dedccc293171e37655ab88f" + integrity sha1-GFvgGFEdhycN7czCkxceN2VauI8= dependencies: babel-runtime "^6.9.2" chokidar "^1.6.0" @@ -2166,26 +3134,30 @@ cpx@^1.5.0: crc-32@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208" + integrity sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA== dependencies: exit-on-epipe "~1.0.1" printj "~1.1.0" -create-error-class@^3.0.0, create-error-class@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" - dependencies: - capture-stack-trace "^1.0.0" - cron-parser@^2.7.3: - version "2.7.3" - resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.7.3.tgz#12603f89f5375af353a9357be2543d3172eac651" + version "2.13.0" + resolved "https://registry.yarnpkg.com/cron-parser/-/cron-parser-2.13.0.tgz#6f930bb6f2931790d2a9eec83b3ec276e27a6725" + integrity sha512-UWeIpnRb0eyoWPVk+pD3TDpNx3KCFQeezO224oJIkktBrcW6RoAPOx5zIKprZGfk6vcYSmA8yQXItejSaDBhbQ== dependencies: is-nan "^1.2.1" - moment-timezone "^0.5.23" + moment-timezone "^0.5.25" + +cross-env@^7.0.2: + version "7.0.2" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.2.tgz#bd5ed31339a93a3418ac4f3ca9ca3403082ae5f9" + integrity sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw== + dependencies: + cross-spawn "^7.0.1" cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" + integrity sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk= dependencies: lru-cache "^4.0.1" shebang-command "^1.2.0" @@ -2202,33 +3174,62 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -crypto-random-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-1.0.0.tgz#a230f64f568310e1498009940790ec99545bca7e" +cross-spawn@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.1.tgz#0ab56286e0f7c24e153d04cc2aa027e43a9a5d14" + integrity sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cryptiles@^4.1.2: + version "4.1.3" + resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-4.1.3.tgz#2461d3390ea0b82c643a6ba79f0ed491b0934c25" + integrity sha512-gT9nyTMSUC1JnziQpPbxKGBbUg8VL7Zn2NB4E1cJYvuXdElHrwxrV9bmltZGDzet45zSDGyYceueke1TjynGzw== + dependencies: + boom "7.x.x" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== crypto@0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/crypto/-/crypto-0.0.3.tgz#470a81b86be4c5ee17acc8207a1f5315ae20dbb0" + integrity sha1-RwqBuGvkxe4XrMggeh9TFa4g27A= + +cssfilter@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/cssfilter/-/cssfilter-0.0.10.tgz#c6d2672632a2e5c83e013e6864a42ce8defd20ae" + integrity sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4= cssom@0.3.x, "cssom@>= 0.3.2 < 0.4.0": - version "0.3.4" - resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.4.tgz#8cd52e8a3acfd68d3aed38ee0a640177d2f9d797" + version "0.3.8" + resolved "https://registry.yarnpkg.com/cssom/-/cssom-0.3.8.tgz#9f1276f5b2b463f2114d3f2c75250af8c1a36f4a" + integrity sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg== -"cssstyle@>= 0.2.37 < 0.3.0": - version "0.2.37" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-0.2.37.tgz#541097234cb2513c83ceed3acddc27ff27987d54" +cssstyle@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.4.0.tgz#9d31328229d3c565c61e586b02041a28fccdccf1" + integrity sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA== dependencies: cssom "0.3.x" -cssstyle@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/cssstyle/-/cssstyle-1.1.1.tgz#18b038a9c44d65f7a8e428a653b9f6fe42faf5fb" +csvtojson@^2.0.10: + version "2.0.10" + resolved "https://registry.yarnpkg.com/csvtojson/-/csvtojson-2.0.10.tgz#11e7242cc630da54efce7958a45f443210357574" + integrity sha512-lUWFxGKyhraKCW8Qghz6Z0f2l/PqB1W3AO0HKJzGIQ5JRSlR651ekJDiGJbBT4sRNNv5ddnSGVEnsxP9XRCVpQ== dependencies: - cssom "0.3.x" + bluebird "^3.5.1" + lodash "^4.17.3" + strip-bom "^2.0.0" currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" + integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= dependencies: array-find-index "^1.0.1" @@ -2242,35 +3243,38 @@ dargs@^4.0.1: dashdash@^1.12.0: version "1.14.1" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= dependencies: assert-plus "^1.0.0" -data-uri-to-buffer@2: - version "2.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.1.tgz#ca8f56fe38b1fd329473e9d1b4a9afcd8ce1c045" - integrity sha512-OkVVLrerfAKZlW2ZZ3Ve2y65jgiWqBKsTfUIAFbn8nVbPcCZg6l6gikKlEYv0kXcmzqGm6mFq/Jf2vriuEkv8A== - dependencies: - "@types/node" "^8.0.7" +dasherize@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/dasherize/-/dasherize-2.0.0.tgz#6d809c9cd0cf7bb8952d80fc84fa13d47ddb1308" + integrity sha1-bYCcnNDPe7iVLYD8hPoT1H3bEwg= data-urls@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe" + integrity sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ== dependencies: abab "^2.0.0" whatwg-mimetype "^2.2.0" whatwg-url "^7.0.0" -date-and-time@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.6.3.tgz#2daee52df67c28bd93bce862756ac86b68cf4237" +date-and-time@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/date-and-time/-/date-and-time-0.12.0.tgz#6d30c91c47fa72edadd628b71ec2ac46909b9267" + integrity sha512-n2RJIAp93AucgF/U/Rz5WRS2Hjg5Z+QxscaaMCi6pVZT1JpJKRH+C08vyH/lRR1kxNXnPxgo3lWfd+jCb/UcuQ== date-fns@^1.27.2: version "1.30.1" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" + integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== dateformat@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-2.2.0.tgz#4065e2013cf9fb916ddfd82efb506ad4c6769062" + integrity sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI= dateformat@^3.0.0: version "3.0.3" @@ -2280,6 +3284,7 @@ dateformat@^3.0.0: dateformat@~1.0.4-1.2.3: version "1.0.12" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-1.0.12.tgz#9f124b67594c937ff706932e4a642cca8dbbfee9" + integrity sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk= dependencies: get-stdin "^4.0.1" meow "^3.3.0" @@ -2287,33 +3292,37 @@ dateformat@~1.0.4-1.2.3: debounce@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + +debug@*, debug@4, debug@4.1.1, debug@^4.1.0, debug@^4.1.1, debug@~4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" -debug@2.6.9, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9: +debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9, debug@~2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: +debug@3.1.0, debug@~3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== dependencies: ms "2.0.0" -debug@4, debug@4.1.1, debug@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -debug@^3.1.0, debug@^3.2.5, debug@^3.2.6: +debug@^3.1.1: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== dependencies: ms "^2.1.1" -decamelize-keys@^1.0.0: +decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -2324,10 +3333,12 @@ decamelize-keys@^1.0.0: decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= decode-uri-component@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" + integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU= decompress-response@^3.3.0: version "3.3.0" @@ -2336,28 +3347,91 @@ decompress-response@^3.3.0: dependencies: mimic-response "^1.0.0" -deep-equal@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" +decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1" + integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ== + dependencies: + file-type "^5.2.0" + is-stream "^1.1.0" + tar-stream "^1.5.2" + +decompress-tarbz2@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b" + integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A== + dependencies: + decompress-tar "^4.1.0" + file-type "^6.1.0" + is-stream "^1.1.0" + seek-bzip "^1.0.5" + unbzip2-stream "^1.0.9" + +decompress-targz@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee" + integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w== + dependencies: + decompress-tar "^4.1.1" + file-type "^5.2.0" + is-stream "^1.1.0" + +decompress-unzip@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69" + integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k= + dependencies: + file-type "^3.8.0" + get-stream "^2.2.0" + pify "^2.3.0" + yauzl "^2.4.2" + +decompress@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d" + integrity sha1-eu3YVCflqS2s/lVnSnxQXpbQH50= + dependencies: + decompress-tar "^4.0.0" + decompress-tarbz2 "^4.0.0" + decompress-targz "^4.0.0" + decompress-unzip "^4.0.1" + graceful-fs "^4.1.10" + make-dir "^1.0.0" + pify "^2.3.0" + strip-dirs "^2.0.0" + +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= + +deep-equal@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-2.0.1.tgz#fc12bbd6850e93212f21344748682ccc5a8813cf" + integrity sha512-7Et6r6XfNW61CPPCIYfm1YPGSmh6+CliYeL4km7GWJcpX5LTAflGF8drLLR+MZX+2P3NZfAfSduutBbSWqER4g== + dependencies: + es-abstract "^1.16.3" + es-get-iterator "^1.0.1" + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + isarray "^2.0.5" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + side-channel "^1.0.1" + which-boxed-primitive "^1.0.1" + which-collection "^1.0.0" deep-extend@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + integrity sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA== deep-is@~0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - -deepmerge@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" - integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== - -default-require-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8" - dependencies: - strip-bom "^2.0.0" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= defaults@^1.0.3: version "1.0.3" @@ -2367,69 +3441,63 @@ defaults@^1.0.3: clone "^1.0.2" defer-to-connect@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.0.2.tgz#4bae758a314b034ae33902b5aac25a8dd6a8633e" - integrity sha512-k09hcQcTDY+cwgiwa6PYKLm3jlagNzQ+RSvhjzESOGOx+MNOuXkxTfEvPrO1IOQ81tArCFYQgi631clB70RpQw== + version "1.1.3" + resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-1.1.3.tgz#331ae050c08dcf789f8c83a7b81f0ed94f4ac591" + integrity sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ== -define-properties@^1.1.1, define-properties@^1.1.2: +define-properties@^1.1.2, define-properties@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== dependencies: object-keys "^1.0.12" define-property@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/define-property/-/define-property-0.2.5.tgz#c35b1ef918ec3c990f9a5bc57be04aacec5c8116" + integrity sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY= dependencies: is-descriptor "^0.1.0" define-property@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/define-property/-/define-property-1.0.0.tgz#769ebaaf3f4a63aad3af9e8d304c9bbe79bfb0e6" + integrity sha1-dp66rz9KY6rTr56NMEybvnm/sOY= dependencies: is-descriptor "^1.0.0" define-property@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/define-property/-/define-property-2.0.2.tgz#d459689e8d654ba77e02a817f8710d702cb16e9d" + integrity sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ== dependencies: is-descriptor "^1.0.2" isobject "^3.0.1" -degenerator@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/degenerator/-/degenerator-1.0.4.tgz#fcf490a37ece266464d9cc431ab98c5819ced095" - integrity sha1-/PSQo37OJmRk2cxDGrmMWBnO0JU= - dependencies: - ast-types "0.x.x" - escodegen "1.x.x" - esprima "3.x.x" - -delay@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/delay/-/delay-2.0.0.tgz#9112eadc03e4ec7e00297337896f273bbd91fae5" - dependencies: - p-defer "^1.0.0" - delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" +denque@^1.1.0, denque@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.1.tgz#6744ff7641c148c3f8a69c307e51235c1f4a37cf" + integrity sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ== -denque@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/denque/-/denque-1.4.0.tgz#79e2f0490195502107f24d9553f374837dabc916" +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= deprecated-decorator@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/deprecated-decorator/-/deprecated-decorator-0.1.6.tgz#00966317b7a12fe92f3cc831f7583af329b86c37" + integrity sha1-AJZjF7ehL+kvPMgx91g68ym4bDc= deprecated-obj@1.0.1: version "1.0.1" @@ -2439,24 +3507,27 @@ deprecated-obj@1.0.1: flat "^4.1.0" lodash "^4.17.11" -deprecation@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-1.0.1.tgz#2df79b79005752180816b7b6e079cbd80490d711" - integrity sha512-ccVHpE72+tcIKaGMql33x5MAjKQIZrk+3x2GbJ7TeraUCZWHoT+KSZpoC+JQFsUBlSTXUrBaGiF0j6zVTepPLg== +deprecation@^2.0.0, deprecation@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" + integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= detect-indent@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + integrity sha1-920GQ1LN9Docts5hnE7jqUdd4gg= dependencies: repeating "^2.0.0" -detect-libc@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" +detect-newline@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-2.1.0.tgz#f41f1c10be4b00e87b5f13da680759f2c5bfd3e2" + integrity sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I= detect-repo-changelog@1.0.1: version "1.0.1" @@ -2468,92 +3539,79 @@ detect-repo-changelog@1.0.1: lodash.find "^4.6.0" pify "^2.3.0" -dicer@0.2.5: - version "0.2.5" - resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.2.5.tgz#5996c086bb33218c812c090bddc09cd12facb70f" - dependencies: - readable-stream "1.1.x" - streamsearch "0.1.2" - -dicer@^0.3.0: +dicer@0.3.0, dicer@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872" + integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA== dependencies: streamsearch "0.1.2" -diff-match-patch@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1" +diff-sequences@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-24.9.0.tgz#5715d6244e2aa65f48bba0bc972db0b0b11e95b5" + integrity sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew== diff@^3.1.0, diff@^3.2.0, diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== diff@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" - integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== - -dir-glob@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.0.0.tgz#0b205d2b6aef98238ca286598a8204d29d0a0034" - dependencies: - arrify "^1.0.1" - path-type "^3.0.0" - -dir-glob@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-2.2.2.tgz#fa09f0694153c8918b18ba0deafae94769fc50c4" - integrity sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw== - dependencies: - path-type "^3.0.0" + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -dockerfile-ast@0.0.16: - version "0.0.16" - resolved "https://registry.yarnpkg.com/dockerfile-ast/-/dockerfile-ast-0.0.16.tgz#10b329d343329dab1de70375833495f85ad65913" - integrity sha512-+HZToHjjiLPl46TqBrok5dMrg5oCkZFPSROMQjRmvin0zG4FxK0DJXTpV/CUPYY2zpmEvVza55XLwSHFx/xZMw== +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== dependencies: - vscode-languageserver-types "^3.5.0" + path-type "^4.0.0" doctrine@0.7.2: version "0.7.2" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-0.7.2.tgz#7cb860359ba3be90e040b26b729ce4bfa654c523" + integrity sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM= dependencies: esutils "^1.1.6" isarray "0.0.1" -dom-storage@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/dom-storage/-/dom-storage-2.1.0.tgz#00fb868bc9201357ea243c7bcfd3304c1e34ea39" - domexception@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" + integrity sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug== dependencies: webidl-conversions "^4.0.2" -dot-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" - integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc= - dependencies: - is-obj "^1.0.0" +dont-sniff-mimetype@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/dont-sniff-mimetype/-/dont-sniff-mimetype-1.1.0.tgz#c7d0427f8bcb095762751252af59d148b0a623b2" + integrity sha512-ZjI4zqTaxveH2/tTlzS1wFp+7ncxNZaIEWYg3lzZRHkKf5zPT/MnEG6WL0BhHMJUabkh8GeU5NL5j+rEUCb7Ug== -dot-prop@^4.1.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" +dot-prop@^5.1.0, dot-prop@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-5.2.0.tgz#c34ecc29556dc45f1f4c22697b6f4904e0cc4fcb" + integrity sha512-uEUyaDKoSQ1M4Oq8l45hSE26SnTxL6snNnqvK/VWx5wJhmff5z0FUVJDKDanor/6w3kzE3i7XZOk+7wC0EXr1A== dependencies: - is-obj "^1.0.0" + is-obj "^2.0.0" dotenv@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-4.0.0.tgz#864ef1379aced55ce6f95debecdce179f7a0cd1d" + integrity sha1-hk7xN5rO1Vzm+V3r7NzhefegzR0= double-ended-queue@^2.1.0-0: version "2.1.0-0" resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= +duplexer2@~0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1" + integrity sha1-ixLauHjA1p4+eJEFFmKjL8a93ME= + dependencies: + readable-stream "^2.0.2" + duplexer3@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" @@ -2562,221 +3620,312 @@ duplexer3@^0.1.4: duplexer@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" + integrity sha1-rOb/gIwc5mtX0ev5eXessCM0z8E= -duplexify@^3.5.0, duplexify@^3.5.4: - version "3.6.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.6.1.tgz#b1a7a29c4abfd639585efaecce80d666b1e34125" +duplexify@^3.5.0, duplexify@^3.6.0: + version "3.7.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" + integrity sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g== dependencies: end-of-stream "^1.0.0" inherits "^2.0.1" readable-stream "^2.0.0" stream-shift "^1.0.0" -duplexify@^3.6.0: - version "3.7.1" - resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" +duplexify@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.1.tgz#7027dc374f157b122a8ae08c2d3ea4d2d953aa61" + integrity sha512-DY3xVEmVHTv1wSzKNbwoU6nVjzI369Y6sPoqfYr0/xlx3IdX2n94xIszTcjPO8W8ZIv0Wb0PXNcjuZyT4wiICA== dependencies: - end-of-stream "^1.0.0" - inherits "^2.0.1" - readable-stream "^2.0.0" + end-of-stream "^1.4.1" + inherits "^2.0.3" + readable-stream "^3.1.1" stream-shift "^1.0.0" dynamic-dedupe@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz#06e44c223f5e4e94d78ef9db23a6515ce2f962a1" + integrity sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE= dependencies: xtend "^4.0.0" -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= dependencies: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.10.tgz#1c595000f04a8897dfb85000892a0f4c33af86c3" +ecdsa-sig-formatter@1.0.11, ecdsa-sig-formatter@^1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== dependencies: safe-buffer "^5.0.1" ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= + +elasticsearch@^16.6.0: + version "16.6.0" + resolved "https://registry.yarnpkg.com/elasticsearch/-/elasticsearch-16.6.0.tgz#ba6b6096269f205f6fcde4171424dcd67229febe" + integrity sha512-MhsdE2JaBJoV1EGzSkCqqhNGxafXJuhPr+eD3vbXmsk/QWhaiU12oyXF0VhjcL8+UlwTHv0CAUbyjtE1wqoIdw== + dependencies: + agentkeepalive "^3.4.1" + chalk "^1.0.0" + lodash "^4.17.10" elegant-spinner@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -email-deep-validator@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/email-deep-validator/-/email-deep-validator-3.1.0.tgz#87392e13f5372a5ba34b7af018fc4946b703d2e9" - dependencies: - loglevel "^1.6.1" - -email-validator@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/email-validator/-/email-validator-2.0.4.tgz#b8dfaa5d0dae28f1b03c95881d904d4e40bfe7ed" - integrity sha512-gYCwo7kh5S3IDyZPLZf6hSS0MnZT8QmJFqYvbqlDZSbwdZlY6QZWxJ4i/6UhITOJ4XzyI647Bm2MXKCLqnJ4nQ== - -empower-core@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/empower-core/-/empower-core-1.2.0.tgz#ce3fb2484d5187fa29c23fba8344b0b2fdf5601c" - dependencies: - call-signature "0.0.2" - core-js "^2.0.0" +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== -empower@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/empower/-/empower-1.3.1.tgz#768979cbbb36d71d8f5edaab663deacb9dab916c" - dependencies: - core-js "^2.0.0" - empower-core "^1.2.0" +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== encodeurl@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -end-of-stream@^1.0.0, end-of-stream@^1.1.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" +end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: + version "1.4.4" + resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" + integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== dependencies: once "^1.4.0" +engine.io-client@~3.4.0: + version "3.4.3" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-3.4.3.tgz#192d09865403e3097e3575ebfeb3861c4d01a66c" + integrity sha512-0NGY+9hioejTEJCaSJZfWZLk4FPI9dN+1H1C4+wj2iuFba47UgZbJzfWs4aNFajnX/qAaYKbe2lLTfEEWzCmcw== + dependencies: + component-emitter "~1.3.0" + component-inherit "0.0.3" + debug "~4.1.0" + engine.io-parser "~2.2.0" + has-cors "1.1.0" + indexof "0.0.1" + parseqs "0.0.5" + parseuri "0.0.5" + ws "~6.1.0" + xmlhttprequest-ssl "~1.5.4" + yeast "0.1.2" + +engine.io-parser@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-2.2.0.tgz#312c4894f57d52a02b420868da7b5c1c84af80ed" + integrity sha512-6I3qD9iUxotsC5HEMuuGsKA0cXerGz+4uGcXQEkfBidgKf0amsjrrtwcbwK/nzpZBxclXlV7gGl9dgWvu4LF6w== + dependencies: + after "0.8.2" + arraybuffer.slice "~0.0.7" + base64-arraybuffer "0.1.5" + blob "0.0.5" + has-binary2 "~1.0.2" + +engine.io@~3.4.0: + version "3.4.2" + resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-3.4.2.tgz#8fc84ee00388e3e228645e0a7d3dfaeed5bd122c" + integrity sha512-b4Q85dFkGw+TqgytGPrGgACRUhsdKc9S9ErRAXpPGy/CXKs4tYoHDkvIRdsseAF7NjfVwjRFIn6KTnbw7LwJZg== + dependencies: + accepts "~1.3.4" + base64id "2.0.0" + cookie "0.3.1" + debug "~4.1.0" + engine.io-parser "~2.2.0" + ws "^7.1.2" + ent@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d" + integrity sha1-6WQhkyWiHQX0RGai9obtbOX13R0= -errno@~0.1.7: - version "0.1.7" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" - dependencies: - prr "~1.0.1" +envinfo@^7.5.1: + version "7.7.2" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.7.2.tgz#098f97a0e902f8141f9150553c92dbb282c4cabe" + integrity sha512-k3Eh5bKuQnZjm49/L7H4cHzs2FlL5QjbTB3JrPxoTI8aJG7hVMe4uKyJxSYH4ahseby2waUwk5OaKX/nAsaYgg== error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== dependencies: is-arrayish "^0.2.1" -es-abstract@^1.5.1: - version "1.12.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165" +erxes-inmemory-storage@^1.0.16: + version "1.0.16" + resolved "https://registry.yarnpkg.com/erxes-inmemory-storage/-/erxes-inmemory-storage-1.0.16.tgz#7a3dd37727fe7fe91ac4635626b73081c5aa3f2b" + integrity sha512-HmRrsCkK1XnP4XIN0xU1gR/scjhkNDDeSFg+UyUeWihm1gWvdsXIpM95hEiEmT1hJhxDSeI/HBTd0CPVXtBfUA== dependencies: - es-to-primitive "^1.1.1" - function-bind "^1.1.1" - has "^1.0.1" - is-callable "^1.1.3" - is-regex "^1.0.4" + "@babel/runtime" "^7.11.2" + redis "^3.0.2" -es-to-primitive@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" +erxes-message-broker@^1.0.17: + version "1.0.17" + resolved "https://registry.yarnpkg.com/erxes-message-broker/-/erxes-message-broker-1.0.17.tgz#2407312163d010f292e153cf1b45e33e0fb16f63" + integrity sha512-UdA5nRLn1tn5pqnDM+hR6LTrkiiOduVBanvAjxlvZujv6bUP+SdKzf17VWQhk4/3jZMmWzHIBZ0vpzHbHaVQTg== + dependencies: + "@babel/runtime" "^7.11.2" + amqplib "^0.6.0" + cote "^1.0.0" + cross-env "^7.0.2" + debug "^4.1.1" + requestify "^0.2.5" + uuid "^8.3.0" + +erxes-telemetry@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/erxes-telemetry/-/erxes-telemetry-1.0.4.tgz#0a703f971414684ffe30f8720b7a069896b9a170" + integrity sha512-TKrpK7pmmAb05Xf7mcV0vxuAAYdKBYJZlLSuUYQo8wfQ8mvr0M4hqxvzteJGV/qmGbOTUmNDKCC60RDbCUZmEw== + dependencies: + "@babel/code-frame" "^7.10.3" + "@babel/runtime" "^7.10.3" + boxen "^4.2.0" + configstore "^5.0.1" + envinfo "^7.5.1" + fs-extra "^8.1.0" + gatsby-core-utils "^1.3.14" + git-up "4.0.1" + is-docker "2.0.0" + lodash "^4.17.15" + node-fetch "2.6.0" + uuid "3.4.0" + +es-abstract@^1.16.3, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4: + version "1.17.4" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.4.tgz#e3aedf19706b20e7c2594c35fc0d57605a79e184" + integrity sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-get-iterator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" + integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== + dependencies: + es-abstract "^1.17.4" + has-symbols "^1.0.1" + is-arguments "^1.0.4" + is-map "^2.0.1" + is-set "^2.0.1" + is-string "^1.0.5" + isarray "^2.0.5" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== dependencies: is-callable "^1.1.4" is-date-object "^1.0.1" is-symbol "^1.0.2" -es6-promise@^4.0.3: - version "4.2.6" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f" - -es6-promise@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.0.2.tgz#010d5858423a5f118979665f46486a95c6ee2bb6" - -es6-promisify@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" - dependencies: - es6-promise "^4.0.3" +escape-goat@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/escape-goat/-/escape-goat-2.1.1.tgz#1b2dc77003676c457ec760b2dc68edb648188675" + integrity sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q== escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= + +escape-regexp@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/escape-regexp/-/escape-regexp-0.0.1.tgz#f44bda12d45bbdf9cb7f862ee7e4827b3dd32254" + integrity sha1-9EvaEtRbvfnLf4Yu5+SCez3TIlQ= escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= -escodegen@1.x.x: - version "1.11.1" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.1.tgz#c485ff8d6b4cdb89e27f4a856e91f118401ca510" - integrity sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw== - dependencies: - esprima "^3.1.3" - estraverse "^4.2.0" - esutils "^2.0.2" - optionator "^0.8.1" - optionalDependencies: - source-map "~0.6.1" - -escodegen@^1.6.1, escodegen@^1.9.1: - version "1.11.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.11.0.tgz#b27a9389481d5bfd5bec76f7bb1eb3f8f4556589" +escodegen@^1.9.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.14.1.tgz#ba01d0c8278b5e95a9a45350142026659027a457" + integrity sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ== dependencies: - esprima "^3.1.3" + esprima "^4.0.1" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: source-map "~0.6.1" -esprima@3.x.x, esprima@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-3.1.3.tgz#fdca51cee6133895e3c88d535ce49dbff62a4633" - -esprima@^4.0.0, esprima@~4.0.0: +esprima@^4.0.0, esprima@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -espurify@^1.6.0: - version "1.8.1" - resolved "https://registry.yarnpkg.com/espurify/-/espurify-1.8.1.tgz#5746c6c1ab42d302de10bd1d5bf7f0e8c0515056" - dependencies: - core-js "^2.0.0" - -estraverse@^4.1.0, estraverse@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" +estraverse@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== esutils@^1.1.6: version "1.1.6" resolved "https://registry.yarnpkg.com/esutils/-/esutils-1.1.6.tgz#c01ccaa9ae4b897c6d0c3e210ae52f3c7a844375" + integrity sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U= esutils@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= event-target-shim@^5.0.0: version "5.0.1" resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +eventemitter2@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.0.0.tgz#218eb512c3603c5341724b6af7b686a1aa5ab8f5" + integrity sha512-ZuNWHD7S7IoikyEmx35vPU8H1W0L+oi644+4mSTg7nwXvBQpIwQL7DPjYUF0VMB0jPkNMo3MqD07E7MYrkFmjQ== eventemitter3@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" + version "3.1.2" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.2.tgz#2d3d48f9c346698fce83a85d7d664e98535df6e7" + integrity sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q== events@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" + integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= -exec-sh@^0.2.0: - version "0.2.2" - resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.2.2.tgz#2a5e7ffcbd7d0ba2755bdecb16e5a427dfbdec36" - dependencies: - merge "^1.2.0" +exec-sh@^0.3.2: + version "0.3.4" + resolved "https://registry.yarnpkg.com/exec-sh/-/exec-sh-0.3.4.tgz#3a018ceb526cc6f6df2bb504b2bfe8e3a4934ec5" + integrity sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A== execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" + integrity sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c= dependencies: cross-spawn "^5.0.1" get-stream "^3.0.0" @@ -2802,20 +3951,29 @@ execa@^1.0.0: exit-hook@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + integrity sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g= exit-on-epipe@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz#0bdd92e87d5285d267daa8171d0eb06159689692" + integrity sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw== + +exit@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" + integrity sha1-BjJjj42HfMghB9MKD/8aF8uhzQw= expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + integrity sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s= dependencies: is-posix-bracket "^0.1.0" expand-brackets@^2.1.4: version "2.1.4" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-2.1.4.tgz#b77735e315ce30f6b6eff0f83b04151a22449622" + integrity sha1-t3c14xXOMPa27/D4OwQVGiJEliI= dependencies: debug "^2.3.3" define-property "^0.2.5" @@ -2828,23 +3986,14 @@ expand-brackets@^2.1.4: expand-range@^1.8.1: version "1.8.2" resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + integrity sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc= dependencies: fill-range "^2.1.0" -expect@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-21.2.1.tgz#003ac2ac7005c3c29e73b38a272d4afadd6d1d7b" - dependencies: - ansi-styles "^3.2.0" - jest-diff "^21.2.1" - jest-get-type "^21.2.0" - jest-matcher-utils "^21.2.1" - jest-message-util "^21.2.1" - jest-regex-util "^21.2.0" - expect@^22.4.0: version "22.4.3" resolved "https://registry.yarnpkg.com/expect/-/expect-22.4.3.tgz#d5a29d0a0e1fb2153557caef2674d4547e914674" + integrity sha512-XcNXEPehqn8b/jm8FYotdX0YrXn36qp4HWlrVT4ktwQas1l1LPxiVWncYnnL2eyMtKAmVIaG0XAp0QlrqJaxaA== dependencies: ansi-styles "^3.2.0" jest-diff "^22.4.3" @@ -2853,62 +4002,78 @@ expect@^22.4.0: jest-message-util "^22.4.3" jest-regex-util "^22.4.3" -express@^4.15.2: - version "4.16.4" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.4.tgz#fddef61926109e24c515ea97fd2f1bdbf62df12e" +expect@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/expect/-/expect-24.9.0.tgz#b75165b4817074fa4a157794f46fe9f1ba15b6ca" + integrity sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q== + dependencies: + "@jest/types" "^24.9.0" + ansi-styles "^3.2.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-regex-util "^24.9.0" + +express@^4.17.1: + version "4.17.1" + resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134" + integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g== dependencies: - accepts "~1.3.5" + accepts "~1.3.7" array-flatten "1.1.1" - body-parser "1.18.3" - content-disposition "0.5.2" + body-parser "1.19.0" + content-disposition "0.5.3" content-type "~1.0.4" - cookie "0.3.1" + cookie "0.4.0" cookie-signature "1.0.6" debug "2.6.9" depd "~1.1.2" encodeurl "~1.0.2" escape-html "~1.0.3" etag "~1.8.1" - finalhandler "1.1.1" + finalhandler "~1.1.2" fresh "0.5.2" merge-descriptors "1.0.1" methods "~1.1.2" on-finished "~2.3.0" - parseurl "~1.3.2" + parseurl "~1.3.3" path-to-regexp "0.1.7" - proxy-addr "~2.0.4" - qs "6.5.2" - range-parser "~1.2.0" + proxy-addr "~2.0.5" + qs "6.7.0" + range-parser "~1.2.1" safe-buffer "5.1.2" - send "0.16.2" - serve-static "1.13.2" - setprototypeof "1.1.0" - statuses "~1.4.0" - type-is "~1.6.16" + send "0.17.1" + serve-static "1.14.1" + setprototypeof "1.1.1" + statuses "~1.5.0" + type-is "~1.6.18" utils-merge "1.0.1" vary "~1.1.2" extend-shallow@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8= dependencies: is-extendable "^0.1.0" extend-shallow@^3.0.0, extend-shallow@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-3.0.2.tgz#26a71aaf073b39fb2127172746131c2704028db8" + integrity sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg= dependencies: assign-symbols "^1.0.0" is-extendable "^1.0.1" -extend@^3.0.0, extend@^3.0.1, extend@^3.0.2, extend@~3.0.2: +extend@^3.0.2, extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== external-editor@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" - integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== dependencies: chardet "^0.7.0" iconv-lite "^0.4.24" @@ -2917,12 +4082,14 @@ external-editor@^3.0.3: extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + integrity sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE= dependencies: is-extglob "^1.0.0" extglob@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/extglob/-/extglob-2.0.4.tgz#ad00fe4dc612a9232e8718711dc5cb5ab0285543" + integrity sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw== dependencies: array-unique "^0.3.2" define-property "^1.0.0" @@ -2936,85 +4103,124 @@ extglob@^2.0.4: extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= extsprintf@^1.2.0: version "1.4.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= faker@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/faker/-/faker-4.1.0.tgz#1e45bbbecc6774b3c195fad2835109c6d748cc3f" + integrity sha1-HkW7vsxndLPBlfrSg1EJxtdIzD8= -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - -fast-glob@^2.0.2: - version "2.2.4" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.4.tgz#e54f4b66d378040e0e4d6a68ec36bbc5b04363c0" - dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== -fast-glob@^2.2.6: - version "2.2.7" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-2.2.7.tgz#6953857c3afa475fff92ee6015d52da70a4cd39d" - integrity sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw== +fast-glob@^3.0.3: + version "3.2.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.2.tgz#ade1a9d91148965d4bf7c51f72e1ca662d32e63d" + integrity sha512-UDV82o4uQyljznxwMxyVRJgZZt3O5wENYojjzbaGEGZgeOxkLFf+V4cnUD+krzb2F72E18RhamkMZ7AdeggF7A== dependencies: - "@mrmlnc/readdir-enhanced" "^2.2.1" - "@nodelib/fs.stat" "^1.1.2" - glob-parent "^3.1.0" - is-glob "^4.0.0" - merge2 "^1.2.3" - micromatch "^3.1.10" + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.0" + merge2 "^1.3.0" + micromatch "^4.0.2" + picomatch "^2.2.1" fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== -fast-levenshtein@~2.0.4: +fast-levenshtein@~2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz#124aa885899261f68aedb42a7c080de9da608743" + integrity sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA== fast-text-encoding@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.0.tgz#3e5ce8293409cfaa7177a71b9ca84e1b1e6f25ef" + integrity sha512-R9bHCvweUxxwkDwhjav5vxpFvdPGlVngtqmx4pIZfSUhM/Q4NiIUHB456BAf+Q1Nwu3HEZYONtu+Rya+af4jiQ== -faye-websocket@0.11.1: - version "0.11.1" - resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.1.tgz#f0efe18c4f56e4f40afc7e06c719fd5ee6188f38" +fastq@^1.6.0: + version "1.6.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.6.1.tgz#4570c74f2ded173e71cf0beb08ac70bb85826791" + integrity sha512-mpIH5sKYueh3YyeJwqtVo8sORi0CgtmkVbK6kZStpQlZBYQuTzG2CZ7idSiJuA7bY0SFCWUc5WIs+oYumGCQNw== + dependencies: + reusify "^1.0.4" + +faye-websocket@0.11.3: + version "0.11.3" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.3.tgz#5c0e9a8968e8912c286639fde977a8b209f2508e" + integrity sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA== dependencies: websocket-driver ">=0.5.1" fb-watchman@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" + version "2.0.1" + resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.1.tgz#fc84fb39d2709cf3ff6d743706157bb5708a8a85" + integrity sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg== dependencies: - bser "^2.0.0" + bser "2.1.1" + +fd-slicer@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e" + integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4= + dependencies: + pend "~1.2.0" + +feature-policy@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069" + integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ== figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + integrity sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4= dependencies: escape-string-regexp "^1.0.5" object-assign "^4.1.0" -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== dependencies: escape-string-regexp "^1.0.5" file-type@^10.4.0: - version "10.7.0" - resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.7.0.tgz#b6a9bf24f1d14ba514ab9087c7864d4da4a7ce76" + version "10.11.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-10.11.0.tgz#2961d09e4675b9fb9a3ee6b69e9cd23f43fd1890" + integrity sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw== + +file-type@^3.8.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9" + integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek= + +file-type@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6" + integrity sha1-LdvqfHP/42No365J3DOMBYwritY= + +file-type@^6.1.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919" + integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg== -file-uri-to-path@1: +file-uri-to-path@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== @@ -3022,23 +4228,19 @@ file-uri-to-path@1: filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" - -fileset@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/fileset/-/fileset-2.0.3.tgz#8e7548a96d3cc2327ee5e674168723a333bba2a0" - dependencies: - glob "^7.0.3" - minimatch "^3.0.3" + integrity sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY= filewatcher@~3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/filewatcher/-/filewatcher-3.0.1.tgz#f4a1957355ddaf443ccd78a895f3d55e23c8a034" + integrity sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ= dependencies: debounce "^1.0.0" fill-range@^2.1.0: version "2.2.4" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + integrity sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q== dependencies: is-number "^2.1.0" isobject "^2.0.0" @@ -3049,42 +4251,69 @@ fill-range@^2.1.0: fill-range@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-4.0.0.tgz#d544811d428f98eb06a63dc402d2403c328c38f7" + integrity sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc= dependencies: extend-shallow "^2.0.1" is-number "^3.0.0" repeat-string "^1.6.1" to-regex-range "^2.1.0" -finalhandler@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" + integrity sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA== dependencies: debug "2.6.9" encodeurl "~1.0.2" escape-html "~1.0.3" on-finished "~2.3.0" - parseurl "~1.3.2" - statuses "~1.4.0" + parseurl "~1.3.3" + statuses "~1.5.0" unpipe "~1.0.0" +find-cache-dir@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.2.0.tgz#e7fe44c1abc1299f516146e563108fd1006c1874" + integrity sha512-1JKclkYYsf1q9WIJKLZa9S9muC+08RIjzAlLrK4QcYLJMS6mk9yombQ9qf+zJ7H9LS800k0s44L4sDq9VYzqyg== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.0" + pkg-dir "^4.1.0" + find-index@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/find-index/-/find-index-0.1.1.tgz#675d358b2ca3892d795a1ab47232f8b6e2e0dde4" + integrity sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ= + +find-package-json@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-package-json/-/find-package-json-1.2.0.tgz#4057d1b943f82d8445fe52dc9cf456f6b8b58083" + integrity sha512-+SOGcLGYDJHtyqHd87ysBhmaeQ95oWspDKnMXBrnQ9Eq4OkLNqejgoaD8xVWu6GPa0B6roa6KinCMEMcVeqONw== find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + integrity sha1-M8RLQpqysvBkYpnF+fcY83b/jVQ= -find-up@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== +find-up@4.1.0, find-up@^4.0.0, find-up@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== dependencies: - locate-path "^3.0.0" + locate-path "^5.0.0" + path-exists "^4.0.0" find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" + integrity sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8= dependencies: path-exists "^2.0.0" pinkie-promise "^2.0.0" @@ -3092,22 +4321,30 @@ find-up@^1.0.0: find-up@^2.0.0, find-up@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= dependencies: locate-path "^2.0.0" -firebase-admin@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-7.2.0.tgz#dd7b78ca93014886090f2ae795ad6669f9157500" +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +firebase-admin@^8.6.1: + version "8.9.2" + resolved "https://registry.yarnpkg.com/firebase-admin/-/firebase-admin-8.9.2.tgz#248ba184dc13b4929b043870a2067787a0dd013e" + integrity sha512-ix4qcx+hHnr3mnc41Z8EzQa9Mr+2nhogLEv6ktkOCCpdKJ+9HxW9vikRCElSbC8ICHLD0KIH0GVOIZK80vbvqw== dependencies: - "@firebase/app" "^0.3.4" - "@firebase/database" "^0.3.6" - "@types/node" "^8.0.53" + "@firebase/database" "^0.5.17" + "@types/node" "^8.10.59" dicer "^0.3.0" jsonwebtoken "8.1.0" node-forge "0.7.4" optionalDependencies: - "@google-cloud/firestore" "^1.2.0" - "@google-cloud/storage" "^2.3.0" + "@google-cloud/firestore" "^3.0.0" + "@google-cloud/storage" "^4.1.2" flat@^4.1.0: version "4.1.0" @@ -3119,166 +4356,202 @@ flat@^4.1.0: flexbuffer@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/flexbuffer/-/flexbuffer-0.0.6.tgz#039fdf23f8823e440c38f3277e6fef1174215b30" - -follow-redirects@^1.3.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.0.tgz#d12452c031e8c67eb6637d861bfc7a8090167933" - dependencies: - debug "=3.1.0" - -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= + integrity sha1-A5/fI/iCPkQMOPMnfm/vEXQhWzA= for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= for-own@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" - dependencies: - for-in "^1.0.1" - -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= + integrity sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4= dependencies: for-in "^1.0.1" forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@3.0.0, form-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.0.tgz#31b7e39c85f1355b7139ee0c647cf0de7f83c682" + integrity sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" -form-data@2.3.3, form-data@~2.3.2: +form-data@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== dependencies: asynckit "^0.4.0" combined-stream "^1.0.6" mime-types "^2.1.12" -formidable@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.1.tgz#70fb7ca0290ee6ff961090415f4b3df3d2082659" +formidable@^1.1.1, formidable@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.2.tgz#bf69aea2972982675f00865342b982986f6b8dd9" + integrity sha512-V8gLm+41I/8kguQ4/o1D3RIHRmhYFG4pnNyonvua+40rqcEmT4+V71yaZ3B457xbbgCsCfjSPi65u/W6vK1U5Q== forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= + +frac@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/frac/-/frac-1.1.2.tgz#3d74f7f6478c88a1b5020306d747dc6313c74d0b" + integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA== fragment-cache@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19" + integrity sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk= dependencies: map-cache "^0.2.2" fresh@0.5.2: version "0.5.2" resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= -fs-capacitor@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-1.0.1.tgz#ff9dbfa14dfaf4472537720f19c3088ed9278df0" +fs-capacitor@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/fs-capacitor/-/fs-capacitor-2.0.4.tgz#5a22e72d40ae5078b4fe64fe4d08c0d3fc88ad3c" + integrity sha512-8S4f4WsCryNw2mJJchi46YgB6CR5Ze+4L1h8ewl9tEpL4SJ3ZO+c/bS4BWhB8bK+O3TMqhuZarTitd0S0eh2pA== + +fs-constants@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" + integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== fs-extra@6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-6.0.0.tgz#0f0afb290bb3deb87978da816fcd3c7797f3a817" + integrity sha512-lk2cUCo8QzbiEWEbt7Cw3m27WMiRG321xsssbcIpfMhpRjrlC08WBOVQqj1/nQYYNnPtyIhP1oqLO3QwT2tPCw== dependencies: graceful-fs "^4.1.2" jsonfile "^4.0.0" universalify "^0.1.0" -fs-minipass@^1.2.5: - version "1.2.5" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" +fs-extra@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" + integrity sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g== dependencies: - minipass "^2.2.1" + graceful-fs "^4.2.0" + jsonfile "^4.0.0" + universalify "^0.1.0" fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fs@0.0.1-security: - version "0.0.1-security" - resolved "https://registry.yarnpkg.com/fs/-/fs-0.0.1-security.tgz#8a7bd37186b6dddf3813f23858b57ecaaf5e41d4" - integrity sha1-invTcYa23d84E/I4WLV+yq9eQdQ= - -fsevents@^1.0.0, fsevents@^1.2.3: - version "1.2.4" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" +fsevents@^1.0.0, fsevents@^1.2.7: + version "1.2.11" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.11.tgz#67bf57f4758f02ede88fb2a1712fef4d15358be3" + integrity sha512-+ux3lx6peh0BpvY0JebGyZoiR4D+oYzdPZMKJwkZ+sFkNJzpL7tXc/wehS49gUAxg3tmMHPHZkA8JU2rhhgDHw== dependencies: - nan "^2.9.2" - node-pre-gyp "^0.10.0" + bindings "^1.5.0" + nan "^2.12.1" -ftp@~0.3.10: - version "0.3.10" - resolved "https://registry.yarnpkg.com/ftp/-/ftp-0.3.10.tgz#9197d861ad8142f3e63d5a83bfe4c59f7330885d" - integrity sha1-kZfYYa2BQvPmPVqDv+TFn3MwiF0= +fstream@^1.0.12: + version "1.0.12" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== dependencies: - readable-stream "1.1.x" - xregexp "2.0.0" + graceful-fs "^4.1.2" + inherits "~2.0.0" + mkdirp ">=0.5 0" + rimraf "2" function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" +gatsby-core-utils@^1.3.14: + version "1.3.14" + resolved "https://registry.yarnpkg.com/gatsby-core-utils/-/gatsby-core-utils-1.3.14.tgz#a830412a9edb87544bc0d1ad9c2556d33aa0d157" + integrity sha512-jfC+x5rrYUfl70MHRLsOtsXqdlqIbQGVDKXrvp6IPIUP8TKU6XIpYktF0Yd4ldJIWmGZTa062RWUOd2DFBHVSw== dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" + ci-info "2.0.0" + configstore "^5.0.1" + fs-extra "^8.1.0" + node-object-hash "^2.0.0" + proper-lockfile "^4.1.1" + xdg-basedir "^4.0.0" -gaxios@^1.0.2, gaxios@^1.0.4, gaxios@^1.2.1, gaxios@^1.5.0: - version "1.8.3" - resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-1.8.3.tgz#7dd79860880d22f854d814b3870332be8b16de56" +gaxios@^2.0.0, gaxios@^2.0.1, gaxios@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-2.3.1.tgz#52bd832b5d6a252072783b9afd9742bde835b2f4" + integrity sha512-DQOesWEx59/bm63lTX0uHDDXpGTW9oKqNsoigwCoRe2lOb5rFqxzHjLTa6aqEBecLcz69dHLw7rbS068z1fvIQ== dependencies: abort-controller "^3.0.0" extend "^3.0.2" - https-proxy-agent "^2.2.1" + https-proxy-agent "^5.0.0" + is-stream "^2.0.0" node-fetch "^2.3.0" -gcp-metadata@^0.6.1, gcp-metadata@^0.6.3: - version "0.6.3" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-0.6.3.tgz#4550c08859c528b370459bd77a7187ea0bdbc4ab" - dependencies: - axios "^0.18.0" - extend "^3.0.1" - retry-axios "0.3.2" - -gcp-metadata@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-1.0.0.tgz#5212440229fa099fc2f7c2a5cdcb95575e9b2ca6" +gcp-metadata@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-3.4.0.tgz#dfcbb6abe2c262c86c8eb01e4729d46b44e21484" + integrity sha512-fizmBtCXHp8b7FZuzbgKaixO8DzsSYoEVmMgZIna7x8t6cfBF3eqirODWYxVbgmasA5qudCAKiszfB7yVwroIQ== dependencies: - gaxios "^1.0.2" + gaxios "^2.1.0" json-bigint "^0.3.0" -gcs-resumable-upload@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-1.1.0.tgz#2b06f5876dcf60f18a309343f79ed951aff01399" +gcs-resumable-upload@^2.2.4: + version "2.3.2" + resolved "https://registry.yarnpkg.com/gcs-resumable-upload/-/gcs-resumable-upload-2.3.2.tgz#f04a7459483f871f0de71db7454296938688a296" + integrity sha512-OPS0iAmPCV+r7PziOIhyxmQOzsazFCy76yYDOS/Z80O/7cuny1KMfqDQa2T0jLaL8EreTU7EMZG5pUuqBKgzHA== dependencies: - abort-controller "^2.0.2" - configstore "^4.0.0" - gaxios "^1.5.0" - google-auth-library "^3.0.0" - pumpify "^1.5.1" + abort-controller "^3.0.0" + configstore "^5.0.0" + gaxios "^2.0.0" + google-auth-library "^5.0.0" + pumpify "^2.0.0" stream-events "^1.0.4" +generate-password@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/generate-password/-/generate-password-1.5.1.tgz#ad463fadee1b4818edb7b827ff6f3499587d8dd5" + integrity sha512-XdsyfiF4mKoOEuzA44w9jSNav50zOurdWOV3V8DbA7SJIxR3Xm9ob14HKYTnMQOPX3ylqiJMnQF0wEa8gXZIMw== + +gensync@^1.0.0-beta.1: + version "1.0.0-beta.1" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" + integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== + get-caller-file@^1.0.1: version "1.0.3" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" + integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== get-pkg-repo@^1.0.0: version "1.4.0" @@ -3291,13 +4564,35 @@ get-pkg-repo@^1.0.0: parse-github-repo-url "^1.3.0" through2 "^2.0.0" +get-port@*: + version "5.1.1" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" + integrity sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ== + +get-port@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/get-port/-/get-port-5.0.0.tgz#aa22b6b86fd926dd7884de3e23332c9f70c031a6" + integrity sha512-imzMU0FjsZqNa6BqOjbbW6w5BivHIuQKopjpPqcnx0AVHJQKCxK1O+Ab3OrVXhrekqfVMjwA9ZYu062R+KcIsQ== + dependencies: + type-fest "^0.3.0" + get-stdin@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe" + integrity sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4= + +get-stream@^2.2.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de" + integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4= + dependencies: + object-assign "^4.0.1" + pinkie-promise "^2.0.0" get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" + integrity sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ= get-stream@^4.0.0, get-stream@^4.1.0: version "4.1.0" @@ -3313,25 +4608,15 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -get-uri@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/get-uri/-/get-uri-2.0.3.tgz#fa13352269781d75162c6fc813c9e905323fbab5" - integrity sha512-x5j6Ks7FOgLD/GlvjKwgu7wdmMR55iuRHhn8hj/+gA+eSbxQvZ+AEomq+3MgVEZj1vpi738QahGbCCSIDtXtkw== - dependencies: - data-uri-to-buffer "2" - debug "4" - extend "~3.0.2" - file-uri-to-path "1" - ftp "~0.3.10" - readable-stream "3" - get-value@^2.0.3, get-value@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/get-value/-/get-value-2.0.6.tgz#dc15ca1c672387ca76bd37ac0a395ba2042a2c28" + integrity sha1-3BXKHGcjh8p2vTesCjlbogQqLCg= getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= dependencies: assert-plus "^1.0.0" @@ -3355,19 +4640,19 @@ git-remote-origin-url@^2.0.0: pify "^2.3.0" git-repo-info@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.0.tgz#13d1f753c75bc2994432e65a71e35377ff563813" - integrity sha512-+kigfDB7j3W80f74BoOUX+lKOmf4pR3/i2Ww6baKTCPe2hD4FRdjhV3s4P5Dy0Tak1uY1891QhKoYNtnyX2VvA== + version "2.1.1" + resolved "https://registry.yarnpkg.com/git-repo-info/-/git-repo-info-2.1.1.tgz#220ffed8cbae74ef8a80e3052f2ccb5179aed058" + integrity sha512-8aCohiDo4jwjOwma4FmYFd3i97urZulL8XL24nIPxuE+GZnfsAyy/g2Shqx6OjUiFKUXZM+Yy+KHnOmmA3FVcg== -git-semver-tags@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-2.0.2.tgz#f506ec07caade191ac0c8d5a21bdb8131b4934e3" - integrity sha512-34lMF7Yo1xEmsK2EkbArdoU79umpvm0MfzaDkSNYSJqtM5QLAVTPWgpiXSVI5o/O9EvZPSrP4Zvnec/CqhSd5w== +git-semver-tags@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.0.tgz#0146c9bc24ee96104c99f443071c8c2d7dc848e3" + integrity sha512-TcxAGeo03HdErzKzi4fDD+xEL7gi8r2Y5YSxH6N2XYdVSV5UkBwfrt7Gqo1b+uSHCjy/sa9Y6BBBxxFLxfbhTg== dependencies: - meow "^4.0.0" - semver "^5.5.0" + meow "^7.0.0" + semver "^6.0.0" -git-up@^4.0.0: +git-up@4.0.1, git-up@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.1.tgz#cb2ef086653640e721d2042fe3104857d89007c0" integrity sha512-LFTZZrBlrCrGCG07/dm1aCjjpL1z9L3+5aEeI9SBhAqSc+kiA9Or1bgZhQFNppJX6h/f5McrvJt1mQXTFm6Qrw== @@ -3392,6 +4677,7 @@ gitconfiglocal@^1.0.0: glob-base@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + integrity sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q= dependencies: glob-parent "^2.0.0" is-glob "^2.0.0" @@ -3399,30 +4685,28 @@ glob-base@^0.3.0: glob-parent@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + integrity sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg= dependencies: is-glob "^2.0.0" -glob-parent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" +glob-parent@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== dependencies: - is-glob "^3.1.0" - path-dirname "^1.0.0" - -glob-to-regexp@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + is-glob "^4.0.1" glob2base@^0.0.12: version "0.0.12" resolved "https://registry.yarnpkg.com/glob2base/-/glob2base-0.0.12.tgz#9d419b3e28f12e83a362164a277055922c9c0d56" + integrity sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY= dependencies: find-index "^0.1.1" -glob@^7.0.0, glob@^7.1.3: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== +glob@^7.0.0, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== dependencies: fs.realpath "^1.0.0" inflight "^1.0.4" @@ -3431,185 +4715,81 @@ glob@^7.0.0, glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2: - version "7.1.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" +global-dirs@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-2.0.1.tgz#acdf3bb6685bcd55cb35e8a052266569e9469201" + integrity sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A== dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" + ini "^1.3.5" -global-dirs@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/global-dirs/-/global-dirs-0.1.1.tgz#b319c0dd4607f353f3be9cca4c72fc148c49f445" - integrity sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU= - dependencies: - ini "^1.3.4" +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + integrity sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ== -globby@9.2.0: - version "9.2.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" - integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== +globby@10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-10.0.2.tgz#277593e745acaa4646c3ab411289ec47a0392543" + integrity sha512-7dUi7RvCoT/xast/o/dLN53oqND4yk0nsHkhRgn9w65C4PofCLOoJ39iSOg+qVDdWQPIEj+eszMHQ+aLVwwQSg== dependencies: "@types/glob" "^7.1.1" - array-union "^1.0.2" - dir-glob "^2.2.2" - fast-glob "^2.2.6" + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.0.3" glob "^7.1.3" - ignore "^4.0.3" - pify "^4.0.1" - slash "^2.0.0" - -globby@^7.1.1: - version "7.1.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-7.1.1.tgz#fb2ccff9401f8600945dfada97440cca972b8680" - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - -globby@^8.0.0: - version "8.0.1" - resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50" - dependencies: - array-union "^1.0.1" - dir-glob "^2.0.0" - fast-glob "^2.0.2" - glob "^7.1.2" - ignore "^3.3.5" - pify "^3.0.0" - slash "^1.0.0" - -google-auth-library@^1.3.1, google-auth-library@^1.6.0: - version "1.6.1" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-1.6.1.tgz#9c73d831ad720c0c3048ab89d0ffdec714d07dd2" - dependencies: - axios "^0.18.0" - gcp-metadata "^0.6.3" - gtoken "^2.3.0" - jws "^3.1.5" - lodash.isstring "^4.0.1" - lru-cache "^4.1.3" - retry-axios "^0.3.2" + ignore "^5.1.1" + merge2 "^1.2.3" + slash "^3.0.0" -google-auth-library@^3.0.0, google-auth-library@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-3.1.2.tgz#ff2f88cd5cd2118a57bd3d5ad3c093c8837fc350" +google-auth-library@^5.0.0, google-auth-library@^5.5.0: + version "5.10.1" + resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-5.10.1.tgz#504ec75487ad140e68dd577c21affa363c87ddff" + integrity sha512-rOlaok5vlpV9rSiUu5EpR0vVpc+PhN62oF4RyX/6++DG1VsaulAFEMlDYBLjJDDPI6OcNOCGAKy9UVB/3NIDXg== dependencies: + arrify "^2.0.0" base64-js "^1.3.0" + ecdsa-sig-formatter "^1.0.11" fast-text-encoding "^1.0.0" - gaxios "^1.2.1" - gcp-metadata "^1.0.0" - gtoken "^2.3.2" - https-proxy-agent "^2.2.1" - jws "^3.1.5" + gaxios "^2.1.0" + gcp-metadata "^3.4.0" + gtoken "^4.1.0" + jws "^4.0.0" lru-cache "^5.0.0" - semver "^5.5.0" -google-auto-auth@^0.10.0: - version "0.10.1" - resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.10.1.tgz#68834a6f3da59a6cb27fce56f76e3d99ee49d0a2" +google-gax@^1.13.0, google-gax@^1.7.5: + version "1.14.2" + resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-1.14.2.tgz#ce4f9a42c1bc2ca4a4ed8e8cc70c6f7a3548b790" + integrity sha512-Nde+FdqALbV3QgMA4KlkxOHfrj9busnZ3EECwy/1gDJm9vhKGwDLWzErqRU5g80OoGSAMgyY7DWIfqz7ina4Jw== dependencies: - async "^2.3.0" - gcp-metadata "^0.6.1" - google-auth-library "^1.3.1" - request "^2.79.0" - -google-auto-auth@^0.9.0: - version "0.9.7" - resolved "https://registry.yarnpkg.com/google-auto-auth/-/google-auto-auth-0.9.7.tgz#70b357ec9ec8e2368cf89a659309a15a1472596b" - dependencies: - async "^2.3.0" - gcp-metadata "^0.6.1" - google-auth-library "^1.3.1" - request "^2.79.0" - -google-gax@^0.16.0: - version "0.16.1" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-0.16.1.tgz#30bf1284a1c384cd31a01163def4d671cec10c0f" - dependencies: - duplexify "^3.5.4" - extend "^3.0.0" - globby "^8.0.0" - google-auto-auth "^0.10.0" - google-proto-files "^0.15.0" - grpc "^1.10.0" - is-stream-ended "^0.1.0" - lodash "^4.17.2" - protobufjs "^6.8.0" - through2 "^2.0.3" - -google-gax@^0.25.0: - version "0.25.6" - resolved "https://registry.yarnpkg.com/google-gax/-/google-gax-0.25.6.tgz#5ea5c743933ba957da63951bc828aef91fb69340" - dependencies: - "@grpc/grpc-js" "^0.3.0" - "@grpc/proto-loader" "^0.4.0" + "@grpc/grpc-js" "^0.6.18" + "@grpc/proto-loader" "^0.5.1" + "@types/fs-extra" "^8.0.1" + "@types/long" "^4.0.0" + abort-controller "^3.0.0" duplexify "^3.6.0" - google-auth-library "^3.0.0" - google-proto-files "^0.20.0" - grpc "^1.16.0" - grpc-gcp "^0.1.1" + google-auth-library "^5.0.0" is-stream-ended "^0.1.4" lodash.at "^4.6.0" lodash.has "^4.5.2" + node-fetch "^2.6.0" protobufjs "^6.8.8" retry-request "^4.0.0" semver "^6.0.0" - walkdir "^0.3.2" - -google-p12-pem@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-1.0.3.tgz#3d8acc140573339a5bca7b2f6a4b206bbea6d8d7" - dependencies: - node-forge "^0.7.5" - pify "^4.0.0" - -google-proto-files@^0.15.0: - version "0.15.1" - resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-0.15.1.tgz#5c9c485e574e2c100fe829a5ec0bbb3d9bc789a2" - dependencies: - globby "^7.1.1" - power-assert "^1.4.4" - protobufjs "^6.8.0" - -google-proto-files@^0.20.0: - version "0.20.0" - resolved "https://registry.yarnpkg.com/google-proto-files/-/google-proto-files-0.20.0.tgz#dfcd1635a0c3f00f49ca057462cf369108ff4b5e" - dependencies: - "@google-cloud/promisify" "^0.4.0" - protobufjs "^6.8.0" - walkdir "^0.3.0" + walkdir "^0.4.0" -googleapis-common@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/googleapis-common/-/googleapis-common-0.2.1.tgz#d4332d47fcdc93a89160a6fca79d630e5a138f7f" - dependencies: - axios "^0.18.0" - google-auth-library "^1.6.0" - pify "^3.0.0" - qs "^6.5.2" - url-template "^2.0.8" - uuid "^3.2.1" - -googleapis@^33.0.0: - version "33.0.0" - resolved "https://registry.yarnpkg.com/googleapis/-/googleapis-33.0.0.tgz#9963391eb93d63ea0e3810148eb27371e3a36862" +google-p12-pem@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/google-p12-pem/-/google-p12-pem-2.0.4.tgz#036462394e266472632a78b685f0cc3df4ef337b" + integrity sha512-S4blHBQWZRnEW44OcR7TL9WR+QCqByRvhNDZ/uuQfpxywfupikf/miba8js1jZi6ZOGv5slgSuoshCWh6EMDzg== dependencies: - google-auth-library "^1.6.0" - googleapis-common "^0.2.0" + node-forge "^0.9.0" -got@9.6.0: +got@9.6.0, got@^9.6.0: version "9.6.0" resolved "https://registry.yarnpkg.com/got/-/got-9.6.0.tgz#edf45e7d67f99545705de1f7bbeeeb121765ed85" integrity sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q== @@ -3626,49 +4806,34 @@ got@9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -got@^6.7.1: - version "6.7.1" - resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" - integrity sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA= - dependencies: - create-error-class "^3.0.0" - duplexer3 "^0.1.4" - get-stream "^3.0.0" - is-redirect "^1.0.0" - is-retry-allowed "^1.0.0" - is-stream "^1.0.0" - lowercase-keys "^1.0.0" - safe-buffer "^5.0.1" - timed-out "^4.0.0" - unzip-response "^2.0.1" - url-parse-lax "^1.0.0" - -graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" +graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6: + version "4.2.3" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" + integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== -graphlib@^2.1.1, graphlib@^2.1.5: - version "2.1.7" - resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.7.tgz#b6a69f9f44bd9de3963ce6804a2fc9e73d86aecc" - integrity sha512-TyI9jIy2J4j0qgPmOOrHTCtpPqJGN/aurBwc6ZT+bRii+di1I+Wv3obRhVrmBEXet+qkMaEX67dXrwsd3QQM6w== - dependencies: - lodash "^4.17.5" +graceful-fs@^4.2.0: + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== -graphql-extensions@0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.0.tgz#5857c7b7b9f20dbccbfd88730fffa5963b3c61ee" - dependencies: - "@apollographql/apollo-tools" "^0.2.6" +"graceful-readlink@>= 1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" + integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU= -graphql-extensions@0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.4.1.tgz#92c49a8409ffbfb24559d7661ab60cc90d6086e4" +graphql-extensions@^0.10.10: + version "0.10.10" + resolved "https://registry.yarnpkg.com/graphql-extensions/-/graphql-extensions-0.10.10.tgz#6b89d6b171f02a83bd4252f1e71c8d69147e7e2d" + integrity sha512-pNb1DmUk6vsGtCjCRecpKoXadKNMyKxyLyE9IX65N9aKSmLL+AF7dJOOc4MWhdaAXlzxaDDhe54GpaOfoH7AOw== dependencies: - "@apollographql/apollo-tools" "^0.2.6" + "@apollographql/apollo-tools" "^0.4.3" + apollo-server-env "^2.4.3" + apollo-server-types "^0.2.10" graphql-redis-subscriptions@^1.4.0: version "1.5.0" resolved "https://registry.yarnpkg.com/graphql-redis-subscriptions/-/graphql-redis-subscriptions-1.5.0.tgz#53c9be28ee69f61ffbfe808f8f82f7e718254acf" + integrity sha512-4R/rv3qg61/UuB/9enCdWJM9s4x6TRwXYubjAlPWXJuNhGcZXn6oELu9mrhm+8QuA924/GvOo8Z7hCqE617SeQ== dependencies: graphql-subscriptions "^0.5.6" iterall "^1.1.3" @@ -3678,16 +4843,11 @@ graphql-redis-subscriptions@^1.4.0: graphql-subscriptions@^0.5.6: version "0.5.8" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-0.5.8.tgz#13a6143c546bce390404657dc73ca501def30aa7" + integrity sha512-0CaZnXKBw2pwnIbvmVckby5Ge5e2ecmjofhYCdyeACbCly2j3WXDP/pl+s+Dqd2GQFC7y99NB+53jrt55CKxYQ== dependencies: iterall "^1.2.1" -graphql-subscriptions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.0.0.tgz#475267694b3bd465af6477dbab4263a3f62702b8" - dependencies: - iterall "^1.2.1" - -graphql-subscriptions@^1.1.0: +graphql-subscriptions@^1.0.0, graphql-subscriptions@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz#5f2fa4233eda44cf7570526adfcf3c16937aef11" integrity sha512-6WzlBFC0lWmXJbIVE8OgFgXIP4RJi3OQgTPa0DVMsDXdpRDjTsM1K9wfl5HSYX7R87QAGlvcv2Y4BIZa/ItonA== @@ -3695,12 +4855,14 @@ graphql-subscriptions@^1.1.0: iterall "^1.2.1" graphql-tag@^2.9.2: - version "2.10.0" - resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.0.tgz#87da024be863e357551b2b8700e496ee2d4353ae" + version "2.10.3" + resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.3.tgz#ea1baba5eb8fc6339e4c4cf049dabe522b0edf03" + integrity sha512-4FOv3ZKfA4WdOKJeHdz6B3F/vxBLSgmBcGeAFPf4n1F64ltJUvOOerNj0rsJxONQGdhUMynQIvd6LzB+1J5oKA== graphql-tools@^4.0.0, graphql-tools@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.3.tgz#23b5cb52c519212b1b2e4630a361464396ad264b" + version "4.0.7" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.7.tgz#743309b96cb657ff45b607ee0a07193cd987e43c" + integrity sha512-rApl8sT8t/W1uQRcwzxMYyUBiCl/XicluApiDkNze5TX/GR0BSTQMjM2UcRGdTmkbsb1Eqq6afkyyeG/zMxZYQ== dependencies: apollo-link "^1.2.3" apollo-utilities "^1.0.1" @@ -3709,75 +4871,41 @@ graphql-tools@^4.0.0, graphql-tools@^4.0.3: uuid "^3.1.0" graphql-upload@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.0.2.tgz#1c1f116f15b7f8485cf40ff593a21368f0f58856" + version "8.1.0" + resolved "https://registry.yarnpkg.com/graphql-upload/-/graphql-upload-8.1.0.tgz#6d0ab662db5677a68bfb1f2c870ab2544c14939a" + integrity sha512-U2OiDI5VxYmzRKw0Z2dmfk0zkqMRaecH9Smh1U277gVgVe9Qn+18xqf4skwr4YJszGIh7iQDZ57+5ygOK9sM/Q== dependencies: - busboy "^0.2.14" - fs-capacitor "^1.0.0" - http-errors "^1.7.1" + busboy "^0.3.1" + fs-capacitor "^2.0.4" + http-errors "^1.7.3" object-path "^0.11.4" -graphql@^14.0.2: - version "14.0.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.0.2.tgz#7dded337a4c3fd2d075692323384034b357f5650" +graphql@*, graphql@^14.0.2, graphql@^14.5.3: + version "14.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.6.0.tgz#57822297111e874ea12f5cd4419616930cd83e49" + integrity sha512-VKzfvHEKybTKjQVpTFrA5yUq2S9ihcZvfJAtsDBBCuV6wauPu1xl/f9ehgVf0FcEJJs4vz6ysb/ZMkGigQZseg== dependencies: iterall "^1.2.2" -growly@^1.2.0, growly@^1.3.0: +growly@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081" + integrity sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE= -grpc-gcp@^0.1.1: - version "0.1.1" - resolved "https://registry.yarnpkg.com/grpc-gcp/-/grpc-gcp-0.1.1.tgz#a11be8a7e7a6edf5f636b44a6a24fb4cc028f71f" - dependencies: - grpc "^1.16.0" - protobufjs "^6.8.8" - -grpc@^1.10.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.17.0.tgz#d7971dd39bd4eec90c69a048f7727795ab504876" - dependencies: - lodash.camelcase "^4.3.0" - lodash.clone "^4.5.0" - nan "^2.0.0" - node-pre-gyp "^0.12.0" - protobufjs "^5.0.3" - -grpc@^1.16.0: - version "1.19.0" - resolved "https://registry.yarnpkg.com/grpc/-/grpc-1.19.0.tgz#129fb30923ea2fa7a9b2623f9e7930eda91a242f" - dependencies: - lodash.camelcase "^4.3.0" - lodash.clone "^4.5.0" - nan "^2.0.0" - node-pre-gyp "^0.12.0" - protobufjs "^5.0.3" - -gtoken@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.0.tgz#4e0ffc16432d7041a1b3dbc1d97aac17a5dc964a" - dependencies: - axios "^0.18.0" - google-p12-pem "^1.0.0" - jws "^3.1.4" - mime "^2.2.0" - pify "^3.0.0" - -gtoken@^2.3.2: - version "2.3.3" - resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-2.3.3.tgz#8a7fe155c5ce0c4b71c886cfb282a9060d94a641" +gtoken@^4.1.0: + version "4.1.4" + resolved "https://registry.yarnpkg.com/gtoken/-/gtoken-4.1.4.tgz#925ff1e7df3aaada06611d30ea2d2abf60fcd6a7" + integrity sha512-VxirzD0SWoFUo5p8RDP8Jt2AGyOmyYcT/pOUgDKJCK+iSw0TMqwrVfY37RXTNmoKwrzmDHSk0GMT9FsgVmnVSA== dependencies: - gaxios "^1.0.4" - google-p12-pem "^1.0.0" - jws "^3.1.5" + gaxios "^2.1.0" + google-p12-pem "^2.0.0" + jws "^4.0.0" mime "^2.2.0" - pify "^4.0.0" -handlebars@^4.0.14, handlebars@^4.1.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== +handlebars@^4.7.3: + version "4.7.3" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.3.tgz#8ece2797826886cf8082d1726ff21d2a022550ee" + integrity sha512-SRGwSYuNfx8DwHD/6InAPzD6RgeruWLT+B8e8a7gGs8FWgHzlExpTFMEq2IA6QpAfOClpKHy6+8IqTjeBCu6Kg== dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -3785,56 +4913,74 @@ handlebars@^4.0.14, handlebars@^4.1.0: optionalDependencies: uglify-js "^3.1.4" -handlebars@^4.0.3: - version "4.0.12" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.12.tgz#2c15c8a96d46da5e266700518ba8cb8d919d5bc5" +handlebars@^4.7.6: + version "4.7.6" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" + integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== dependencies: - async "^2.5.0" - optimist "^0.6.1" + minimist "^1.2.5" + neo-async "^2.6.0" source-map "^0.6.1" + wordwrap "^1.0.0" optionalDependencies: uglify-js "^3.1.4" har-schema@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= -har-validator@~5.1.0: +har-validator@~5.1.3: version "5.1.3" resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== dependencies: ajv "^6.5.5" har-schema "^2.0.0" +hard-rejection@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/hard-rejection/-/hard-rejection-2.1.0.tgz#1c6eda5c1685c63942766d79bb40ae773cecd883" + integrity sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA== + has-ansi@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + integrity sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE= dependencies: ansi-regex "^2.0.0" -has-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" +has-binary2@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-binary2/-/has-binary2-1.0.3.tgz#7776ac627f3ea77250cfc332dab7ddf5e4f5d11d" + integrity sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw== + dependencies: + isarray "2.0.1" -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-cors@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-cors/-/has-cors-1.1.0.tgz#5e474793f7ea9843d1bb99c23eef49ff126fff39" + integrity sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk= has-flag@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== has-value@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/has-value/-/has-value-0.3.1.tgz#7b1f58bada62ca827ec0a2078025654845995e1f" + integrity sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8= dependencies: get-value "^2.0.3" has-values "^0.1.4" @@ -3843,6 +4989,7 @@ has-value@^0.3.1: has-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-value/-/has-value-1.0.0.tgz#18b281da585b1c5c51def24c930ed29a0be6b177" + integrity sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc= dependencies: get-value "^2.0.6" has-values "^1.0.0" @@ -3851,58 +4998,131 @@ has-value@^1.0.0: has-values@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/has-values/-/has-values-0.1.4.tgz#6d61de95d91dfca9b9a02089ad384bff8f62b771" + integrity sha1-bWHeldkd/Km5oCCJrThL/49it3E= has-values@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-values/-/has-values-1.0.0.tgz#95b0b63fec2146619a6fe57fe75628d5a39efe4f" + integrity sha1-lbC2P+whRmGab+V/51Yo1aOe/k8= dependencies: is-number "^3.0.0" kind-of "^4.0.0" -has@^1.0.1: +has-yarn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/has-yarn/-/has-yarn-2.1.0.tgz#137e11354a7b5bf11aa5cb649cf0c6f3ff2b2e77" + integrity sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw== + +has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== dependencies: function-bind "^1.1.1" -hash-stream-validation@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.1.tgz#ecc9b997b218be5bb31298628bb807869b73dcd1" +hash-stream-validation@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/hash-stream-validation/-/hash-stream-validation-0.2.2.tgz#6b34c4fce5e9fce265f1d3380900049d92a10090" + integrity sha512-cMlva5CxWZOrlS/cY0C+9qAzesn5srhFA8IT1VPiHc9bWWBLkJfEUIZr7MWoi89oOOGmpg8ymchaOjiArsGu5A== dependencies: through2 "^2.0.0" +helmet-crossdomain@0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/helmet-crossdomain/-/helmet-crossdomain-0.4.0.tgz#5f1fe5a836d0325f1da0a78eaa5fd8429078894e" + integrity sha512-AB4DTykRw3HCOxovD1nPR16hllrVImeFp5VBV9/twj66lJ2nU75DP8FPL0/Jp4jj79JhTfG+pFI2MD02kWJ+fA== + +helmet-csp@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/helmet-csp/-/helmet-csp-2.10.0.tgz#685dde1747bc16c5e28ad9d91e229a69f0a85e84" + integrity sha512-Rz953ZNEFk8sT2XvewXkYN0Ho4GEZdjAZy4stjiEQV3eN7GDxg1QKmYggH7otDyIA7uGA6XnUMVSgeJwbR5X+w== + dependencies: + bowser "2.9.0" + camelize "1.0.0" + content-security-policy-builder "2.1.0" + dasherize "2.0.0" + +helmet@^3.23.3: + version "3.23.3" + resolved "https://registry.yarnpkg.com/helmet/-/helmet-3.23.3.tgz#5ba30209c5f73ded4ab65746a3a11bedd4579ab7" + integrity sha512-U3MeYdzPJQhtvqAVBPntVgAvNSOJyagwZwyKsFdyRa8TV3pOKVFljalPOCxbw5Wwf2kncGhmP0qHjyazIdNdSA== + dependencies: + depd "2.0.0" + dont-sniff-mimetype "1.1.0" + feature-policy "0.3.0" + helmet-crossdomain "0.4.0" + helmet-csp "2.10.0" + hide-powered-by "1.1.0" + hpkp "2.0.0" + hsts "2.2.0" + nocache "2.1.0" + referrer-policy "1.2.0" + x-xss-protection "1.3.0" + +hide-powered-by@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.1.0.tgz#be3ea9cab4bdb16f8744be873755ca663383fa7a" + integrity sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg== + +hoek@6.x.x: + version "6.1.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" + integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + integrity sha1-42w/LSyufXRqhX440Y1fMqeILbg= dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.1" -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: - version "2.7.1" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047" +hosted-git-info@^2.1.4: + version "2.8.8" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" + integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== + +hpkp@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/hpkp/-/hpkp-2.0.0.tgz#10e142264e76215a5d30c44ec43de64dee6d1672" + integrity sha1-EOFCJk52IVpdMMROxD3mTe5tFnI= + +hsts@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/hsts/-/hsts-2.2.0.tgz#09119d42f7a8587035d027dda4522366fe75d964" + integrity sha512-ToaTnQ2TbJkochoVcdXYm4HOCliNozlviNsg+X2XQLQvZNI/kCHR9rZxVYpJB3UPcHz80PgxRyWQ7PdU1r+VBQ== + dependencies: + depd "2.0.0" -html-encoding-sniffer@^1.0.1, html-encoding-sniffer@^1.0.2: +html-encoding-sniffer@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz#e70d84b94da53aa375e11fe3a351be6642ca46f8" + integrity sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw== dependencies: whatwg-encoding "^1.0.1" +html-escaper@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.0.tgz#71e87f931de3fe09e56661ab9a29aadec707b491" + integrity sha512-a4u9BeERWGu/S8JiWEAQcdrg9v4QArtP9keViQjGMdff20fBdd8waotXaNmODqBe6uZ3Nafi7K/ho4gCQHV3Ig== + http-cache-semantics@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz#495704773277eeef6e43f9ab2c2c7d259dda25c5" - integrity sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew== + version "4.1.0" + resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" + integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== -http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: - version "1.6.3" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" +http-errors@1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.2.tgz#4f5029cf13239f31036e5b2e55292bcfbcc85c8f" + integrity sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg== dependencies: depd "~1.1.2" inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.4.0 < 2" + setprototypeof "1.1.1" + statuses ">= 1.5.0 < 2" + toidentifier "1.0.0" -http-errors@1.7.3: +http-errors@^1.7.3, http-errors@~1.7.2: version "1.7.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.3.tgz#6c619e4f9c60308c38519498c14fbb10aacebb06" integrity sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw== @@ -3913,97 +5133,90 @@ http-errors@1.7.3: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -http-errors@^1.7.1: - version "1.7.1" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" - dependencies: - depd "~1.1.2" - inherits "2.0.3" - setprototypeof "1.1.0" - statuses ">= 1.5.0 < 2" - toidentifier "1.0.0" - -http-parser-js@>=0.4.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.0.tgz#d65edbede84349d0dc30320815a15d39cc3cbbd8" +"http-parser-js@>=0.4.0 <0.4.11": + version "0.4.10" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" + integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== +http-proxy-agent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== dependencies: - agent-base "4" - debug "3.1.0" + "@tootallnate/once" "1" + agent-base "6" + debug "4" http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= dependencies: assert-plus "^1.0.0" jsprim "^1.2.2" sshpk "^1.7.0" -https-proxy-agent@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" +https-proxy-agent@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz#702b71fb5520a132a66de1f67541d9e62154d82b" + integrity sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg== + dependencies: + agent-base "5" + debug "4" + +https-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz#e2a90542abb68a762e0a0850f6c9edadfd8506b2" + integrity sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA== + dependencies: + agent-base "6" + debug "4" + +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0= dependencies: - agent-base "^4.1.0" - debug "^3.1.0" + ms "^2.0.0" husky@^0.13.4: version "0.13.4" resolved "https://registry.yarnpkg.com/husky/-/husky-0.13.4.tgz#48785c5028de3452a51c48c12c4f94b2124a1407" + integrity sha1-SHhcUCjeNFKlHEjBLE+UshJKFAc= dependencies: chalk "^1.1.3" find-parent-dir "^0.3.0" is-ci "^1.0.9" normalize-path "^1.0.0" -iconv-lite@0.4.23: - version "0.4.23" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" - dependencies: - safer-buffer ">= 2.1.2 < 3" - -iconv-lite@0.4.24, iconv-lite@^0.4.24, iconv-lite@^0.4.4: +iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" -ieee754@1.1.8: - version "1.1.8" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.8.tgz#be33d40ac10ef1926701f6f08a2d86fbfd1ad3e4" - -ieee754@^1.1.4: - version "1.1.12" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.12.tgz#50bf24e5b9c8bb98af4964c941cdb0918da7b60b" - -ignore-walk@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" - dependencies: - minimatch "^3.0.4" - -ignore@^3.3.5: - version "3.3.10" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.3.10.tgz#0a97fb876986e8081c631160f8f9f389157f0043" +ieee754@1.1.13, ieee754@^1.1.4: + version "1.1.13" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" + integrity sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== -ignore@^4.0.3: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== +ignore@^5.1.1: + version "5.1.4" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.4.tgz#84b7b3dbe64552b6ef0eca99f6743dbec6d97adf" + integrity sha512-MzbUSahkTW1u7JpKKjY7LCARd1fU5W2rLdxlM4kdkayuCwZImjkpluF9CM1aLewYJguPDqewLam18Y6AU69A8A== immediate@~3.0.5: version "3.0.6" resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b" + integrity sha1-nbHb0Pr43m++D13V5Wu2BigN5ps= -import-cwd@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-2.1.0.tgz#aa6cf36e722761285cb371ec6519f53e2435b0a9" - integrity sha1-qmzzbnInYShcs3HsZRn1PiQ1sKk= +import-cwd@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-cwd/-/import-cwd-3.0.0.tgz#20845547718015126ea9b3676b7592fb8bd4cf92" + integrity sha512-4pnzH16plW+hgvRECbDWpQl3cqtvSofHWh44met7ESfZ8UZOWWddm8hEyDTqREJ9RbYHY8gi8DqmaelApoOGMg== dependencies: - import-from "^2.1.0" + import-from "^3.0.0" import-fresh@^2.0.0: version "2.0.0" @@ -4013,91 +5226,92 @@ import-fresh@^2.0.0: caller-path "^2.0.0" resolve-from "^3.0.0" -import-from@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/import-from/-/import-from-2.1.0.tgz#335db7f2a7affd53aaa471d4b8021dee36b7f3b1" - integrity sha1-M1238qev/VOqpHHUuAId7ja387E= +import-from@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966" + integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== dependencies: - resolve-from "^3.0.0" + resolve-from "^5.0.0" import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" integrity sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM= +import-local@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-2.0.0.tgz#55070be38a5993cf18ef6db7e961f5bee5c5a09d" + integrity sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ== + dependencies: + pkg-dir "^3.0.0" + resolve-cwd "^2.0.0" + imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= dependencies: repeating "^2.0.0" indent-string@^3.0.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= + +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== indexof@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/indexof/-/indexof-0.0.1.tgz#82dc336d232b9062179d05ab3293a66059fd435d" + integrity sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10= inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + wrappy "1" -inherits@2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== -ini@^1.3.0, ini@^1.3.2, ini@^1.3.4, ini@~1.3.0: +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +ini@^1.3.2, ini@^1.3.5, ini@~1.3.0: version "1.3.5" resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== -inquirer@6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.3.1.tgz#7a413b5e7950811013a3db491c61d1f3b776e8e7" - integrity sha512-MmL624rfkFt4TG9y/Jvmt8vdmOo836U7Y0Hxr2aFk3RelZEGX4Igk0KabWrcaaZaTv9uzglOqWh1Vly+FAWAXA== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.11" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -inquirer@^6.2.2: - version "6.4.1" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.4.1.tgz#7bd9e5ab0567cd23b41b0180b68e0cfa82fc3c0b" - integrity sha512-/Jw+qPZx4EDYsaT6uz7F4GJRNFMRdKNeUZw3ZnKV8lyuUgz/YWRCSUAJMZSVhSq4Ec0R2oYnyi6b3d4JXcL5Nw== +inquirer@7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.4.tgz#99af5bde47153abca23f5c7fc30db247f39da703" + integrity sha512-Bu5Td5+j11sCkqfqmUTiwv+tWisMtP0L7Q8WrqA2C/BbBhy1YTdFrvjjlrKq8oagA/tLQBski2Gcx/Sqyi2qSQ== dependencies: - ansi-escapes "^3.2.0" + ansi-escapes "^4.2.1" chalk "^2.4.2" - cli-cursor "^2.1.0" + cli-cursor "^3.1.0" cli-width "^2.0.0" external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.11" - mute-stream "0.0.7" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" + rxjs "^6.5.3" + string-width "^4.1.0" strip-ansi "^5.1.0" through "^2.3.6" @@ -4106,19 +5320,22 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296" integrity sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw== -invariant@^2.2.2: +invariant@^2.2.2, invariant@^2.2.4: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== dependencies: loose-envify "^1.0.0" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" +invert-kv@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" + integrity sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA== ioredis@^3.1.2, ioredis@^3.2.2: version "3.2.2" resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-3.2.2.tgz#b7d5ff3afd77bb9718bb2821329b894b9a44c00b" + integrity sha512-g+ShTQYLsCcOUkNOK6CCEZbj3aRDVPw3WOwXk+LxlUKvuS9ujEqP2MppBHyRVYrNNFW/vcPaTBUZ2ctGNSiOCA== dependencies: bluebird "^3.3.4" cluster-key-slot "^1.0.6" @@ -4144,57 +5361,68 @@ ioredis@^3.1.2, ioredis@^3.2.2: redis-commands "^1.2.0" redis-parser "^2.4.0" -ip@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" - integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= - -ipaddr.js@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== is-accessor-descriptor@^0.1.6: version "0.1.6" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz#a9e12cb3ae8d876727eeef3843f8a0897b5c98d6" + integrity sha1-qeEss66Nh2cn7u84Q/igiXtcmNY= dependencies: kind-of "^3.0.2" is-accessor-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz#169c2f6d3df1f992618072365c9b0ea1f6878656" + integrity sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ== dependencies: kind-of "^6.0.0" +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-bigint@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.0.tgz#73da8c33208d00f130e9b5e15d23eac9215601c4" + integrity sha512-t5mGUXC/xRheCK431ylNiSkGGpBp8bHENBcENTkDT6ppwPzEVxNGZRvgvmOEfbWkFhA7D2GEuE2mmQTr78sl2g== is-binary-path@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + integrity sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg= dependencies: binary-extensions "^1.0.0" -is-buffer@^1.0.2, is-buffer@^1.1.5: +is-boolean-object@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" + integrity sha512-TqZuVwa/sppcrhUCAYkGBk7w0yxfQQnxq28fjkO53tnK9FQXmdwz2JS5+GjsWQ6RByES1K40nI+yDic5c9/aAQ== + +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== is-buffer@~2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.3.tgz#4ecf3fcf749cbd1e472689e109ac66261a25e725" - integrity sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw== - -is-builtin-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-1.0.0.tgz#540572d34f7ac3119f8f76c30cbc1b1e037affbe" - dependencies: - builtin-modules "^1.0.0" + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== -is-callable@^1.1.3, is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== -is-ci@2.0.0: +is-ci@2.0.0, is-ci@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c" integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w== @@ -4204,28 +5432,33 @@ is-ci@2.0.0: is-ci@^1.0.10, is-ci@^1.0.9: version "1.2.1" resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.2.1.tgz#e3779c8ee17fccf428488f6e281187f2e632841c" + integrity sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg== dependencies: ci-info "^1.5.0" is-data-descriptor@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz#0b5ee648388e2c860282e793f1856fec3f301b56" + integrity sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y= dependencies: kind-of "^3.0.2" is-data-descriptor@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz#d84876321d0e7add03990406abbbbd36ba9268c7" + integrity sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ== dependencies: kind-of "^6.0.0" is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== is-descriptor@^0.1.0: version "0.1.6" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-0.1.6.tgz#366d8240dde487ca51823b1ab9f07a10a78251ca" + integrity sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg== dependencies: is-accessor-descriptor "^0.1.6" is-data-descriptor "^0.1.4" @@ -4234,6 +5467,7 @@ is-descriptor@^0.1.0: is-descriptor@^1.0.0, is-descriptor@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-descriptor/-/is-descriptor-1.0.2.tgz#3b159746a66604b04f8c81524ba365c5f14d86ec" + integrity sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg== dependencies: is-accessor-descriptor "^1.0.0" is-data-descriptor "^1.0.0" @@ -4244,126 +5478,174 @@ is-directory@^0.3.1: resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= +is-docker@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.0.0.tgz#2cb0df0e75e2d064fe1864c37cdeacb7b2dcf25b" + integrity sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ== + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + integrity sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE= is-equal-shallow@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + integrity sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ= dependencies: is-primitive "^2.0.0" is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik= is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== dependencies: is-plain-object "^2.0.4" is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + integrity sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA= -is-extglob@^2.1.0, is-extglob@^2.1.1: +is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= is-finite@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" - dependencies: - number-is-nan "^1.0.0" + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.1.0.tgz#904135c77fb42c0641d6aa1bcdbc4daa8da082f3" + integrity sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w== is-fullwidth-code-point@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + integrity sha1-754xOG8DGn8NZDr4L95QxFfvAMs= dependencies: number-is-nan "^1.0.0" is-fullwidth-code-point@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== is-generator-fn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" + integrity sha1-lp1J4bszKfa7fwkIm+JleLLd1Go= + +is-generator-fn@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" + integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== is-glob@^2.0.0, is-glob@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + integrity sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM= dependencies: is-extglob "^1.0.0" -is-glob@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-3.1.0.tgz#7ba5ae24217804ac70707b96922567486cc3e84a" - dependencies: - is-extglob "^2.1.0" - -is-glob@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" +is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" -is-installed-globally@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.1.0.tgz#0dfd98f5a9111716dd535dda6492f67bf3d25a80" - integrity sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA= +is-installed-globally@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-installed-globally/-/is-installed-globally-0.3.1.tgz#679afef819347a72584617fd19497f010b8ed35f" + integrity sha512-oiEcGoQbGc+3/iijAijrK2qFpkNoNjsHOm/5V5iaeydyrS/hnwaRCEgH5cpW0P3T1lSjV5piB7S5b5lEugNLhg== dependencies: - global-dirs "^0.1.0" - is-path-inside "^1.0.0" + global-dirs "^2.0.1" + is-path-inside "^3.0.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-map@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" + integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== is-nan@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.2.1.tgz#9faf65b6fb6db24b7f5c0628475ea71f988401e2" + version "1.3.0" + resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.0.tgz#85d1f5482f7051c2019f5673ccebdb06f3b0db03" + integrity sha512-z7bbREymOqt2CCaZVly8aC4ML3Xhfi0ekuOnjO2L8vKdl+CttdVoGZQhd4adMFAsxQ5VeRVwORs4tU8RH+HFtQ== dependencies: - define-properties "^1.1.1" + define-properties "^1.1.3" -is-npm@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-1.0.0.tgz#f2fb63a65e4905b406c86072765a1a4dc793b9f4" - integrity sha1-8vtjpl5JBbQGyGBydloaTceTufQ= +is-natural-number@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8" + integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg= + +is-npm@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-npm/-/is-npm-4.0.0.tgz#c90dd8380696df87a7a6d823c20d0b12bbe3c84d" + integrity sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig== + +is-number-object@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.4.tgz#36ac95e741cf18b283fc1ddf5e83da798e3ec197" + integrity sha512-zohwelOAur+5uXtk8O3GPQ1eAcu4ZX3UwxQhUlfFFMNpUd83gXgjbhJh6HmB6LUNV/ieOLQuDwJO3dWJosUeMw== is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + integrity sha1-Afy7s5NGOlSPL0ZszhbezknbkI8= dependencies: kind-of "^3.0.2" is-number@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-3.0.0.tgz#24fd6201a4782cf50561c810276afc7d12d71195" + integrity sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU= dependencies: kind-of "^3.0.2" is-number@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + integrity sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ== -is-obj@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== -is-path-inside@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.1.tgz#8ef5b7de50437a3fdca6b4e865ef7aa55cb48036" - integrity sha1-jvW33lBDej/cprToZe96pVy0gDY= - dependencies: - path-is-inside "^1.0.1" +is-obj@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" + integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== + +is-path-inside@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.2.tgz#f5220fc82a3e233757291dddc9c5877f2a1f3017" + integrity sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg== is-plain-obj@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= -is-plain-object@^2.0.1, is-plain-object@^2.0.3, is-plain-object@^2.0.4: +is-plain-object@^2.0.3, is-plain-object@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== dependencies: isobject "^3.0.1" @@ -4377,35 +5659,34 @@ is-plain-object@^3.0.0: is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + integrity sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q= is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + integrity sha1-IHurkWOEmcB7Kt8kCkGochADRXU= is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= -is-redirect@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" - integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" +is-regex@^1.0.4, is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== dependencies: - has "^1.0.1" + has "^1.0.3" is-regular-file@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/is-regular-file/-/is-regular-file-1.1.1.tgz#ffcf9cae56ec63bc55b17d6fed1af441986dab66" integrity sha512-+1U3MZrVwC4HM6VUKk3L5fiHtNd2d9kayzEJhmQ+B+uIBPE/p8Fy8QVdkx0HIr3o9J5TOKJY40eI5GfTfBqbdA== -is-retry-allowed@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" - integrity sha1-EaBgVotnM5REAz0BJaYaINVk+zQ= +is-set@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" + integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== is-ssh@^1.3.0: version "1.3.1" @@ -4414,69 +5695,111 @@ is-ssh@^1.3.0: dependencies: protocols "^1.1.0" -is-stream-ended@^0.1.0, is-stream-ended@^0.1.4: +is-stream-ended@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/is-stream-ended/-/is-stream-ended-0.1.4.tgz#f50224e95e06bce0e356d440a4827cd35b267eda" + integrity sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw== -is-stream@^1.0.0, is-stream@^1.1.0: +is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" + integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= + +is-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.0.tgz#bde9c32680d6fae04129d6ac9d921ce7815f78e3" + integrity sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw== + +is-string@^1.0.4, is-string@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" + integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== dependencies: - has-symbols "^1.0.0" + has-symbols "^1.0.1" -is-text-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-2.0.0.tgz#b2484e2b720a633feb2e85b67dc193ff72c75636" - integrity sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw== +is-text-path@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-text-path/-/is-text-path-1.0.1.tgz#4e1aa0fb51bfbcb3e92688001397202c1775b66e" + integrity sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4= dependencies: - text-extensions "^2.0.0" + text-extensions "^1.0.0" -is-typedarray@~1.0.0: +is-typedarray@^1.0.0, is-typedarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= is-utf8@^0.2.0: version "0.2.1" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" + integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= + +is-weakmap@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.1.tgz#5008b59bdc43b698201d18f62b37b2ca243e8cf2" + integrity sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA== + +is-weakset@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.1.tgz#e9a0af88dbd751589f5e50d80f4c98b780884f83" + integrity sha512-pi4vhbhVHGLxohUw7PhGsueT4vRGFoXhP7+RGN0jKIv9+8PWYCQTqtADngrxOm2g46hoH0+g8uZZBzMrvVGDmw== is-windows@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" + integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== is-wsl@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-1.1.0.tgz#1f16e4aa22b04d1336b66188a66af3c600c3a66d" integrity sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0= -is@^3.0.1, is@^3.2.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/is/-/is-3.3.0.tgz#61cff6dd3c4193db94a3d62582072b44e5645d79" +is-yarn-global@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/is-yarn-global/-/is-yarn-global-0.3.0.tgz#d502d3382590ea3004893746754c89139973e232" + integrity sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw== isarray@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8= isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isarray@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.1.tgz#a37d94ed9cda2d59865c9f76fe596ee1f338741e" + integrity sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4= + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== isexe@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= isobject@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= dependencies: isarray "1.0.0" isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= isobject@^4.0.0: version "4.0.0" @@ -4486,36 +5809,22 @@ isobject@^4.0.0: isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= -istanbul-api@^1.1.1: - version "1.3.7" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.3.7.tgz#a86c770d2b03e11e3f778cd7aedd82d2722092aa" - dependencies: - async "^2.1.4" - fileset "^2.0.2" - istanbul-lib-coverage "^1.2.1" - istanbul-lib-hook "^1.2.2" - istanbul-lib-instrument "^1.10.2" - istanbul-lib-report "^1.1.5" - istanbul-lib-source-maps "^1.2.6" - istanbul-reports "^1.5.1" - js-yaml "^3.7.0" - mkdirp "^0.5.1" - once "^1.4.0" - -istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.2.1: +istanbul-lib-coverage@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.1.tgz#ccf7edcd0a0bb9b8f729feeb0930470f9af664f0" + integrity sha512-PzITeunAgyGbtY1ibVIUiV679EFChHjoMNRibEIobvmrCRaIgwLxNucOSimtNWUhEib/oO7QY2imD75JVgCJWQ== -istanbul-lib-hook@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-hook/-/istanbul-lib-hook-1.2.2.tgz#bc6bf07f12a641fbf1c85391d0daa8f0aea6bf86" - dependencies: - append-transform "^0.4.0" +istanbul-lib-coverage@^2.0.2, istanbul-lib-coverage@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz#675f0ab69503fad4b1d849f736baaca803344f49" + integrity sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA== -istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2, istanbul-lib-instrument@^1.4.2: +istanbul-lib-instrument@^1.10.1: version "1.10.2" resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.2.tgz#1f55ed10ac3c47f2bdddd5307935126754d0a9ca" + integrity sha512-aWHxfxDqvh/ZlxR8BBaEPVSWDPUkGD63VjGQn3jcw8jCp7sHEMKcrj4xfJn/ABzdMEHiQNyvDQhqm5o8+SQg7A== dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" @@ -4525,94 +5834,83 @@ istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.10.2, istanbul-lib-i istanbul-lib-coverage "^1.2.1" semver "^5.3.0" -istanbul-lib-report@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.5.tgz#f2a657fc6282f96170aaf281eb30a458f7f4170c" +istanbul-lib-instrument@^3.0.1, istanbul-lib-instrument@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz#a5f63d91f0bbc0c3e479ef4c5de027335ec6d630" + integrity sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA== + dependencies: + "@babel/generator" "^7.4.0" + "@babel/parser" "^7.4.3" + "@babel/template" "^7.4.0" + "@babel/traverse" "^7.4.3" + "@babel/types" "^7.4.0" + istanbul-lib-coverage "^2.0.5" + semver "^6.0.0" + +istanbul-lib-report@^2.0.4: + version "2.0.8" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz#5a8113cd746d43c4889eba36ab10e7d50c9b4f33" + integrity sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ== dependencies: - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - path-parse "^1.0.5" - supports-color "^3.1.2" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + supports-color "^6.1.0" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.6.tgz#37b9ff661580f8fca11232752ee42e08c6675d8f" +istanbul-lib-source-maps@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz#284997c48211752ec486253da97e3879defba8c8" + integrity sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw== dependencies: - debug "^3.1.0" - istanbul-lib-coverage "^1.2.1" - mkdirp "^0.5.1" - rimraf "^2.6.1" - source-map "^0.5.3" + debug "^4.1.1" + istanbul-lib-coverage "^2.0.5" + make-dir "^2.1.0" + rimraf "^2.6.3" + source-map "^0.6.1" -istanbul-reports@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.5.1.tgz#97e4dbf3b515e8c484caea15d6524eebd3ff4e1a" +istanbul-reports@^2.2.6: + version "2.2.7" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-2.2.7.tgz#5d939f6237d7b48393cc0959eab40cd4fd056931" + integrity sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg== dependencies: - handlebars "^4.0.3" + html-escaper "^2.0.0" iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.2.2.tgz#92d70deb8028e0c39ff3164fdbf4d8b088130cd7" + version "1.3.0" + resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" + integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== -jest-changed-files@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-21.2.0.tgz#5dbeecad42f5d88b482334902ce1cba6d9798d29" +jest-changed-files@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-24.9.0.tgz#08d8c15eb79a7fa3fc98269bc14b451ee82f8039" + integrity sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg== dependencies: + "@jest/types" "^24.9.0" + execa "^1.0.0" throat "^4.0.0" -jest-cli@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-21.2.1.tgz#9c528b6629d651911138d228bdb033c157ec8c00" - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.1" - glob "^7.1.2" - graceful-fs "^4.1.11" - is-ci "^1.0.10" - istanbul-api "^1.1.1" - istanbul-lib-coverage "^1.0.1" - istanbul-lib-instrument "^1.4.2" - istanbul-lib-source-maps "^1.1.0" - jest-changed-files "^21.2.0" - jest-config "^21.2.1" - jest-environment-jsdom "^21.2.1" - jest-haste-map "^21.2.0" - jest-message-util "^21.2.1" - jest-regex-util "^21.2.0" - jest-resolve-dependencies "^21.2.0" - jest-runner "^21.2.1" - jest-runtime "^21.2.1" - jest-snapshot "^21.2.1" - jest-util "^21.2.1" - micromatch "^2.3.11" - node-notifier "^5.0.2" - pify "^3.0.0" - slash "^1.0.0" - string-length "^2.0.0" - strip-ansi "^4.0.0" - which "^1.2.12" - worker-farm "^1.3.1" - yargs "^9.0.0" - -jest-config@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-21.2.1.tgz#c7586c79ead0bcc1f38c401e55f964f13bf2a480" +jest-cli@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-24.9.0.tgz#ad2de62d07472d419c6abc301fc432b98b10d2af" + integrity sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg== dependencies: + "@jest/core" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" chalk "^2.0.1" - glob "^7.1.1" - jest-environment-jsdom "^21.2.1" - jest-environment-node "^21.2.1" - jest-get-type "^21.2.0" - jest-jasmine2 "^21.2.1" - jest-regex-util "^21.2.0" - jest-resolve "^21.2.0" - jest-util "^21.2.1" - jest-validate "^21.2.1" - pretty-format "^21.2.1" + exit "^0.1.2" + import-local "^2.0.0" + is-ci "^2.0.0" + jest-config "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + prompts "^2.0.1" + realpath-native "^1.1.0" + yargs "^13.3.0" jest-config@^22.4.3, jest-config@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-22.4.4.tgz#72a521188720597169cd8b4ff86934ef5752d86a" + integrity sha512-9CKfo1GC4zrXSoMLcNeDvQBfgtqGTB1uP8iDIZ97oB26RCUb886KkKWhVcpyxVDOUxbhN+uzcBCeFe7w+Iem4A== dependencies: chalk "^2.0.1" glob "^7.1.1" @@ -4626,93 +5924,140 @@ jest-config@^22.4.3, jest-config@^22.4.4: jest-validate "^22.4.4" pretty-format "^22.4.0" -jest-diff@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-21.2.1.tgz#46cccb6cab2d02ce98bc314011764bb95b065b4f" +jest-config@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-24.9.0.tgz#fb1bbc60c73a46af03590719efa4825e6e4dd1b5" + integrity sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ== dependencies: + "@babel/core" "^7.1.0" + "@jest/test-sequencer" "^24.9.0" + "@jest/types" "^24.9.0" + babel-jest "^24.9.0" chalk "^2.0.1" - diff "^3.2.0" - jest-get-type "^21.2.0" - pretty-format "^21.2.1" + glob "^7.1.1" + jest-environment-jsdom "^24.9.0" + jest-environment-node "^24.9.0" + jest-get-type "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + micromatch "^3.1.10" + pretty-format "^24.9.0" + realpath-native "^1.1.0" jest-diff@^22.4.0, jest-diff@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-22.4.3.tgz#e18cc3feff0aeef159d02310f2686d4065378030" + integrity sha512-/QqGvCDP5oZOF6PebDuLwrB2BMD8ffJv6TAGAdEVuDx1+uEgrHpSFrfrOiMRx2eJ1hgNjlQrOQEHetVwij90KA== dependencies: chalk "^2.0.1" diff "^3.2.0" jest-get-type "^22.4.3" pretty-format "^22.4.3" -jest-docblock@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-21.2.0.tgz#51529c3b30d5fd159da60c27ceedc195faf8d414" +jest-diff@^24.3.0, jest-diff@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-24.9.0.tgz#931b7d0d5778a1baf7452cb816e325e3724055da" + integrity sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ== + dependencies: + chalk "^2.0.1" + diff-sequences "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" + +jest-docblock@^24.3.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-24.9.0.tgz#7970201802ba560e1c4092cc25cbedf5af5a8ce2" + integrity sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA== + dependencies: + detect-newline "^2.1.0" -jest-environment-jsdom@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-21.2.1.tgz#38d9980c8259b2a608ec232deee6289a60d9d5b4" +jest-each@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-24.9.0.tgz#eb2da602e2a610898dbc5f1f6df3ba86b55f8b05" + integrity sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog== dependencies: - jest-mock "^21.2.0" - jest-util "^21.2.1" - jsdom "^9.12.0" + "@jest/types" "^24.9.0" + chalk "^2.0.1" + jest-get-type "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" jest-environment-jsdom@^22.4.1: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-22.4.3.tgz#d67daa4155e33516aecdd35afd82d4abf0fa8a1e" + integrity sha512-FviwfR+VyT3Datf13+ULjIMO5CSeajlayhhYQwpzgunswoaLIPutdbrnfUHEMyJCwvqQFaVtTmn9+Y8WCt6n1w== dependencies: jest-mock "^22.4.3" jest-util "^22.4.3" jsdom "^11.5.1" -jest-environment-node@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-21.2.1.tgz#98c67df5663c7fbe20f6e792ac2272c740d3b8c8" +jest-environment-jsdom@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz#4b0806c7fc94f95edb369a69cc2778eec2b7375b" + integrity sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA== dependencies: - jest-mock "^21.2.0" - jest-util "^21.2.1" + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" + jsdom "^11.5.1" jest-environment-node@^22.4.1: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-22.4.3.tgz#54c4eaa374c83dd52a9da8759be14ebe1d0b9129" + integrity sha512-reZl8XF6t/lMEuPWwo9OLfttyC26A5AMgDyEQ6DBgZuyfyeNUzYT8BFo6uxCCP/Av/b7eb9fTi3sIHFPBzmlRA== dependencies: jest-mock "^22.4.3" jest-util "^22.4.3" -jest-get-type@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" +jest-environment-node@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-24.9.0.tgz#333d2d2796f9687f2aeebf0742b519f33c1cbfd3" + integrity sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA== + dependencies: + "@jest/environment" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/types" "^24.9.0" + jest-mock "^24.9.0" + jest-util "^24.9.0" -jest-get-type@^22.1.0, jest-get-type@^22.3.3, jest-get-type@^22.4.3: +jest-get-type@^22.1.0, jest-get-type@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" + integrity sha512-/jsz0Y+V29w1chdXVygEKSz2nBoHoYqNShPe+QgxSNjAuP1i8+k4LbQNrfoliKej0P45sivkSCh7yiD6ubHS3w== -jest-haste-map@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-21.2.0.tgz#1363f0a8bb4338f24f001806571eff7a4b2ff3d8" - dependencies: - fb-watchman "^2.0.0" - graceful-fs "^4.1.11" - jest-docblock "^21.2.0" - micromatch "^2.3.11" - sane "^2.0.0" - worker-farm "^1.3.1" +jest-get-type@^24.3.0, jest-get-type@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" + integrity sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q== -jest-jasmine2@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-21.2.1.tgz#9cc6fc108accfa97efebce10c4308548a4ea7592" +jest-haste-map@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-24.9.0.tgz#b38a5d64274934e21fa417ae9a9fbeb77ceaac7d" + integrity sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ== dependencies: - chalk "^2.0.1" - expect "^21.2.1" - graceful-fs "^4.1.11" - jest-diff "^21.2.1" - jest-matcher-utils "^21.2.1" - jest-message-util "^21.2.1" - jest-snapshot "^21.2.1" - p-cancelable "^0.3.0" + "@jest/types" "^24.9.0" + anymatch "^2.0.0" + fb-watchman "^2.0.0" + graceful-fs "^4.1.15" + invariant "^2.2.4" + jest-serializer "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.9.0" + micromatch "^3.1.10" + sane "^4.0.3" + walker "^1.0.7" + optionalDependencies: + fsevents "^1.2.7" jest-jasmine2@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-22.4.4.tgz#c55f92c961a141f693f869f5f081a79a10d24e23" + integrity sha512-nK3vdUl50MuH7vj/8at7EQVjPGWCi3d5+6aCi7Gxy/XMWdOdbH1qtO/LjKbqD8+8dUAEH+BVVh7HkjpCWC1CSw== dependencies: chalk "^2.0.1" co "^4.6.0" @@ -4726,41 +6071,59 @@ jest-jasmine2@^22.4.4: jest-util "^22.4.1" source-map-support "^0.5.0" -jest-matcher-utils@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-21.2.1.tgz#72c826eaba41a093ac2b4565f865eb8475de0f64" +jest-jasmine2@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz#1f7b1bd3242c1774e62acabb3646d96afc3be6a0" + integrity sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw== dependencies: + "@babel/traverse" "^7.1.0" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" chalk "^2.0.1" - jest-get-type "^21.2.0" - pretty-format "^21.2.1" + co "^4.6.0" + expect "^24.9.0" + is-generator-fn "^2.0.0" + jest-each "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-runtime "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + pretty-format "^24.9.0" + throat "^4.0.0" + +jest-leak-detector@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz#b665dea7c77100c5c4f7dfcb153b65cf07dcf96a" + integrity sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA== + dependencies: + jest-get-type "^24.9.0" + pretty-format "^24.9.0" jest-matcher-utils@^22.4.0, jest-matcher-utils@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-22.4.3.tgz#4632fe428ebc73ebc194d3c7b65d37b161f710ff" + integrity sha512-lsEHVaTnKzdAPR5t4B6OcxXo9Vy4K+kRRbG5gtddY8lBEC+Mlpvm1CJcsMESRjzUhzkz568exMV1hTB76nAKbA== dependencies: chalk "^2.0.1" jest-get-type "^22.4.3" pretty-format "^22.4.3" -jest-matcher-utils@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-23.6.0.tgz#726bcea0c5294261a7417afb6da3186b4b8cac80" - dependencies: - chalk "^2.0.1" - jest-get-type "^22.1.0" - pretty-format "^23.6.0" - -jest-message-util@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-21.2.1.tgz#bfe5d4692c84c827d1dcf41823795558f0a1acbe" +jest-matcher-utils@^24.7.0, jest-matcher-utils@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz#f5b3661d5e628dffe6dd65251dfdae0e87c3a073" + integrity sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA== dependencies: chalk "^2.0.1" - micromatch "^2.3.11" - slash "^1.0.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + pretty-format "^24.9.0" jest-message-util@^22.4.0, jest-message-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-22.4.3.tgz#cf3d38aafe4befddbfc455e57d65d5239e399eb7" + integrity sha512-iAMeKxhB3Se5xkSjU0NndLLCHtP4n+GtCqV0bISKA5dmOXQfEbdEmYiu2qpnWBDCQdEafNDDU6Q+l6oBMd/+BA== dependencies: "@babel/code-frame" "^7.0.0-beta.35" chalk "^2.0.1" @@ -4768,94 +6131,138 @@ jest-message-util@^22.4.0, jest-message-util@^22.4.3: slash "^1.0.0" stack-utils "^1.0.1" -jest-mock@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-21.2.0.tgz#7eb0770e7317968165f61ea2a7281131534b3c0f" +jest-message-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-24.9.0.tgz#527f54a1e380f5e202a8d1149b0ec872f43119e3" + integrity sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw== + dependencies: + "@babel/code-frame" "^7.0.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/stack-utils" "^1.0.1" + chalk "^2.0.1" + micromatch "^3.1.10" + slash "^2.0.0" + stack-utils "^1.0.1" jest-mock@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-22.4.3.tgz#f63ba2f07a1511772cdc7979733397df770aabc7" + integrity sha512-+4R6mH5M1G4NK16CKg9N1DtCaFmuxhcIqF4lQK/Q1CIotqMs/XBemfpDPeVZBFow6iyUNu6EBT9ugdNOTT5o5Q== + +jest-mock@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-24.9.0.tgz#c22835541ee379b908673ad51087a2185c13f1c6" + integrity sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w== + dependencies: + "@jest/types" "^24.9.0" -jest-regex-util@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-21.2.0.tgz#1b1e33e63143babc3e0f2e6c9b5ba1eb34b2d530" +jest-pnp-resolver@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz#ecdae604c077a7fbc70defb6d517c3c1c898923a" + integrity sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ== jest-regex-util@^22.1.0, jest-regex-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-22.4.3.tgz#a826eb191cdf22502198c5401a1fc04de9cef5af" + integrity sha512-LFg1gWr3QinIjb8j833bq7jtQopiwdAs67OGfkPrvy7uNUbVMfTXXcOKXJaeY5GgjobELkKvKENqq1xrUectWg== -jest-resolve-dependencies@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-21.2.0.tgz#9e231e371e1a736a1ad4e4b9a843bc72bfe03d09" - dependencies: - jest-regex-util "^21.2.0" +jest-regex-util@^24.3.0, jest-regex-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-24.9.0.tgz#c13fb3380bde22bf6575432c493ea8fe37965636" + integrity sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA== -jest-resolve@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-21.2.0.tgz#068913ad2ba6a20218e5fd32471f3874005de3a6" +jest-resolve-dependencies@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz#ad055198959c4cfba8a4f066c673a3f0786507ab" + integrity sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g== dependencies: - browser-resolve "^1.11.2" - chalk "^2.0.1" - is-builtin-module "^1.0.0" + "@jest/types" "^24.9.0" + jest-regex-util "^24.3.0" + jest-snapshot "^24.9.0" jest-resolve@^22.4.2: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-22.4.3.tgz#0ce9d438c8438229aa9b916968ec6b05c1abb4ea" + integrity sha512-u3BkD/MQBmwrOJDzDIaxpyqTxYH+XqAXzVJP51gt29H8jpj3QgKof5GGO2uPGKGeA1yTMlpbMs1gIQ6U4vcRhw== dependencies: browser-resolve "^1.11.2" chalk "^2.0.1" -jest-runner@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-21.2.1.tgz#194732e3e518bfb3d7cbfc0fd5871246c7e1a467" - dependencies: - jest-config "^21.2.1" - jest-docblock "^21.2.0" - jest-haste-map "^21.2.0" - jest-jasmine2 "^21.2.1" - jest-message-util "^21.2.1" - jest-runtime "^21.2.1" - jest-util "^21.2.1" - pify "^3.0.0" - throat "^4.0.0" - worker-farm "^1.3.1" - -jest-runtime@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-21.2.1.tgz#99dce15309c670442eee2ebe1ff53a3cbdbbb73e" +jest-resolve@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-24.9.0.tgz#dff04c7687af34c4dd7e524892d9cf77e5d17321" + integrity sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ== dependencies: - babel-core "^6.0.0" - babel-jest "^21.2.0" - babel-plugin-istanbul "^4.0.0" + "@jest/types" "^24.9.0" + browser-resolve "^1.11.3" chalk "^2.0.1" - convert-source-map "^1.4.0" - graceful-fs "^4.1.11" - jest-config "^21.2.1" - jest-haste-map "^21.2.0" - jest-regex-util "^21.2.0" - jest-resolve "^21.2.0" - jest-util "^21.2.1" - json-stable-stringify "^1.0.1" - micromatch "^2.3.11" - slash "^1.0.0" - strip-bom "3.0.0" - write-file-atomic "^2.1.0" - yargs "^9.0.0" + jest-pnp-resolver "^1.2.1" + realpath-native "^1.1.0" + +jest-runner@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-24.9.0.tgz#574fafdbd54455c2b34b4bdf4365a23857fcdf42" + integrity sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + chalk "^2.4.2" + exit "^0.1.2" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-docblock "^24.3.0" + jest-haste-map "^24.9.0" + jest-jasmine2 "^24.9.0" + jest-leak-detector "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" + jest-runtime "^24.9.0" + jest-util "^24.9.0" + jest-worker "^24.6.0" + source-map-support "^0.5.6" + throat "^4.0.0" -jest-snapshot@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-21.2.1.tgz#29e49f16202416e47343e757e5eff948c07fd7b0" - dependencies: +jest-runtime@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-24.9.0.tgz#9f14583af6a4f7314a6a9d9f0226e1a781c8e4ac" + integrity sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw== + dependencies: + "@jest/console" "^24.7.1" + "@jest/environment" "^24.9.0" + "@jest/source-map" "^24.3.0" + "@jest/transform" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" chalk "^2.0.1" - jest-diff "^21.2.1" - jest-matcher-utils "^21.2.1" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - pretty-format "^21.2.1" + exit "^0.1.2" + glob "^7.1.3" + graceful-fs "^4.1.15" + jest-config "^24.9.0" + jest-haste-map "^24.9.0" + jest-message-util "^24.9.0" + jest-mock "^24.9.0" + jest-regex-util "^24.3.0" + jest-resolve "^24.9.0" + jest-snapshot "^24.9.0" + jest-util "^24.9.0" + jest-validate "^24.9.0" + realpath-native "^1.1.0" + slash "^2.0.0" + strip-bom "^3.0.0" + yargs "^13.3.0" + +jest-serializer@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-24.9.0.tgz#e6d7d7ef96d31e8b9079a714754c5d5c58288e73" + integrity sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ== jest-snapshot@^22.4.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-22.4.3.tgz#b5c9b42846ffb9faccb76b841315ba67887362d2" + integrity sha512-JXA0gVs5YL0HtLDCGa9YxcmmV2LZbwJ+0MfyXBBc5qpgkEYITQFJP7XNhcHFbUvRiniRpRbGVfJrOoYhhGE0RQ== dependencies: chalk "^2.0.1" jest-diff "^22.4.3" @@ -4864,28 +6271,37 @@ jest-snapshot@^22.4.0: natural-compare "^1.4.0" pretty-format "^22.4.3" -jest-tobetype@^1.1.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/jest-tobetype/-/jest-tobetype-1.2.0.tgz#3bab33d22603e2774428a435ad835629917b230d" - dependencies: - jest-get-type "^22.3.3" - jest-matcher-utils "^23.6.0" - -jest-util@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-21.2.1.tgz#a274b2f726b0897494d694a6c3d6a61ab819bb78" +jest-snapshot@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-24.9.0.tgz#ec8e9ca4f2ec0c5c87ae8f925cf97497b0e951ba" + integrity sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew== dependencies: - callsites "^2.0.0" + "@babel/types" "^7.0.0" + "@jest/types" "^24.9.0" chalk "^2.0.1" - graceful-fs "^4.1.11" - jest-message-util "^21.2.1" - jest-mock "^21.2.0" - jest-validate "^21.2.1" + expect "^24.9.0" + jest-diff "^24.9.0" + jest-get-type "^24.9.0" + jest-matcher-utils "^24.9.0" + jest-message-util "^24.9.0" + jest-resolve "^24.9.0" mkdirp "^0.5.1" + natural-compare "^1.4.0" + pretty-format "^24.9.0" + semver "^6.2.0" + +jest-tobetype@^1.1.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/jest-tobetype/-/jest-tobetype-1.2.3.tgz#ae0d0e972a52039ae1317e80f5793e17d942a623" + integrity sha512-9wVkY9lDW4BhcUc0Hpc0hq7n1i/erZW59RbGMl+oAi4IKPi4YtaXHdJgQg0QMDPWtK5PiM82hw6jPbVjH61Z5A== + dependencies: + jest-get-type "^24.3.0" + jest-matcher-utils "^24.7.0" jest-util@^22.4.1, jest-util@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-22.4.3.tgz#c70fec8eec487c37b10b0809dc064a7ecf6aafac" + integrity sha512-rfDfG8wyC5pDPNdcnAlZgwKnzHvZDu8Td2NJI/jAGKEGxJPYiE4F0ss/gSAkG4778Y23Hvbz+0GMrDJTeo7RjQ== dependencies: callsites "^2.0.0" chalk "^2.0.1" @@ -4895,18 +6311,28 @@ jest-util@^22.4.1, jest-util@^22.4.3: mkdirp "^0.5.1" source-map "^0.6.0" -jest-validate@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" - dependencies: +jest-util@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-24.9.0.tgz#7396814e48536d2e85a37de3e4c431d7cb140162" + integrity sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg== + dependencies: + "@jest/console" "^24.9.0" + "@jest/fake-timers" "^24.9.0" + "@jest/source-map" "^24.9.0" + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + callsites "^3.0.0" chalk "^2.0.1" - jest-get-type "^21.2.0" - leven "^2.1.0" - pretty-format "^21.2.1" + graceful-fs "^4.1.15" + is-ci "^2.0.0" + mkdirp "^0.5.1" + slash "^2.0.0" + source-map "^0.6.0" jest-validate@^22.4.4: version "22.4.4" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-22.4.4.tgz#1dd0b616ef46c995de61810d85f57119dbbcec4d" + integrity sha512-dmlf4CIZRGvkaVg3fa0uetepcua44DHtktHm6rcoNVtYlpwe6fEJRkMFsaUVcFHLzbuBJ2cPw9Gl9TKfnzMVwg== dependencies: chalk "^2.0.1" jest-config "^22.4.4" @@ -4914,29 +6340,78 @@ jest-validate@^22.4.4: leven "^2.1.0" pretty-format "^22.4.0" -jest@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-21.2.1.tgz#c964e0b47383768a1438e3ccf3c3d470327604e1" +jest-validate@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-24.9.0.tgz#0775c55360d173cd854e40180756d4ff52def8ab" + integrity sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ== + dependencies: + "@jest/types" "^24.9.0" + camelcase "^5.3.1" + chalk "^2.0.1" + jest-get-type "^24.9.0" + leven "^3.1.0" + pretty-format "^24.9.0" + +jest-watcher@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-24.9.0.tgz#4b56e5d1ceff005f5b88e528dc9afc8dd4ed2b3b" + integrity sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw== + dependencies: + "@jest/test-result" "^24.9.0" + "@jest/types" "^24.9.0" + "@types/yargs" "^13.0.0" + ansi-escapes "^3.0.0" + chalk "^2.0.1" + jest-util "^24.9.0" + string-length "^2.0.0" + +jest-worker@^24.6.0, jest-worker@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-24.9.0.tgz#5dbfdb5b2d322e98567898238a9697bcce67b3e5" + integrity sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw== dependencies: - jest-cli "^21.2.1" + merge-stream "^2.0.0" + supports-color "^6.1.0" + +jest@^24.9: + version "24.9.0" + resolved "https://registry.yarnpkg.com/jest/-/jest-24.9.0.tgz#987d290c05a08b52c56188c1002e368edb007171" + integrity sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw== + dependencies: + import-local "^2.0.0" + jest-cli "^24.9.0" jmespath@0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.15.0.tgz#a3f222a9aae9f966f5d27c796510e28091764217" + integrity sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc= jquery@^3.1.0: - version "3.3.1" - resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.3.1.tgz#958ce29e81c9790f31be7792df5d4d95fc57fbca" + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +js-md5@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/js-md5/-/js-md5-0.7.3.tgz#b4f2fbb0b327455f598d6727e38ec272cd09c3f2" + integrity sha512-ZC41vPSTLKGwIRjqDh8DfXoCrdQIyBgspJVPXHBGu4nZlAEvG3nf+jO9avM9RmLiGakg7vz974ms99nEV0tmTQ== + +js-sha1@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/js-sha1/-/js-sha1-0.6.0.tgz#adbee10f0e8e18aa07cdea807cf08e9183dbc7f9" + integrity sha512-01gwBFreYydzmU9BmZxpVk6svJJHrVxEN3IOiGl6VO93bVKYETJ0sIth6DASI6mIFdt7NmfX9UiByRzsYHGU9w== "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-yaml@^3.13.0, js-yaml@^3.13.1: +js-yaml@^3.13.1, js-yaml@^3.4.3: version "3.13.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== @@ -4944,20 +6419,15 @@ js-yaml@^3.13.0, js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.4.3, js-yaml@^3.7.0: - version "3.12.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= jsdom@^11.5.1: version "11.12.0" resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.12.0.tgz#1a80d40ddd378a1de59656e9e6dc5a3ba8657bc8" + integrity sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw== dependencies: abab "^2.0.0" acorn "^5.5.3" @@ -4986,37 +6456,20 @@ jsdom@^11.5.1: ws "^5.2.0" xml-name-validator "^3.0.0" -jsdom@^9.12.0: - version "9.12.0" - resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-9.12.0.tgz#e8c546fffcb06c00d4833ca84410fed7f8a097d4" - dependencies: - abab "^1.0.3" - acorn "^4.0.4" - acorn-globals "^3.1.0" - array-equal "^1.0.0" - content-type-parser "^1.0.1" - cssom ">= 0.3.2 < 0.4.0" - cssstyle ">= 0.2.37 < 0.3.0" - escodegen "^1.6.1" - html-encoding-sniffer "^1.0.1" - nwmatcher ">= 1.3.9 < 2.0.0" - parse5 "^1.5.1" - request "^2.79.0" - sax "^1.2.1" - symbol-tree "^3.2.1" - tough-cookie "^2.3.2" - webidl-conversions "^4.0.0" - whatwg-encoding "^1.0.1" - whatwg-url "^4.3.0" - xml-name-validator "^2.0.1" - jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + integrity sha1-RsP+yMGJKxKwgz25vHYiF226s0s= + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== json-bigint@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-0.3.0.tgz#0ccd912c4b8270d05f056fbd13814b53d3825b1e" + integrity sha1-DM2RLEuCcNBfBW+9E4FLU9OCWx4= dependencies: bignumber.js "^7.0.0" @@ -5030,39 +6483,55 @@ json-parse-better-errors@^1.0.1: resolved "https://registry.yarnpkg.com/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" - -json-stable-stringify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz#9a759d39c5f2ff503fd5300646ed445f88c4f9af" - dependencies: - jsonify "~0.0.0" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +json2csv@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json2csv/-/json2csv-5.0.1.tgz#5a472b32f177954911c47d33c03a4940ac61eeab" + integrity sha512-QFMifUX1y8W2tKi2TwZpnzf2rHdZvzdmgZUMEMDF46F90f4a9mUeWfx/qg4kzXSZYJYc3cWA5O+eLXk5lj9g8g== + dependencies: + commander "^5.0.0" + jsonparse "^1.3.1" + lodash.get "^4.4.2" json5@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + integrity sha1-Hq3nrMASA0rYTiOWdn6tn6VJWCE= + +json5@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.1.1.tgz#81b6cb04e9ba496f1c7005d07b4368a2638f90b6" + integrity sha512-l+3HXD0GEI3huGq1njuqtzYK8OYJyXMkOLtQ53pjWh89tvWS2h6l+1zMkYWqlb57+SiQodKZyvMEFb2X+KrFhQ== + dependencies: + minimist "^1.2.0" jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" + integrity sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss= optionalDependencies: graceful-fs "^4.1.6" -jsonify@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" - -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= @@ -5070,6 +6539,7 @@ jsonparse@^1.2.0: jsonwebtoken@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.1.0.tgz#c6397cd2e5fd583d65c007a83dc7bb78e6982b83" + integrity sha1-xjl80uX9WD1lwAeoPce7eOaYK4M= dependencies: jws "^3.1.4" lodash.includes "^4.3.0" @@ -5083,10 +6553,11 @@ jsonwebtoken@8.1.0: xtend "^4.0.1" jsonwebtoken@^8.1.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.4.0.tgz#8757f7b4cb7440d86d5e2f3becefa70536c8e46a" + version "8.5.1" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d" + integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w== dependencies: - jws "^3.1.5" + jws "^3.2.2" lodash.includes "^4.3.0" lodash.isboolean "^3.0.3" lodash.isinteger "^4.0.4" @@ -5095,48 +6566,71 @@ jsonwebtoken@^8.1.0: lodash.isstring "^4.0.1" lodash.once "^4.0.0" ms "^2.1.1" + semver "^5.6.0" jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= dependencies: assert-plus "1.0.0" extsprintf "1.3.0" json-schema "0.2.3" verror "1.10.0" -jszip@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.1.5.tgz#e3c2a6c6d706ac6e603314036d43cd40beefdf37" +jszip@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.2.2.tgz#b143816df7e106a9597a94c77493385adca5bd1d" + integrity sha512-NmKajvAFQpbg3taXQXr/ccS2wcucR1AZ+NtyWp2Nq7HHVsXhcJFR8p0Baf32C2yVvBylFWVeKf+WI2AnvlPhpA== dependencies: - core-js "~2.3.0" - es6-promise "~3.0.2" - lie "~3.1.0" + lie "~3.3.0" pako "~1.0.2" - readable-stream "~2.0.6" + readable-stream "~2.3.6" + set-immediate-shim "~1.0.1" + +just-extend@^4.0.2: + version "4.1.0" + resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.1.0.tgz#7278a4027d889601640ee0ce0e5a00b992467da4" + integrity sha512-ApcjaOdVTJ7y4r08xI5wIqpvwS48Q0PBG4DJROcEkH1f8MdAiNFyFxz3xoL0LWAVwjrwPYZdVHHxhRHcx/uGLA== -just-extend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-4.0.2.tgz#f3f47f7dfca0f989c55410a7ebc8854b07108afc" +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" -jwa@^1.1.5: - version "1.1.6" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.1.6.tgz#87240e76c9808dbde18783cf2264ef4929ee50e6" +jwa@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-2.0.0.tgz#a7e9c3f29dae94027ebcaf49975c9345593410fc" + integrity sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA== dependencies: buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.10" + ecdsa-sig-formatter "1.0.11" safe-buffer "^5.0.1" -jws@^3.1.4, jws@^3.1.5: - version "3.1.5" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.1.5.tgz#80d12d05b293d1e841e7cb8b4e69e561adcf834f" +jws@^3.1.4, jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== dependencies: - jwa "^1.1.5" + jwa "^1.4.1" safe-buffer "^5.0.1" -kareem@2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.0.tgz#ef33c42e9024dce511eeaf440cd684f3af1fc769" +jws@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jws/-/jws-4.0.0.tgz#2d4e8cf6a318ffaa12615e9dec7e86e6c97310f4" + integrity sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg== + dependencies: + jwa "^2.0.0" + safe-buffer "^5.0.1" + +kareem@2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/kareem/-/kareem-2.3.1.tgz#def12d9c941017fabfb00f873af95e9c99e1be87" + integrity sha512-l3hLhffs9zqoDe8zjmb/mAN4B8VT3L56EUvKNqLFVs9YlFA+zx7ke1DO8STAdDyYNkeSo1nKmjuvQeI12So8Xw== keyv@^3.0.0: version "3.1.0" @@ -5145,75 +6639,88 @@ keyv@^3.0.0: dependencies: json-buffer "3.0.0" -kind-of@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5" - integrity sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU= - dependencies: - is-buffer "^1.0.2" - -kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0, kind-of@^3.2.2: +kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + integrity sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ= dependencies: is-buffer "^1.1.5" kind-of@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-4.0.0.tgz#20813df3d712928b207378691a45066fae72dd57" + integrity sha1-IIE989cSkosgc3hpGkUGb65y3Vc= dependencies: is-buffer "^1.1.5" kind-of@^5.0.0: version "5.1.0" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-5.1.0.tgz#729c91e2d857b7a419a1f9aa65685c4c33f5845d" + integrity sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw== -kind-of@^6.0.0, kind-of@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" +kind-of@^6.0.0, kind-of@^6.0.2, kind-of@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== -latest-version@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" - integrity sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU= - dependencies: - package-json "^4.0.0" +kleur@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" + integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lazy-cache@^0.2.3: - version "0.2.7" - resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-0.2.7.tgz#7feddf2dcb6edb77d11ef1d117ab5ffdf0ab1b65" - integrity sha1-f+3fLctu23fRHvHRF6tf/fCrG2U= +latest-version@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-5.1.0.tgz#119dfe908fe38d15dfa43ecd13fa12ec8832face" + integrity sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA== + dependencies: + package-json "^6.3.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" +lcid@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" + integrity sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA== dependencies: - invert-kv "^1.0.0" + invert-kv "^2.0.0" left-pad@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" + integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== leven@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" + integrity sha1-wuep93IJTe6dNCAq6KzORoeHVYA= + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== levn@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= dependencies: prelude-ls "~1.1.2" type-check "~0.3.2" -lie@~3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e" +lie@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a" + integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ== dependencies: immediate "~3.0.5" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + lint-staged@^3.6.0: version "3.6.1" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-3.6.1.tgz#24423c8b7bd99d96e15acd1ac8cb392a78e58582" + integrity sha1-JEI8i3vZnZbhWs0ayMs5KnjlhYI= dependencies: app-root-path "^2.0.0" cosmiconfig "^1.1.0" @@ -5225,13 +6732,20 @@ lint-staged@^3.6.0: p-map "^1.1.1" staged-git-files "0.0.4" +listenercount@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/listenercount/-/listenercount-1.0.1.tgz#84c8a72ab59c4725321480c975e6508342e70937" + integrity sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc= + listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= listr-update-renderer@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.2.0.tgz#ca80e1779b4e70266807e8eed1ad6abe398550f9" + integrity sha1-yoDhd5tOcCZoB+ju0a1qvjmFUPk= dependencies: chalk "^1.1.3" cli-truncate "^0.2.1" @@ -5245,6 +6759,7 @@ listr-update-renderer@^0.2.0: listr-verbose-renderer@^0.4.0: version "0.4.1" resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" + integrity sha1-ggb0z21S3cWCfl/RSYng6WWTOjU= dependencies: chalk "^1.1.3" cli-cursor "^1.0.2" @@ -5254,6 +6769,7 @@ listr-verbose-renderer@^0.4.0: listr@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/listr/-/listr-0.12.0.tgz#6bce2c0f5603fa49580ea17cd6a00cc0e5fa451a" + integrity sha1-a84sD1YD+klYDqF81qAMwOX6RRo= dependencies: chalk "^1.1.3" cli-truncate "^0.2.1" @@ -5275,6 +6791,7 @@ listr@^0.12.0: load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" + integrity sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA= dependencies: graceful-fs "^4.1.2" parse-json "^2.2.0" @@ -5282,15 +6799,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -5304,6 +6812,7 @@ load-json-file@^4.0.0: locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= dependencies: p-locate "^2.0.0" path-exists "^3.0.0" @@ -5316,49 +6825,21 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -lodash._arraycopy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arraycopy/-/lodash._arraycopy-3.0.0.tgz#76e7b7c1f1fb92547374878a562ed06a3e50f6e1" - -lodash._arrayeach@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._arrayeach/-/lodash._arrayeach-3.0.0.tgz#bab156b2a90d3f1bbd5c653403349e5e5933ef9e" - -lodash._baseassign@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz#8c38a099500f215ad09e59f1722fd0c52bfe0a4e" +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== dependencies: - lodash._basecopy "^3.0.0" - lodash.keys "^3.0.0" + p-locate "^4.1.0" -lodash._baseclone@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/lodash._baseclone/-/lodash._baseclone-3.3.0.tgz#303519bf6393fe7e42f34d8b630ef7794e3542b7" +lockfile@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/lockfile/-/lockfile-1.0.4.tgz#07f819d25ae48f87e538e6578b6964a4981a5609" + integrity sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA== dependencies: - lodash._arraycopy "^3.0.0" - lodash._arrayeach "^3.0.0" - lodash._baseassign "^3.0.0" - lodash._basefor "^3.0.0" - lodash.isarray "^3.0.0" - lodash.keys "^3.0.0" - -lodash._basecopy@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz#8da0e6a876cf344c0ad8a54882111dd3c5c7ca36" - -lodash._basefor@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/lodash._basefor/-/lodash._basefor-3.0.3.tgz#7550b4e9218ef09fad24343b612021c79b4c20c2" - -lodash._bindcallback@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e" - -lodash._getnative@^3.0.0: - version "3.9.1" - resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" + signal-exit "^3.0.2" -lodash._reinterpolate@~3.0.0: +lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= @@ -5366,50 +6847,47 @@ lodash._reinterpolate@~3.0.0: lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" - -lodash.assignin@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" - integrity sha1-uo31+4QesKPoBEIysOJjqNxqKKI= + integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= lodash.at@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.at/-/lodash.at-4.6.0.tgz#93cdce664f0a1994ea33dd7cd40e23afd11b0ff8" + integrity sha1-k83OZk8KGZTqM9181A4jr9EbD/g= lodash.bind@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" + integrity sha1-euMBfpOWIqwxt9fX3LGzTbFpDTU= lodash.camelcase@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" + integrity sha1-soqmKIorn8ZRA1x3EfZathkDMaY= lodash.chunk@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.chunk/-/lodash.chunk-4.2.0.tgz#66e5ce1f76ed27b4303d8c6512e8d1216e8106bc" + integrity sha1-ZuXOH3btJ7QwPYxlEujRIW6BBrw= lodash.clone@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clone/-/lodash.clone-4.5.0.tgz#195870450f5a13192478df4bc3d23d2dea1907b6" + integrity sha1-GVhwRQ9aExkkeN9Lw9I9LeoZB7Y= -lodash.clonedeep@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-3.0.2.tgz#a0a1e40d82a5ea89ff5b147b8444ed63d92827db" - dependencies: - lodash._baseclone "^3.0.0" - lodash._bindcallback "^3.0.0" - -lodash.clonedeep@^4.3.0, lodash.clonedeep@^4.5.0: +lodash.clonedeep@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef" + integrity sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8= lodash.defaults@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw= lodash.difference@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" + integrity sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw= lodash.find@^4.6.0: version "4.6.0" @@ -5419,42 +6897,42 @@ lodash.find@^4.6.0: lodash.flatten@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" + integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8= lodash.foreach@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" + integrity sha1-Gmo16s5AEoDH8G3d7DUWWrJ+PlM= -lodash.get@4.4.2, lodash.get@^4.4.2: +lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" + integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= lodash.has@^4.5.2: version "4.5.2" resolved "https://registry.yarnpkg.com/lodash.has/-/lodash.has-4.5.2.tgz#d19f4dc1095058cccbe2b0cdf4ee0fe4aa37c862" + integrity sha1-0Z9NwQlQWMzL4rDN9O4P5Ko3yGI= lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" - -lodash.isarguments@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - -lodash.isarray@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" + integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8= lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" + integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY= lodash.isempty@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" + integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4= lodash.isinteger@^4.0.4: version "4.0.4" resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343" + integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M= lodash.ismatch@^4.4.0: version "4.4.0" @@ -5464,50 +6942,47 @@ lodash.ismatch@^4.4.0: lodash.isnumber@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc" + integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w= lodash.isplainobject@^4.0.6: version "4.0.6" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" + integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs= lodash.isstring@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451" - -lodash.keys@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" - dependencies: - lodash._getnative "^3.0.0" - lodash.isarguments "^3.0.0" - lodash.isarray "^3.0.0" + integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE= lodash.keys@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-4.2.0.tgz#a08602ac12e4fb83f91fc1fb7a360a4d9ba35205" - -lodash.merge@^4.6.0, lodash.merge@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" + integrity sha1-oIYCrBLk+4P5H8H7ejYKTZujUgU= lodash.noop@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/lodash.noop/-/lodash.noop-3.0.1.tgz#38188f4d650a3a474258439b96ec45b32617133c" + integrity sha1-OBiPTWUKOkdCWEObluxFsyYXEzw= lodash.once@^4.0.0: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac" + integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w= lodash.partial@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.partial/-/lodash.partial-4.2.1.tgz#49f3d8cfdaa3bff8b3a91d127e923245418961d4" + integrity sha1-SfPYz9qjv/izqR0SfpIyRUGJYdQ= lodash.pick@^4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= lodash.sample@^4.2.1: version "4.2.1" resolved "https://registry.yarnpkg.com/lodash.sample/-/lodash.sample-4.2.1.tgz#5e4291b0c753fa1abeb0aab8fb29df1b66f07f6d" + integrity sha1-XkKRsMdT+hq+sKq4+ynfG2bwf20= lodash.set@^4.3.2: version "4.3.2" @@ -5517,33 +6992,32 @@ lodash.set@^4.3.2: lodash.shuffle@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.shuffle/-/lodash.shuffle-4.2.0.tgz#145b5053cf875f6f5c2a33f48b6e9948c6ec7b4b" + integrity sha1-FFtQU8+HX29cKjP0i26ZSMbse0s= lodash.snakecase@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d" + integrity sha1-OdcUo1NXFHg3rv1ktdy7Fr7Nj40= lodash.sortby@^4.7.0: version "4.7.0" resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= lodash.template@^4.0.2: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" - integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= + version "4.5.0" + resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" + integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== dependencies: - lodash._reinterpolate "~3.0.0" + lodash._reinterpolate "^3.0.0" lodash.templatesettings "^4.0.0" lodash.templatesettings@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" - integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== dependencies: - lodash._reinterpolate "~3.0.0" - -lodash.toarray@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.toarray/-/lodash.toarray-4.4.0.tgz#24c4bfcd6b2fba38bfd0594db1179d8e9b656561" + lodash._reinterpolate "^3.0.0" lodash.uniq@^4.5.0: version "4.5.0" @@ -5553,18 +7027,22 @@ lodash.uniq@^4.5.0: lodash.values@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.values/-/lodash.values-4.3.0.tgz#a3a6c2b0ebecc5c2cba1c17e6e620fe81b53d347" + integrity sha1-o6bCsOvsxcLLocF+bmIP6BtT00c= -lodash@4.17.11, lodash@^4, lodash@^4.13.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" +lodash@4.17.15, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.4: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -log-driver@1.2.7: - version "1.2.7" - resolved "https://registry.yarnpkg.com/log-driver/-/log-driver-1.2.7.tgz#63b95021f0702fedfa2c9bb0a24e7797d71871d8" +lodash@^4.17.14, lodash@^4.17.3: + version "4.17.19" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.19.tgz#e48ddedbe30b3321783c5b4301fbd353bc1e4a4b" + integrity sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ== log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + integrity sha1-N2/3tY6jCGoPCfrMdGF+ylAeGhg= dependencies: chalk "^1.0.0" @@ -5575,46 +7053,54 @@ log-symbols@^2.2.0: dependencies: chalk "^2.0.1" +log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + log-update@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + integrity sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE= dependencies: ansi-escapes "^1.0.0" cli-cursor "^1.0.2" -loglevel@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" - -lolex@^2.3.2: - version "2.7.5" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-2.7.5.tgz#113001d56bfc7e02d56e36291cc5c413d1aa0733" +lolex@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-4.2.0.tgz#ddbd7f6213ca1ea5826901ab1222b65d714b3cd7" + integrity sha512-gKO5uExCXvSm6zbF562EvM+rd1kQDnB9AZBbiQVzf1ZmdDpxUSvpnAaVOP83N/31mRK8Ml8/VE8DMvsAZQ+7wg== -lolex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lolex/-/lolex-3.0.0.tgz#f04ee1a8aa13f60f1abd7b0e8f4213ec72ec193e" +lolex@^5.0.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" + integrity sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A== + dependencies: + "@sinonjs/commons" "^1.7.0" long-timeout@0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514" + integrity sha1-lyHXiLR+C8taJMLivuGg2lXatRQ= long@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" - -long@~3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/long/-/long-3.2.0.tgz#d821b7138ca1cb581c172990ef14db200b5c474b" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== dependencies: js-tokens "^3.0.0 || ^4.0.0" loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" + integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= dependencies: currently-unhandled "^0.4.1" signal-exit "^3.0.0" @@ -5629,9 +7115,10 @@ lowercase-keys@^2.0.0: resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== -lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3: +lru-cache@^4.0.1: version "4.1.5" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== dependencies: pseudomap "^1.0.2" yallist "^2.1.2" @@ -5639,6 +7126,7 @@ lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.2, lru-cache@^4.1.3: lru-cache@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== dependencies: yallist "^3.0.2" @@ -5650,74 +7138,104 @@ macos-release@^2.2.0: make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" + integrity sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ== dependencies: pify "^3.0.0" +make-dir@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-2.1.0.tgz#5f0310e18b8be898cc07009295a30ae41e91e6f5" + integrity sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA== + dependencies: + pify "^4.0.1" + semver "^5.6.0" + +make-dir@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" + integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + dependencies: + semver "^6.0.0" + make-error@^1.1.1: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== makeerror@1.0.x: version "1.0.11" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c" + integrity sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw= dependencies: tmpl "1.0.x" +map-age-cleaner@^0.1.1: + version "0.1.3" + resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" + integrity sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w== + dependencies: + p-defer "^1.0.0" + map-cache@^0.2.2: version "0.2.2" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" + integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= map-obj@^1.0.0, map-obj@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" + integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= map-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= +map-obj@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" + integrity sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g== + map-visit@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f" + integrity sha1-7Nyo8TFE5mDxtb1B8S80edmN+48= dependencies: object-visit "^1.0.0" -marked-terminal@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/marked-terminal/-/marked-terminal-3.2.0.tgz#3fc91d54569332bcf096292af178d82219000474" - dependencies: - ansi-escapes "^3.1.0" - cardinal "^2.1.1" - chalk "^2.4.1" - cli-table "^0.3.1" - node-emoji "^1.4.1" - supports-hyperlinks "^1.0.1" - -marked@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/marked/-/marked-0.5.2.tgz#3efdb27b1fd0ecec4f5aba362bddcd18120e5ba9" - math-random@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" + version "1.0.4" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.4.tgz#5dd6943c938548267016d4e34f057583080c514c" + integrity sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A== + +md5-file@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/md5-file/-/md5-file-4.0.0.tgz#f3f7ba1e2dd1144d5bf1de698d0e5f44a4409584" + integrity sha512-UC0qFwyAjn4YdPpKaDNw6gNxRf7Mcx7jC1UGCY4boCzgvU2Aoc1mOGzTtrjjLKhM5ivsnhoKpQVxKPp+1j1qwg== media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -mem@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mem/-/mem-1.1.0.tgz#5edd52b485ca1d900fe64895505399a0dfa45f76" +mem@^4.0.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/mem/-/mem-4.3.0.tgz#461af497bc4ae09608cdb2e60eefb69bff744178" + integrity sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w== dependencies: - mimic-fn "^1.0.0" + map-age-cleaner "^0.1.1" + mimic-fn "^2.0.0" + p-is-promise "^2.0.0" memory-pager@^1.0.2: - version "1.4.0" - resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.4.0.tgz#8902c72ce2fa34319adc0dae586b7d83cec6d6ac" + version "1.5.0" + resolved "https://registry.yarnpkg.com/memory-pager/-/memory-pager-1.5.0.tgz#d8751655d22d384682741c972f2c3d6dfa3e66b5" + integrity sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg== meow@^3.3.0: version "3.7.0" resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" + integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= dependencies: camelcase-keys "^2.0.0" decamelize "^1.1.2" @@ -5745,35 +7263,54 @@ meow@^4.0.0: redent "^2.0.0" trim-newlines "^2.0.0" +meow@^7.0.0: + version "7.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-7.1.1.tgz#7c01595e3d337fcb0ec4e8eed1666ea95903d306" + integrity sha512-GWHvA5QOcS412WCo8vwKDlTelGLsCGBVevQB5Kva961rmNfun0PCbv5+xta2kUMFJyR8/oWnn7ddeKdosbAPbA== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" + merge-descriptors@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= -merge2@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.2.3.tgz#7ee99dbd69bb6481689253f018488a1b902b0ed5" +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" +merge2@^1.2.3, merge2@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.3.0.tgz#5b366ee83b2f1582c48f87e47cf1a9352103ca81" + integrity sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw== meteor-random@^0.0.3: version "0.0.3" resolved "https://registry.yarnpkg.com/meteor-random/-/meteor-random-0.0.3.tgz#0d1489ecdb9bcb58bb52decebfbceddf54473a68" + integrity sha1-DRSJ7Nuby1i7Ut7Ov7zt31RHOmg= dependencies: crypto "0.0.3" -methmeth@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/methmeth/-/methmeth-1.1.0.tgz#e80a26618e52f5c4222861bb748510bd10e29089" - -methods@~1.1.2: +methods@1.1.2, methods@^1.1.2, methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= micromatch@^2.1.5, micromatch@^2.3.11: version "2.3.11" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + integrity sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU= dependencies: arr-diff "^2.0.0" array-unique "^0.2.1" @@ -5792,6 +7329,7 @@ micromatch@^2.1.5, micromatch@^2.3.11: micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" + integrity sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg== dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -5807,9 +7345,18 @@ micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" +micromatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" + integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== + dependencies: + braces "^3.0.1" + picomatch "^2.0.5" + migrate@^1.6.2: version "1.6.2" resolved "https://registry.yarnpkg.com/migrate/-/migrate-1.6.2.tgz#8970d596780553fe9f545bdf83806df8473f025b" + integrity sha512-XAFab+ArPTo9BHzmihKjsZ5THKRryenA+lwob0R+ax0hLDs7YzJFJT5YZE3gtntZgzdgcuFLs82EJFB/Dssr+g== dependencies: chalk "^1.1.3" commander "^2.9.0" @@ -5820,73 +7367,69 @@ migrate@^1.6.2: mkdirp "^0.5.1" slug "^0.9.2" -mime-db@1.40.0: - version "1.40.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32" - integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA== - -"mime-db@>= 1.38.0 < 2": - version "1.39.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.39.0.tgz#f95a20275742f7d2ad0429acfe40f4233543780e" - -mime-db@~1.37.0: - version "1.37.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8" - -mime-db@~1.38.0: - version "1.38.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" - -mime-types@2.1.24: - version "2.1.24" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81" - integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ== - dependencies: - mime-db "1.40.0" - -mime-types@^2.0.8: - version "2.1.22" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" - dependencies: - mime-db "~1.38.0" +mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== -mime-types@^2.1.12, mime-types@~2.1.18, mime-types@~2.1.19: - version "2.1.21" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96" +mime-types@2.1.26, mime-types@^2.0.8, mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== dependencies: - mime-db "~1.37.0" + mime-db "1.43.0" -mime@1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" - -mime@^1.3.4: +mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.2.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.0.tgz#e051fd881358585f3279df333fe694da0bcffdd6" + version "2.4.4" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" + integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + +mime@^2.4.6: + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== mimic-fn@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== -mimic-fn@^2.1.0: +mimic-fn@^2.0.0, mimic-fn@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== mimic-response@^1.0.0, mimic-response@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" +minimist-options@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" + integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== + dependencies: + arrify "^1.0.1" + is-plain-obj "^1.1.0" + kind-of "^6.0.3" + minimist-options@^3.0.1: version "3.0.2" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" @@ -5898,145 +7441,211 @@ minimist-options@^3.0.1: minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== minimist@~0.0.1: version "0.0.10" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf" - -minipass@^2.2.1, minipass@^2.3.4: - version "2.3.5" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.5.tgz#cacebe492022497f656b0f0f51e2682a9ed2d848" - dependencies: - safe-buffer "^5.1.2" - yallist "^3.0.0" - -minizlib@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.2.1.tgz#dd27ea6136243c7c880684e8672bb3a45fd9b614" - dependencies: - minipass "^2.2.1" + integrity sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8= mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= +"mkdirp@>=0.5 0": + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" + minimist "^1.2.5" -mkdirp@^0.5.0, mkdirp@^0.5.1: +mkdirp@^0.5.1: version "0.5.1" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= dependencies: minimist "0.0.8" -modelo@^4.2.0: - version "4.2.3" - resolved "https://registry.yarnpkg.com/modelo/-/modelo-4.2.3.tgz#b278588a4db87fc1e5107ae3a277c0876f38d894" - modify-values@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment-timezone@^0.5.23: - version "0.5.23" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463" +moment-timezone@^0.5.25: + version "0.5.28" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" + integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== + dependencies: + moment ">= 2.9.0" + +moment-timezone@^0.5.31: + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== dependencies: moment ">= 2.9.0" "moment@>= 2.9.0", moment@^2.18.1: - version "2.23.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.23.0.tgz#759ea491ac97d54bac5ad776996e2a58cc1bc225" + version "2.24.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" + integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + +mongo-uri@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/mongo-uri/-/mongo-uri-0.1.2.tgz#173af01403339002e0abd0b4d675987d3cdcf99e" + integrity sha1-FzrwFAMzkALgq9C01nWYfTzc+Z4= + +mongodb-memory-server-core@6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/mongodb-memory-server-core/-/mongodb-memory-server-core-6.3.1.tgz#13b8404623eaab98f7b0738200cd5377a3ae1782" + integrity sha512-aRbRJJYAJOLbGmwOMb6ghbmGLBYrpJnEFSGhIlrAQJ+gjGLErbKsBG1CjLD9VEcjZ1/V1rPHnsrGlaGTNTmJxA== + dependencies: + "@types/cross-spawn" "^6.0.1" + "@types/debug" "^4.1.5" + "@types/decompress" "^4.2.3" + "@types/dedent" "^0.7.0" + "@types/find-cache-dir" "^2.0.0" + "@types/find-package-json" "^1.1.0" + "@types/get-port" "^4.0.1" + "@types/lockfile" "^1.0.1" + "@types/md5-file" "^4.0.0" + "@types/mkdirp" "^0.5.2" + "@types/tmp" "0.1.0" + "@types/uuid" "3.4.6" + camelcase "^5.3.1" + cross-spawn "^7.0.1" + debug "^4.1.1" + decompress "^4.2.0" + dedent "^0.7.0" + find-cache-dir "3.2.0" + find-package-json "^1.2.0" + get-port "5.0.0" + https-proxy-agent "4.0.0" + lockfile "^1.0.4" + md5-file "^4.0.0" + mkdirp "^0.5.1" + tmp "^0.1.0" + uuid "^3.3.3" + optionalDependencies: + mongodb "^3.2.7" -mongodb-core@3.1.9: - version "3.1.9" - resolved "https://registry.yarnpkg.com/mongodb-core/-/mongodb-core-3.1.9.tgz#c31ee407bf932b0149eaed775c17ee09974e4ca3" +mongodb-memory-server@^6.0.2: + version "6.3.1" + resolved "https://registry.yarnpkg.com/mongodb-memory-server/-/mongodb-memory-server-6.3.1.tgz#505cecc6292ab56d2f9a48d4ab71e71eb072a5b3" + integrity sha512-S9ucLu4dLrSANqli3WOKzCQwAqj1RMia+oXoNcuSCTkbW7R1s1Ll0nH0n73Q5BPvTuLsKbMoVEgwxg+q5zGBYw== + dependencies: + mongodb-memory-server-core "6.3.1" + +mongodb@3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.3.2.tgz#ff086b5f552cf07e24ce098694210f3d42d668b2" + integrity sha512-fqJt3iywelk4yKu/lfwQg163Bjpo5zDKhXiohycvon4iQHbrfflSAz9AIlRE6496Pm/dQKQK5bMigdVo2s6gBg== dependencies: - bson "^1.1.0" + bson "^1.1.1" require_optional "^1.0.1" safe-buffer "^5.1.2" - optionalDependencies: - saslprep "^1.0.0" -mongodb@3.1.10: - version "3.1.10" - resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.1.10.tgz#45ad9b74ea376f4122d0881b75e5489b9e504ed7" +mongodb@^3.2.7: + version "3.5.4" + resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-3.5.4.tgz#f7609cfa9f8c56c35e844b4216ddc3a1b1ec5bef" + integrity sha512-xGH41Ig4dkSH5ROGezkgDbsgt/v5zbNUwE3TcFsSbDc6Qn3Qil17dhLsESSDDPTiyFDCPJRpfd4887dtsPgKtA== dependencies: - mongodb-core "3.1.9" + bl "^2.2.0" + bson "^1.1.1" + denque "^1.4.1" + require_optional "^1.0.1" safe-buffer "^5.1.2" + optionalDependencies: + saslprep "^1.0.0" mongoose-legacy-pluralize@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/mongoose-legacy-pluralize/-/mongoose-legacy-pluralize-1.0.2.tgz#3ba9f91fa507b5186d399fb40854bff18fb563e4" + integrity sha512-Yo/7qQU4/EyIS8YDFSeenIvXxZN+ld7YdV9LqFVQJzTLye8unujAWPZ4NWKfFA+RNjh+wvTWKY9Z3E5XM6ZZiQ== mongoose-type-email@^1.0.5: - version "1.0.10" - resolved "https://registry.yarnpkg.com/mongoose-type-email/-/mongoose-type-email-1.0.10.tgz#ee2ecf7f2b760ad36a60b74bdc2008f2aee6fb41" + version "1.0.12" + resolved "https://registry.yarnpkg.com/mongoose-type-email/-/mongoose-type-email-1.0.12.tgz#6ec4e96081c83cf7e49fa7ef16396ba8280883d6" + integrity sha512-hSnNKJ1eJfhsIIzPbxTlddfI+kac7xGXIgKS3FL3bHnrEPEgcRTXX3zuOiXZSY5v8F/bbflK1PBO+gM036CodQ== + dependencies: + cryptiles "^4.1.2" -mongoose@^5.2.16: - version "5.4.0" - resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.4.0.tgz#78e784a38aab8552c07ef336a342aa75a413c463" - dependencies: - async "2.6.1" - bson "~1.1.0" - kareem "2.3.0" - lodash.get "4.4.2" - mongodb "3.1.10" - mongodb-core "3.1.9" +mongoose@5.7.5: + version "5.7.5" + resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-5.7.5.tgz#b787b47216edf62036aa358c3ef0f1869c46cdc2" + integrity sha512-BZ4FxtnbTurc/wcm/hLltLdI4IDxo4nsE0D9q58YymTdZwreNzwO62CcjVtaHhmr8HmJtOInp2W/T12FZaMf8g== + dependencies: + bson "~1.1.1" + kareem "2.3.1" + mongodb "3.3.2" mongoose-legacy-pluralize "1.0.2" - mpath "0.5.1" - mquery "3.2.0" - ms "2.0.0" - regexp-clone "0.0.1" + mpath "0.6.0" + mquery "3.2.2" + ms "2.1.2" + regexp-clone "1.0.0" safe-buffer "5.1.2" + sift "7.0.1" sliced "1.0.1" -mpath@0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.5.1.tgz#17131501f1ff9e6e4fbc8ffa875aa7065b5775ab" +mpath@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/mpath/-/mpath-0.6.0.tgz#aa922029fca4f0f641f360e74c5c1b6a4c47078e" + integrity sha512-i75qh79MJ5Xo/sbhxrDrPSEG0H/mr1kcZXJ8dH6URU5jD/knFxCVqVC/gVSW7GIXL/9hHWlT9haLbCXWOll3qw== -mquery@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.0.tgz#e276472abd5109686a15eb2a8e0761db813c81cc" +mquery@3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/mquery/-/mquery-3.2.2.tgz#e1383a3951852ce23e37f619a9b350f1fb3664e7" + integrity sha512-XB52992COp0KP230I3qloVUbkLUxJIu328HBP2t2EsxSFtf4W1HPSOBWOXf1bqxK4Xbb66lfMJ+Bpfd9/yZE1Q== dependencies: bluebird "3.5.1" debug "3.1.0" - regexp-clone "0.0.1" + regexp-clone "^1.0.0" safe-buffer "5.1.2" sliced "1.0.1" ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= -ms@^2.0.0, ms@^2.1.1: +ms@2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +ms@2.1.2, ms@^2.0.0, ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== -nan@^2.0.0, nan@^2.9.2: - version "2.12.1" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" +nan@^2.12.1: + version "2.14.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" + integrity sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg== nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" + integrity sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA== dependencies: arr-diff "^4.0.0" array-unique "^0.3.2" @@ -6053,139 +7662,84 @@ nanomatch@^1.2.9: natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= -nconf@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/nconf/-/nconf-0.10.0.tgz#da1285ee95d0a922ca6cee75adcf861f48205ad2" - integrity sha512-fKiXMQrpP7CYWJQzKkPPx9hPgmq+YLDyxcG9N8RpiE9FoCkCbzD0NyW0YhE3xn3Aupe7nnDeIx4PFzYehpHT9Q== - dependencies: - async "^1.4.0" - ini "^1.3.0" - secure-keys "^1.0.0" - yargs "^3.19.0" - -needle@^2.2.1: - version "2.2.4" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.4.tgz#51931bff82533b1928b7d1d69e01f1b00ffd2a4e" - dependencies: - debug "^2.1.2" - iconv-lite "^0.4.4" - sax "^1.2.4" - -needle@^2.2.4: - version "2.4.0" - resolved "https://registry.yarnpkg.com/needle/-/needle-2.4.0.tgz#6833e74975c444642590e15a750288c5f939b57c" - integrity sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg== - dependencies: - debug "^3.2.6" - iconv-lite "^0.4.4" - sax "^1.2.4" - -negotiator@0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +negotiator@0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" + integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== neo-async@^2.6.0: version "2.6.1" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== -netmask@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/netmask/-/netmask-1.0.6.tgz#20297e89d86f6f6400f250d9f4f6b4c1945fcd35" - integrity sha1-ICl+idhvb2QA8lDZ9Pa0wZRfzTU= - nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== -nise@^1.4.7: - version "1.4.8" - resolved "https://registry.yarnpkg.com/nise/-/nise-1.4.8.tgz#ce91c31e86cf9b2c4cac49d7fcd7f56779bfd6b0" +nise@^1.5.2: + version "1.5.3" + resolved "https://registry.yarnpkg.com/nise/-/nise-1.5.3.tgz#9d2cfe37d44f57317766c6e9408a359c5d3ac1f7" + integrity sha512-Ymbac/94xeIrMf59REBPOv0thr+CJVFMhrlAkW/gjCIE58BGQdCj0x7KRCb3yz+Ga2Rz3E9XXSvUyyxqqhjQAQ== dependencies: - "@sinonjs/formatio" "^3.1.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/text-encoding" "^0.7.1" just-extend "^4.0.2" - lolex "^2.3.2" + lolex "^5.0.1" path-to-regexp "^1.7.0" - text-encoding "^0.6.4" -node-emoji@^1.4.1: - version "1.10.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.10.0.tgz#8886abd25d9c7bb61802a658523d1f8d2a89b2da" - dependencies: - lodash.toarray "^4.4.0" +nocache@2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/nocache/-/nocache-2.1.0.tgz#120c9ffec43b5729b1d5de88cd71aa75a0ba491f" + integrity sha512-0L9FvHG3nfnnmaEQPjT9xhfN4ISk0A8/2j4M37Np4mcDesJjHgEUfgPhdCyZuFI954tjokaIj/A3NdpFNdEh4Q== -node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.3.0.tgz#1a1d940bbfb916a1d3e0219f037e89e71f8c5fa5" +node-fetch@2.6.0, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0: + version "2.6.0" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" + integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== node-forge@0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.4.tgz#8e6e9f563a1e32213aa7508cded22aa791dbf986" + integrity sha512-8Df0906+tq/omxuCZD6PqhPaQDYuyJ1d+VITgxoIA8zvQd1ru+nMJcDChHH324MWitIgbVkAkQoGEEVJNpn/PA== -node-forge@^0.7.5: - version "0.7.6" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.6.tgz#fdf3b418aee1f94f0ef642cd63486c77ca9724ac" +node-forge@^0.9.0: + version "0.9.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.1.tgz#775368e6846558ab6676858a4d8c6e8d16c677b5" + integrity sha512-G6RlQt5Sb4GMBzXvhfkeFmbqR6MzhtnT7VTHuLadjkii3rdYHNdw0m8zA4BTxVIh68FicCQ2NSUANpsqkr9jvQ== node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" + integrity sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs= -node-notifier@^4.0.2: - version "4.6.1" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-4.6.1.tgz#056d14244f3dcc1ceadfe68af9cff0c5473a33f3" - dependencies: - cli-usage "^0.1.1" - growly "^1.2.0" - lodash.clonedeep "^3.0.0" - minimist "^1.1.1" - semver "^5.1.0" - shellwords "^0.1.0" - which "^1.0.5" +node-modules-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz#8d9dbe28964a4ac5712e9131642107c71e90ec40" + integrity sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA= -node-notifier@^5.0.2: - version "5.3.0" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.3.0.tgz#c77a4a7b84038733d5fb351aafd8a268bfe19a01" +node-notifier@^5.4.0, node-notifier@^5.4.2: + version "5.4.3" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.4.3.tgz#cb72daf94c93904098e28b9c590fd866e464bd50" + integrity sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q== dependencies: growly "^1.3.0" + is-wsl "^1.1.0" semver "^5.5.0" shellwords "^0.1.1" which "^1.3.0" -node-pre-gyp@^0.10.0: - version "0.10.3" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" - -node-pre-gyp@^0.12.0: - version "0.12.0" - resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz#39ba4bb1439da030295f899e3b520b7785766149" - dependencies: - detect-libc "^1.0.2" - mkdirp "^0.5.1" - needle "^2.2.1" - nopt "^4.0.1" - npm-packlist "^1.1.6" - npmlog "^4.0.2" - rc "^1.2.7" - rimraf "^2.6.1" - semver "^5.3.0" - tar "^4" +node-object-hash@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/node-object-hash/-/node-object-hash-2.0.0.tgz#9971fcdb7d254f05016bd9ccf508352bee11116b" + integrity sha512-VZR0zroAusy1ETZMZiGeLkdu50LGjG5U1KHZqTruqtTyQ2wfWhHG2Ow4nsUbfTFGlaREgNHcCWoM/OzEm6p+NQ== node-schedule@^1.2.5: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.1.tgz#6909dd644211bca153b15afc62e1dc0afa7d28be" + version "1.3.2" + resolved "https://registry.yarnpkg.com/node-schedule/-/node-schedule-1.3.2.tgz#d774b383e2a6f6ade59eecc62254aea07cd758cb" + integrity sha512-GIND2pHMHiReSZSvS6dpZcDH7pGPGFfWBIEud6S00Q8zEIzAs9ommdyRK1ZbQt8y1LyZsJYZgPnyi7gpU2lcdw== dependencies: cron-parser "^2.7.3" long-timeout "0.1.1" @@ -6194,39 +7748,27 @@ node-schedule@^1.2.5: nodemailer@^4.1.3: version "4.7.0" resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-4.7.0.tgz#4420e06abfffd77d0618f184ea49047db84f4ad8" + integrity sha512-IludxDypFpYw4xpzKdMAozBSkzKHmNBvGanUREjJItgJ2NYcK/s8+PggVhj7c2yGFQykKsnnmv1+Aqo0ZfjHmw== -nopt@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" - dependencies: - abbrev "1" - osenv "^0.1.4" - -normalize-package-data@^2.3.0, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5: +normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== dependencies: hosted-git-info "^2.1.4" resolve "^1.10.0" semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-package-data@^2.3.2: - version "2.4.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.4.0.tgz#12f95a307d58352075a04907b84ac8be98ac012f" - dependencies: - hosted-git-info "^2.1.4" - is-builtin-module "^1.0.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - normalize-path@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + integrity sha1-MtDkcvkf80VwHBWoMRAY07CpA3k= normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + integrity sha1-GrKLVW4Zg2Oowab35vogE3/mrtk= dependencies: remove-trailing-separator "^1.0.1" @@ -6236,111 +7778,116 @@ normalize-url@^3.3.0: integrity sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg== normalize-url@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.3.0.tgz#9c49e10fc1876aeb76dba88bf1b2b5d9fa57b2ee" - integrity sha512-0NLtR71o4k6GLP+mr6Ty34c5GA6CMoEsncKJxvQd8NzPxaHRJNnb5gZE8R1XF4CPIS7QPHLJ74IFszwtNVAHVQ== - -npm-bundled@^1.0.1: - version "1.0.5" - resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.5.tgz#3c1732b7ba936b3a10325aef616467c0ccbcc979" - -npm-packlist@^1.1.6: - version "1.1.12" - resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.12.tgz#22bde2ebc12e72ca482abd67afc51eb49377243a" - dependencies: - ignore-walk "^3.0.1" - npm-bundled "^1.0.1" + version "4.5.0" + resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" + integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== npm-path@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64" + integrity sha512-IFsj0R9C7ZdR5cP+ET342q77uSRdtWOlWpih5eC+lu29tIDbNEgDbzgVJ5UFvYHWhxDZ5TFkJafFioO0pPQjCw== dependencies: which "^1.2.10" npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" + integrity sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8= dependencies: path-key "^2.0.0" npm-which@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa" + integrity sha1-kiXybsOihcIJyuZ8OxGmtKtxQKo= dependencies: commander "^2.9.0" npm-path "^2.0.2" which "^1.2.10" -npmlog@^4.0.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" - -"nwmatcher@>= 1.3.9 < 2.0.0": - version "1.4.4" - resolved "https://registry.yarnpkg.com/nwmatcher/-/nwmatcher-1.4.4.tgz#2285631f34a95f0d0395cd900c96ed39b58f346e" + integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0= nwsapi@^2.0.7: - version "2.0.9" - resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016" + version "2.2.0" + resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" + integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== oauth-sign@~0.9.0: version "0.9.0" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" - -oauth@^0.9.15: - version "0.9.15" - resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + +object-component@0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" + integrity sha1-8MaapQ78lbhmwYb0AKM3acsvEpE= object-copy@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/object-copy/-/object-copy-0.1.0.tgz#7e7d858b781bd7c991a41ba975ed3812754e998c" + integrity sha1-fn2Fi3gb18mRpBupde04EnVOmYw= dependencies: copy-descriptor "^0.1.0" define-property "^0.2.5" kind-of "^3.0.3" -object-hash@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-1.3.1.tgz#fde452098a951cb145f039bb7d455449ddc126df" - integrity sha512-OSuu/pU4ENM9kmREg0BdNrUDIl1heYa4mBZacJc+vVWz4GtAwu7jO8s4AIt2aGRUTqxykpWzI3Oqnsm13tTMDA== +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== -object-keys@^1.0.0, object-keys@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.12.tgz#09c53855377575310cca62f55bb334abff7b3ed2" +object-is@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== object-path@^0.11.4: version "0.11.4" resolved "https://registry.yarnpkg.com/object-path/-/object-path-0.11.4.tgz#370ae752fbf37de3ea70a861c23bba8915691949" + integrity sha1-NwrnUvvzfePqcKhhwju6iRVpGUk= object-visit@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/object-visit/-/object-visit-1.0.1.tgz#f79c4493af0c5377b59fe39d395e41042dd045bb" + integrity sha1-95xEk68MU3e1n+OdOV5BBC3QRbs= dependencies: isobject "^3.0.0" -object.getownpropertydescriptors@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz#8758c846f5b407adab0f236e0986f14b051caa16" +object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== dependencies: define-properties "^1.1.2" - es-abstract "^1.5.1" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" object.omit@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + integrity sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo= dependencies: for-own "^0.1.4" is-extendable "^0.1.1" @@ -6348,6 +7895,7 @@ object.omit@^2.0.0: object.pick@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/object.pick/-/object.pick-1.3.0.tgz#87a10ac4c1694bd2e1cbf53591a66141fb5dd747" + integrity sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c= dependencies: isobject "^3.0.1" @@ -6359,18 +7907,21 @@ octokit-pagination-methods@^1.1.0: on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= dependencies: ee-first "1.1.1" once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + integrity sha1-ofeDj4MUxRbwXs78vEzP4EtO14k= onetime@^2.0.0: version "2.0.1" @@ -6382,39 +7933,55 @@ onetime@^2.0.0: onetime@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.0.tgz#fff0f3c91617fe62bb50189636e99ac8a6df7be5" + integrity sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q== dependencies: mimic-fn "^2.1.0" -opn@^5.5.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc" - integrity sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA== - dependencies: - is-wsl "^1.1.0" - optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" + integrity sha1-2j6nRob6IaGaERwybpDrFaAZZoY= dependencies: minimist "~0.0.1" wordwrap "~0.0.2" optionator@^0.8.1: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + version "0.8.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495" + integrity sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA== dependencies: deep-is "~0.1.3" - fast-levenshtein "~2.0.4" + fast-levenshtein "~2.0.6" levn "~0.3.0" prelude-ls "~1.1.2" type-check "~0.3.2" - wordwrap "~1.0.0" + word-wrap "~1.2.3" -optjs@~3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/optjs/-/optjs-3.2.2.tgz#69a6ce89c442a44403141ad2f9b370bd5bb6f4ee" +ora@4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-4.0.3.tgz#752a1b7b4be4825546a7a3d59256fa523b6b6d05" + integrity sha512-fnDebVFyz309A73cqCipVL1fBZewq4vwgSHfxh43vVy31mbyoQ8sCH3Oeaog/owYOs/lLlGVPCISQonTneg6Pg== + dependencies: + chalk "^3.0.0" + cli-cursor "^3.1.0" + cli-spinners "^2.2.0" + is-interactive "^1.0.0" + log-symbols "^3.0.0" + mute-stream "0.0.8" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +ora@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + integrity sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q= + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" -ora@3.4.0, ora@^3.4.0: +ora@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/ora/-/ora-3.4.0.tgz#bf0752491059a3ef3ed4c85097531de9fdbcd318" integrity sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg== @@ -6426,34 +7993,21 @@ ora@3.4.0, ora@^3.4.0: strip-ansi "^5.2.0" wcwidth "^1.0.1" -ora@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" - dependencies: - chalk "^1.1.1" - cli-cursor "^1.0.2" - cli-spinners "^0.1.2" - object-assign "^4.0.1" - os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - dependencies: - lcid "^1.0.0" - -os-locale@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-2.1.0.tgz#42bc2900a6b5b8bd17376c8e882b65afccf24bf2" +os-locale@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" + integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== dependencies: - execa "^0.7.0" - lcid "^1.0.0" - mem "^1.1.0" + execa "^1.0.0" + lcid "^2.0.0" + mem "^4.0.0" -os-name@3.1.0, os-name@^3.0.0: +os-name@3.1.0, os-name@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-name/-/os-name-3.1.0.tgz#dec19d966296e1cd62d701a5a66ee1ddeae70801" integrity sha512-h8L+8aNjNcMpo/mAIBPn5PXCM16iyPGjHNWo6U1YO8sJTMHtEtyczI6QJnLoplswm6goopQkqc7OAnjhWcugVg== @@ -6461,20 +8015,10 @@ os-name@3.1.0, os-name@^3.0.0: macos-release "^2.2.0" windows-release "^3.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - -osenv@^0.1.4: - version "0.1.5" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" - dependencies: - os-homedir "^1.0.0" - os-tmpdir "^1.0.0" - -p-cancelable@^0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-0.3.0.tgz#b9e123800bcebb7ac13a479be195b507b98d30fa" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= p-cancelable@^1.0.0: version "1.1.0" @@ -6484,27 +8028,48 @@ p-cancelable@^1.0.0: p-defer@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" + integrity sha1-n26xgvbJqozXQwBKfU+WsZaw+ww= + +p-defer@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-3.0.0.tgz#d1dceb4ee9b2b604b1d94ffec83760175d4e6f83" + integrity sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw== + +p-each-series@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-each-series/-/p-each-series-1.0.0.tgz#930f3d12dd1f50e7434457a22cd6f04ac6ad7f71" + integrity sha1-kw89Et0fUOdDRFeiLNbwSsatf3E= + dependencies: + p-reduce "^1.0.0" p-finally@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" + integrity sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4= + +p-is-promise@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.1.0.tgz#918cebaea248a62cf7ffab8e3bca8c5f882fc42e" + integrity sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg== p-limit@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== dependencies: p-try "^1.0.0" -p-limit@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" - integrity sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ== +p-limit@^2.0.0, p-limit@^2.2.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== dependencies: p-try "^2.0.0" p-locate@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= dependencies: p-limit "^1.1.0" @@ -6515,57 +8080,47 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + integrity sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA== + +p-reduce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" + integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -pac-proxy-agent@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pac-proxy-agent/-/pac-proxy-agent-3.0.0.tgz#11d578b72a164ad74bf9d5bac9ff462a38282432" - integrity sha512-AOUX9jES/EkQX2zRz0AW7lSx9jD//hQS8wFXBvcnd/J2Py9KaMJMqV/LPqJssj1tgGufotb2mmopGPR15ODv1Q== - dependencies: - agent-base "^4.2.0" - debug "^3.1.0" - get-uri "^2.0.0" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - pac-resolver "^3.0.0" - raw-body "^2.2.0" - socks-proxy-agent "^4.0.1" - -pac-resolver@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pac-resolver/-/pac-resolver-3.0.0.tgz#6aea30787db0a891704deb7800a722a7615a6f26" - integrity sha512-tcc38bsjuE3XZ5+4vP96OfhOugrX+JcnpUbhfuc4LuXBLQhoTthOstZeoQJBDnQUDYzYmdImKsbz0xSl1/9qeA== - dependencies: - co "^4.6.0" - degenerator "^1.0.4" - ip "^1.1.5" - netmask "^1.0.6" - thunkify "^2.1.2" - -package-json@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/package-json/-/package-json-4.0.1.tgz#8869a0401253661c4c4ca3da6c2121ed555f5eed" - integrity sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0= +package-json@^6.3.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/package-json/-/package-json-6.5.0.tgz#6feedaca35e75725876d0b0e64974697fed145b0" + integrity sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ== dependencies: - got "^6.7.1" - registry-auth-token "^3.0.1" - registry-url "^3.0.3" - semver "^5.1.0" + got "^9.6.0" + registry-auth-token "^4.0.0" + registry-url "^5.0.0" + semver "^6.2.0" pako@~1.0.2: - version "1.0.7" - resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.7.tgz#2473439021b57f1516c82f58be7275ad8ef1bb27" + version "1.0.11" + resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" + integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw== parse-github-repo-url@^1.3.0: version "1.4.1" @@ -6575,6 +8130,7 @@ parse-github-repo-url@^1.3.0: parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + integrity sha1-ssN2z7EfNVE7rdFz7wu246OIORw= dependencies: glob-base "^0.3.0" is-dotfile "^1.0.0" @@ -6584,6 +8140,7 @@ parse-glob@^3.0.4: parse-json@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= dependencies: error-ex "^1.2.0" @@ -6595,6 +8152,16 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-json@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.1.0.tgz#f96088cdf24a8faa9aea9a009f2d9d942c999646" + integrity sha512-+mi/lmVVNKFNVyLXV31ERiy2CY5E1/F6QtJFEzoChPRwwngMNXRDQ9GJ5WdE2Z2P4AujsOi0/+2qHID68KwfIQ== + dependencies: + "@babel/code-frame" "^7.0.0" + error-ex "^1.3.1" + json-parse-even-better-errors "^2.3.0" + lines-and-columns "^1.1.6" + parse-path@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-4.0.1.tgz#0ec769704949778cb3b8eda5e994c32073a1adff" @@ -6616,231 +8183,203 @@ parse-url@^5.0.0: parse5@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" + integrity sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA== -parse5@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-1.5.1.tgz#9b7f3b0de32be78dc2401b17573ccaf0f6f59d94" +parseqs@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d" + integrity sha1-1SCKNzjkZ2bikbouoXNoSSGouJ0= + dependencies: + better-assert "~1.0.0" -parseurl@~1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" +parseuri@0.0.5: + version "0.0.5" + resolved "https://registry.yarnpkg.com/parseuri/-/parseuri-0.0.5.tgz#80204a50d4dbb779bfdc6ebe2778d90e4bce320a" + integrity sha1-gCBKUNTbt3m/3G6+J3jZDkvOMgo= + dependencies: + better-assert "~1.0.0" + +parseurl@^1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" - -path-dirname@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-dirname/-/path-dirname-1.0.2.tgz#cc33d24d525e099a5388c0336c6e32b9160609e0" + integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= path-exists@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-2.1.0.tgz#0feb6c64f0fc518d9a754dd5efb62c7022761f4b" + integrity sha1-D+tsZPD8UY2adU3V77YscCJ2H0s= dependencies: pinkie-promise "^2.0.0" path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - -path-is-inside@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" - integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-key@^2.0.0, path-key@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.5, path-parse@^1.0.6: +path-parse@^1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= path-to-regexp@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" + version "1.8.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a" + integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA== dependencies: isarray "0.0.1" path-type@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441" + integrity sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE= dependencies: graceful-fs "^4.1.2" pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" + integrity sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg== dependencies: pify "^3.0.0" -path@0.12.7: - version "0.12.7" - resolved "https://registry.yarnpkg.com/path/-/path-0.12.7.tgz#d4dc2a506c4ce2197eb481ebfcd5b36c0140b10f" - integrity sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8= - dependencies: - process "^0.11.1" - util "^0.10.3" +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +pend@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" + integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA= performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +picomatch@^2.0.5, picomatch@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.1.tgz#21bac888b6ed8601f831ce7816e335bc779f0a4a" + integrity sha512-ISBaA8xQNmwELC7eOjqFKMESB2VIqt4PPDD0nsS95b/9dZXvVKOlz9keMSnoGGKcOHXfTvDD6WMaRoSc9UuhRA== pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= pify@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" + integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= -pify@^4.0.0, pify@^4.0.1: +pify@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" + integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g== pinkie-promise@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" + integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= dependencies: pinkie "^2.0.0" pinkie@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" + integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= + +pirates@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.1.tgz#643a92caf894566f91b2b986d2c66950a8e2fb87" + integrity sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA== + dependencies: + node-modules-regexp "^1.0.0" pkg-dir@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= dependencies: find-up "^2.1.0" -pn@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" - -posix-character-classes@^0.1.0: - version "0.1.1" - resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" - -power-assert-context-formatter@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-context-formatter/-/power-assert-context-formatter-1.2.0.tgz#8fbe72692288ec5a7203cdf215c8b838a6061d2a" - dependencies: - core-js "^2.0.0" - power-assert-context-traversal "^1.2.0" - -power-assert-context-reducer-ast@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-context-reducer-ast/-/power-assert-context-reducer-ast-1.2.0.tgz#c7ca1c9e39a6fb717f7ac5fe9e76e192bf525df3" - dependencies: - acorn "^5.0.0" - acorn-es7-plugin "^1.0.12" - core-js "^2.0.0" - espurify "^1.6.0" - estraverse "^4.2.0" - -power-assert-context-traversal@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-context-traversal/-/power-assert-context-traversal-1.2.0.tgz#f6e71454baf640de5c1c9c270349f5c9ab0b2e94" - dependencies: - core-js "^2.0.0" - estraverse "^4.1.0" - -power-assert-formatter@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/power-assert-formatter/-/power-assert-formatter-1.4.1.tgz#5dc125ed50a3dfb1dda26c19347f3bf58ec2884a" - dependencies: - core-js "^2.0.0" - power-assert-context-formatter "^1.0.7" - power-assert-context-reducer-ast "^1.0.7" - power-assert-renderer-assertion "^1.0.7" - power-assert-renderer-comparison "^1.0.7" - power-assert-renderer-diagram "^1.0.7" - power-assert-renderer-file "^1.0.7" - -power-assert-renderer-assertion@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-renderer-assertion/-/power-assert-renderer-assertion-1.2.0.tgz#3db6ffcda106b37bc1e06432ad0d748a682b147a" - dependencies: - power-assert-renderer-base "^1.1.1" - power-assert-util-string-width "^1.2.0" - -power-assert-renderer-base@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/power-assert-renderer-base/-/power-assert-renderer-base-1.1.1.tgz#96a650c6fd05ee1bc1f66b54ad61442c8b3f63eb" - -power-assert-renderer-comparison@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-renderer-comparison/-/power-assert-renderer-comparison-1.2.0.tgz#e4f88113225a69be8aa586ead05aef99462c0495" +pkg-dir@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3" + integrity sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw== dependencies: - core-js "^2.0.0" - diff-match-patch "^1.0.0" - power-assert-renderer-base "^1.1.1" - stringifier "^1.3.0" - type-name "^2.0.1" + find-up "^3.0.0" -power-assert-renderer-diagram@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-renderer-diagram/-/power-assert-renderer-diagram-1.2.0.tgz#37f66e8542e5677c5b58e6d72b01c0d9a30e2219" +pkg-dir@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== dependencies: - core-js "^2.0.0" - power-assert-renderer-base "^1.1.1" - power-assert-util-string-width "^1.2.0" - stringifier "^1.3.0" + find-up "^4.0.0" -power-assert-renderer-file@^1.0.7: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-renderer-file/-/power-assert-renderer-file-1.2.0.tgz#3f4bebd9e1455d75cf2ac541e7bb515a87d4ce4b" - dependencies: - power-assert-renderer-base "^1.1.1" +pn@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb" + integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== -power-assert-util-string-width@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/power-assert-util-string-width/-/power-assert-util-string-width-1.2.0.tgz#6e06d5e3581bb876c5d377c53109fffa95bd91a0" +portfinder@1.0.25: + version "1.0.25" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" + integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== dependencies: - eastasianwidth "^0.2.0" + async "^2.6.2" + debug "^3.1.1" + mkdirp "^0.5.1" -power-assert@^1.4.4: - version "1.6.1" - resolved "https://registry.yarnpkg.com/power-assert/-/power-assert-1.6.1.tgz#b28cbc02ae808afd1431d0cd5093a39ac5a5b1fe" - dependencies: - define-properties "^1.1.2" - empower "^1.3.1" - power-assert-formatter "^1.4.1" - universal-deep-strict-equal "^1.2.1" - xtend "^4.0.0" +posix-character-classes@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab" + integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs= prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= -prepend-file@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/prepend-file/-/prepend-file-1.3.1.tgz#83b16e0b4ac1901fce88dbd945a22f4cc81df579" - integrity sha1-g7FuC0rBkB/OiNvZRaIvTMgd9Xk= +prepend-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prepend-file/-/prepend-file-2.0.0.tgz#2d3256376a64ca3b5640153890a89cadbebaf1a9" + integrity sha512-U6on3jv5hQ+CNEO7gFn00PUlm3F/oXIQTMg6jpeQTQHLYSZl/Cxb4NpH44FA0By+maPXpfUaqmCoPUTu/Z3/8g== dependencies: - tmp "0.0.31" - -prepend-http@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" - integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= + path-exists "^4.0.0" + temp-write "^4.0.0" prepend-http@^2.0.0: version "2.0.0" @@ -6850,72 +8389,67 @@ prepend-http@^2.0.0: preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + integrity sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks= prettier@^1.14.2: - version "1.15.3" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.15.3.tgz#1feaac5bdd181237b54dbe65d874e02a1472786a" - -pretty-format@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" + version "1.19.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" + integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== pretty-format@^22.4.0, pretty-format@^22.4.3: version "22.4.3" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-22.4.3.tgz#f873d780839a9c02e9664c8a082e9ee79eaac16f" + integrity sha512-S4oT9/sT6MN7/3COoOy+ZJeA92VmOnveLHgrwBE3Z1W5N9S2A1QGNYiE1z75DAENbJrXXUb+OWXhpJcg05QKQQ== dependencies: ansi-regex "^3.0.0" ansi-styles "^3.2.0" -pretty-format@^23.6.0: - version "23.6.0" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" +pretty-format@^24.9.0: + version "24.9.0" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-24.9.0.tgz#12fac31b37019a4eea3c11aa9a959eb7628aa7c9" + integrity sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA== dependencies: - ansi-regex "^3.0.0" + "@jest/types" "^24.9.0" + ansi-regex "^4.0.0" ansi-styles "^3.2.0" + react-is "^16.8.4" printj@~1.1.0, printj@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/printj/-/printj-1.1.2.tgz#d90deb2975a8b9f600fb3a1c94e3f4c53c78a222" + integrity sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ== private@^0.1.8: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" - -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" + integrity sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg== process-nextick-args@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" - -process@^0.11.1: - version "0.11.10" - resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" - integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI= + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -"promise@>=3.2 <8": - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== +prompts@^2.0.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.3.1.tgz#b63a9ce2809f106fa9ae1277c275b167af46ea05" + integrity sha512-qIP2lQyCwYbdzcqHIUi2HAxiWixhoM9OdLCWf8txXsapC/X9YdsCoeyRIXE/GP+Q0J37Q7+XN/MFqbUa7IzXNA== dependencies: - asap "~2.0.3" + kleur "^3.0.3" + sisteransi "^1.0.4" -protobufjs@^5.0.3: - version "5.0.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-5.0.3.tgz#e4dfe9fb67c90b2630d15868249bcc4961467a17" +proper-lockfile@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/proper-lockfile/-/proper-lockfile-4.1.1.tgz#284cf9db9e30a90e647afad69deb7cb06881262c" + integrity sha512-1w6rxXodisVpn7QYvLk706mzprPTAPCYAqxMvctmPN3ekuRk/kuGkGc82pangZiAt4R3lwSuUzheTTn0/Yb7Zg== dependencies: - ascli "~1" - bytebuffer "~5" - glob "^7.0.5" - yargs "^3.10.0" + graceful-fs "^4.1.11" + retry "^0.12.0" + signal-exit "^3.0.2" -protobufjs@^6.8.0, protobufjs@^6.8.1, protobufjs@^6.8.6, protobufjs@^6.8.8: +protobufjs@^6.8.1, protobufjs@^6.8.6, protobufjs@^6.8.8: version "6.8.8" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.8.8.tgz#c8b4f1282fd7a90e6f5b109ed11c84af82908e7c" + integrity sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -6936,50 +8470,23 @@ protocols@^1.1.0, protocols@^1.4.0: resolved "https://registry.yarnpkg.com/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" integrity sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg== -proxy-addr@~2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" +proxy-addr@~2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" + integrity sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw== dependencies: forwarded "~0.1.2" - ipaddr.js "1.8.0" - -proxy-agent@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/proxy-agent/-/proxy-agent-3.1.0.tgz#3cf86ee911c94874de4359f37efd9de25157c113" - integrity sha512-IkbZL4ClW3wwBL/ABFD2zJ8iP84CY0uKMvBPk/OceQe/cEjrxzN1pMHsLwhbzUoRhG9QbSxYC+Z7LBkTiBNvrA== - dependencies: - agent-base "^4.2.0" - debug "^3.1.0" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.1" - lru-cache "^4.1.2" - pac-proxy-agent "^3.0.0" - proxy-from-env "^1.0.0" - socks-proxy-agent "^4.0.1" - -proxy-from-env@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee" - integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4= - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" + ipaddr.js "1.9.1" pseudomap@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= -psl@^1.1.24, psl@^1.1.28: - version "1.1.31" - resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184" - -pump@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" +psl@^1.1.28: + version "1.7.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.7.0.tgz#f1c4c47a8ef97167dea5d6bbf4816d736e884a3c" + integrity sha512-5NsSEDv8zY70ScRnOTn7bK7eanl2MvFrOrS/R6x+dBt5g1ghnj9Zv90kO8GwT8gxcu2ANyFprnFYB85IogIJOQ== pump@^3.0.0: version "3.0.0" @@ -6989,106 +8496,124 @@ pump@^3.0.0: end-of-stream "^1.1.0" once "^1.3.1" -pumpify@^1.5.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-1.5.1.tgz#36513be246ab27570b1a374a5ce278bfd74370ce" +pumpify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pumpify/-/pumpify-2.0.1.tgz#abfc7b5a621307c728b551decbbefb51f0e4aa1e" + integrity sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw== dependencies: - duplexify "^3.6.0" + duplexify "^4.1.1" inherits "^2.0.3" - pump "^2.0.0" + pump "^3.0.0" punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" - -punycode@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" + integrity sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0= punycode@^2.1.0, punycode@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +pupa@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/pupa/-/pupa-2.0.1.tgz#dbdc9ff48ffbea4a26a069b6f9f7abb051008726" + integrity sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA== + dependencies: + escape-goat "^2.0.0" q@^0.9.7: version "0.9.7" resolved "https://registry.yarnpkg.com/q/-/q-0.9.7.tgz#4de2e6cb3b29088c9e4cbc03bf9d42fb96ce2f75" + integrity sha1-TeLmyzspCIyeTLwDv51C+5bOL3U= q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" integrity sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc= -qs@6.5.2, qs@~6.5.2: +qs@6.7.0: + version "6.7.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.7.0.tgz#41dc1a015e3d581f1621776be31afb2876a9b1bc" + integrity sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ== + +qs@^6.9.4: + version "6.9.4" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.4.tgz#9090b290d1f91728d3c22e54843ca44aea5ab687" + integrity sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ== + +qs@~6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" - -qs@^6.5.2: - version "6.6.0" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== quick-lru@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= +quick-lru@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" + integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== + randomatic@^3.0.0: version "3.1.1" resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.1.1.tgz#b776efc59375984e36c537b2f51a1f0aff0da1ed" + integrity sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw== dependencies: is-number "^4.0.0" kind-of "^6.0.0" math-random "^1.0.1" -range-parser@~1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" - -raw-body@2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" - dependencies: - bytes "3.0.0" - http-errors "1.6.3" - iconv-lite "0.4.23" - unpipe "1.0.0" +range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== -raw-body@^2.2.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" - integrity sha512-9WmIKF6mkvA0SLmA2Knm9+qj89e+j1zqgyn8aXGd7+nAduPoqgI9lO57SAZNn/Byzo5P7JhXTyg9PzaJbH73bA== +raw-body@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332" + integrity sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q== dependencies: bytes "3.1.0" - http-errors "1.7.3" + http-errors "1.7.2" iconv-lite "0.4.24" unpipe "1.0.0" -rc@^1.0.1, rc@^1.1.6, rc@^1.2.7: +rc@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw== dependencies: deep-extend "^0.6.0" ini "~1.3.0" minimist "^1.2.0" strip-json-comments "~2.0.1" +react-is@^16.8.4: + version "16.13.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.0.tgz#0f37c3613c34fe6b37cd7f763a0d6293ab15c527" + integrity sha512-GFMtL0vHkiBv9HluwNZTggSn/sCyEt9n02aM0dSAjGGyqyNlAyftYm4phPxdvCigG15JreC5biwxCgTAJZ7yAA== + read-pkg-up@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-1.0.1.tgz#9d63c13276c065918d57f002a57f40a1b643fb02" + integrity sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI= dependencies: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -7097,22 +8622,32 @@ read-pkg-up@^3.0.0: find-up "^2.0.0" read-pkg "^3.0.0" +read-pkg-up@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" + integrity sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA== + dependencies: + find-up "^3.0.0" + read-pkg "^3.0.0" + +read-pkg-up@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" + integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== + dependencies: + find-up "^4.1.0" + read-pkg "^5.2.0" + type-fest "^0.8.1" + read-pkg@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-1.1.0.tgz#f5ffaa5ecd29cb31c0474bca7d756b6bb29e3f28" + integrity sha1-9f+qXs0pyzHAR0vKfXVra7KePyg= dependencies: load-json-file "^1.0.0" normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -7122,72 +8657,64 @@ read-pkg@^3.0.0: normalize-package-data "^2.3.2" path-type "^3.0.0" -readable-stream@1.1.x: +read-pkg@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" + integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^2.5.0" + parse-json "^5.0.0" + type-fest "^0.6.0" + +"readable-stream@1.x >=1.1.9": version "1.1.14" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" + integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= dependencies: core-util-is "~1.0.0" inherits "~2.0.1" isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.2: - version "3.3.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.3.0.tgz#cb8011aad002eb717bf040291feba8569c986fb9" - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@3: - version "3.4.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.4.0.tgz#a51c26754658e0a3c21dbf59163bd45ba6f447fc" - integrity sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ== +"readable-stream@2 || 3", readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== dependencies: inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.6, readable-stream@^2.2.2, readable-stream@~2.3.6: - version "2.3.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@~1.0.32: - version "1.0.34" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" -readable-stream@~2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e" +readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.3.0, readable-stream@^2.3.5, readable-stream@~2.3.6: + version "2.3.7" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" + integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== dependencies: core-util-is "~1.0.0" - inherits "~2.0.1" + inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" - string_decoder "~0.10.x" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" util-deprecate "~1.0.1" readdirp@^2.0.0: version "2.2.1" resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525" + integrity sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ== dependencies: graceful-fs "^4.1.11" micromatch "^3.1.10" readable-stream "^2.0.2" +realpath-native@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" + integrity sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA== + dependencies: + util.promisify "^1.0.0" + rechoir@^0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" @@ -7198,6 +8725,7 @@ rechoir@^0.6.2: redent@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" + integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= dependencies: indent-string "^2.1.0" strip-indent "^1.0.1" @@ -7210,21 +8738,42 @@ redent@^2.0.0: indent-string "^3.0.0" strip-indent "^2.0.0" -redeyed@~2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/redeyed/-/redeyed-2.1.1.tgz#8984b5815d99cb220469c99eeeffe38913e6cc0b" +redent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" + integrity sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== dependencies: - esprima "~4.0.0" + indent-string "^4.0.0" + strip-indent "^3.0.0" redis-commands@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.4.0.tgz#52f9cf99153efcce56a8f86af986bd04e988602f" + version "1.5.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.5.0.tgz#80d2e20698fe688f227127ff9e5164a7dd17e785" + integrity sha512-6KxamqpZ468MeQC3bkWmCB1fp56XL64D4Kf0zJSwDZbVLLm7KFkoIcHrgRvQ+sk8dnhySs7+yBg94yIkAK7aJg== + +redis-commands@^1.5.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.6.0.tgz#36d4ca42ae9ed29815cdb30ad9f97982eba1ce23" + integrity sha512-2jnZ0IkjZxvguITjFTrGiLyzQZcTvaw8DAaCXxZq/dsHXz7KfMQ3OUJy7Tz9vnRtZRVz6VRCPDvruvU8Ts44wQ== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= redis-parser@^2.4.0, redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" + integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" -redis@^2.8.0: +redis@^2.7.1, redis@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== @@ -7233,111 +8782,150 @@ redis@^2.8.0: redis-commands "^1.2.0" redis-parser "^2.6.0" +redis@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.0.2.tgz#bd47067b8a4a3e6a2e556e57f71cc82c7360150a" + integrity sha512-PNhLCrjU6vKVuMOyFu7oSP296mwBkcE6lrAjruBYG5LgdSqtRBoVQIylrMyVZD/lkF24RSNNatzvYag6HRBHjQ== + dependencies: + denque "^1.4.1" + redis-commands "^1.5.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + +referrer-policy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/referrer-policy/-/referrer-policy-1.2.0.tgz#b99cfb8b57090dc454895ef897a4cc35ef67a98e" + integrity sha512-LgQJIuS6nAy1Jd88DCQRemyE3mS+ispwlqMk3b0yjZ257fI1v9c+/p6SD5gP5FGyXUIgrNOAfmyioHwZtYv2VA== + regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== + +regenerator-runtime@^0.13.4: + version "0.13.7" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz#cac2dacc8a1ea675feaabaeb8ae833898ae46f55" + integrity sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew== regex-cache@^0.4.2: version "0.4.4" resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + integrity sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ== dependencies: is-equal-shallow "^0.1.3" regex-not@^1.0.0, regex-not@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c" + integrity sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A== dependencies: extend-shallow "^3.0.2" safe-regex "^1.1.0" -regexp-clone@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-0.0.1.tgz#a7c2e09891fdbf38fbb10d376fb73003e68ac589" +regexp-clone@1.0.0, regexp-clone@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/regexp-clone/-/regexp-clone-1.0.0.tgz#222db967623277056260b992626354a04ce9bf63" + integrity sha512-TuAasHQNamyyJ2hb97IuBEif4qBHGjPHBS64sZwytpLEqtBQ1gPJTnOaQ6qmpET16cK14kkjbazl6+p0RRv0yw== -registry-auth-token@^3.0.1: - version "3.4.0" - resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-3.4.0.tgz#d7446815433f5d5ed6431cd5dca21048f66b397e" - integrity sha512-4LM6Fw8eBQdwMYcES4yTnn2TqIasbXuwDx3um+QRs7S55aMKCBKBxvPXl2RiUjHwuJLTyYfxSpmfSAjQpcuP+A== +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== dependencies: - rc "^1.1.6" - safe-buffer "^5.0.1" + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" -registry-url@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-3.1.0.tgz#3d4ef870f73dde1d77f0cf9a381432444e174942" - integrity sha1-PU74cPc93h138M+aOBQyRE4XSUI= +registry-auth-token@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/registry-auth-token/-/registry-auth-token-4.1.1.tgz#40a33be1e82539460f94328b0f7f0f84c16d9479" + integrity sha512-9bKS7nTl9+/A1s7tnPeGrUpRcVY+LUh7bfFgzpndALdPfXQBfQV77rQVtqgUV3ti4vc/Ik81Ex8UJDWDQ12zQA== + dependencies: + rc "^1.2.8" + +registry-url@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/registry-url/-/registry-url-5.1.0.tgz#e98334b50d5434b81136b44ec638d9c2009c5009" + integrity sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw== dependencies: - rc "^1.0.1" + rc "^1.2.8" -release-it@^12.2.1, release-it@^12.3.0: - version "12.3.0" - resolved "https://registry.yarnpkg.com/release-it/-/release-it-12.3.0.tgz#2ac7c0d37d7773dcaacb4b29cde64c487862a512" - integrity sha512-mMEbdhyUDyX05viUl1pKcDLpNOxpgiig6OfvJXleNJqYKjIvNtYVkBxLHXH//kmT2NYnAkx8NvN8IZJQRei8zQ== +release-it@^12.4.3: + version "12.6.2" + resolved "https://registry.yarnpkg.com/release-it/-/release-it-12.6.2.tgz#85a7cee2a8b5d6bf1d8cf25443834ae3eef71b3e" + integrity sha512-2aLGlJMbmKBoWj46OGh0RxsMqcq1J9C0W58warVNV54cSr7PserFYtk2rd6XWEy3RUcWTtF66mPYBjyCVCMB1w== dependencies: "@iarna/toml" "2.2.3" - "@octokit/rest" "16.25.0" - async-retry "1.2.3" - chalk "2.4.2" - cosmiconfig "5.2.0" + "@octokit/rest" "16.43.1" + async-retry "1.3.1" + chalk "3.0.0" + cosmiconfig "5.2.1" debug "4.1.1" deprecated-obj "1.0.1" detect-repo-changelog "1.0.1" - find-up "3.0.0" - form-data "2.3.3" + find-up "4.1.0" + form-data "3.0.0" git-url-parse "11.1.2" - globby "9.2.0" + globby "10.0.2" got "9.6.0" - import-cwd "2.1.0" - inquirer "6.3.1" + import-cwd "3.0.0" + inquirer "7.0.4" is-ci "2.0.0" - lodash "4.17.11" - mime-types "2.1.24" - ora "3.4.0" + lodash "4.17.15" + mime-types "2.1.26" + ora "4.0.3" os-name "3.1.0" - semver "6.0.0" + semver "7.1.3" + shell-quote "1.7.2" shelljs "0.8.3" - supports-color "6.1.0" - update-notifier "2.5.0" - url-join "4.0.0" - uuid "3.3.2" + supports-color "7.1.0" + update-notifier "4.1.0" + url-join "4.0.1" + uuid "7.0.1" window-size "1.1.1" - yargs-parser "13.0.0" + yargs-parser "17.0.0" remove-trailing-separator@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + integrity sha1-wkvOKig62tW8P1jg1IJJuSN52O8= repeat-element@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce" + integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== repeat-string@^1.5.2, repeat-string@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= repeating@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + integrity sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo= dependencies: is-finite "^1.0.0" -request-promise-core@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6" +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== dependencies: - lodash "^4.13.1" + lodash "^4.17.15" request-promise-native@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.5.tgz#5281770f68e0c9719e5163fd3fab482215f4fda5" + version "1.0.8" + resolved "https://registry.yarnpkg.com/request-promise-native/-/request-promise-native-1.0.8.tgz#a455b960b826e44e2bf8999af64dff2bfe58cb36" + integrity sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ== dependencies: - request-promise-core "1.1.1" - stealthy-require "^1.1.0" - tough-cookie ">=2.3.3" + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" -request@^2.68.0, request@^2.79.0, request@^2.81.0, request@^2.87.0, request@^2.88.0: - version "2.88.0" - resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef" +request@^2.87.0, request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== dependencies: aws-sign2 "~0.7.0" aws4 "^1.8.0" @@ -7346,7 +8934,7 @@ request@^2.68.0, request@^2.79.0, request@^2.81.0, request@^2.87.0, request@^2.8 extend "~3.0.2" forever-agent "~0.6.1" form-data "~2.3.2" - har-validator "~5.1.0" + har-validator "~5.1.3" http-signature "~1.2.0" is-typedarray "~1.0.0" isstream "~0.1.2" @@ -7356,13 +8944,14 @@ request@^2.68.0, request@^2.79.0, request@^2.81.0, request@^2.87.0, request@^2.8 performance-now "^2.1.0" qs "~6.5.2" safe-buffer "^5.1.2" - tough-cookie "~2.4.3" + tough-cookie "~2.5.0" tunnel-agent "^0.6.0" uuid "^3.3.2" requestify@^0.2.5: version "0.2.5" resolved "https://registry.yarnpkg.com/requestify/-/requestify-0.2.5.tgz#80249f1ca7dfdf79fa2a6048aeac37d43e23c905" + integrity sha1-gCSfHKff33n6KmBIrqw31D4jyQU= dependencies: jquery "^3.1.0" q "^0.9.7" @@ -7371,55 +8960,72 @@ requestify@^0.2.5: require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= require-from-string@^1.1.0: version "1.2.1" resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-1.2.1.tgz#529c9ccef27380adfec9a2f965b649bbee636418" + integrity sha1-UpyczvJzgK3+yaL5ZbZJu+5jZBg= require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" + integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== require_optional@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require_optional/-/require_optional-1.0.1.tgz#4cf35a4247f64ca3df8c2ef208cc494b1ca8fc2e" + integrity sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g== dependencies: resolve-from "^2.0.0" semver "^5.1.0" +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= + +resolve-cwd@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a" + integrity sha1-AKn3OHVW4nA46uIyyqNypqWbZlo= + dependencies: + resolve-from "^3.0.0" + resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" + integrity sha1-lICrIOlP+h2egKgEx+oUdhGWa1c= resolve-from@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-3.0.0.tgz#b22c7af7d9d6881bc8b6e653335eebcb0a188748" integrity sha1-six699nWiBvItuZTM17rywoYh0g= +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-url@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" + integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= resolve@1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" + integrity sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs= -resolve@^1.0.0, resolve@^1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" - dependencies: - path-parse "^1.0.6" - -resolve@^1.1.6: - version "1.11.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.11.1.tgz#ea10d8110376982fef578df8fc30b9ac30a07a3e" - integrity sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw== - dependencies: - path-parse "^1.0.6" - -resolve@^1.1.7, resolve@^1.3.2: - version "1.9.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.9.0.tgz#a14c6fdfa8f92a7df1d996cb7105fa744658ea06" +resolve@^1.0.0, resolve@^1.1.6, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.3.2: + version "1.15.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.15.1.tgz#27bdcdeffeaf2d6244b95bb0f9f4b4653451f3e8" + integrity sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w== dependencies: path-parse "^1.0.6" @@ -7433,6 +9039,7 @@ responselike@^1.0.2: restore-cursor@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + integrity sha1-NGYfRohjJ/7SmRR5FSJS35LapUE= dependencies: exit-hook "^1.0.0" onetime "^1.0.0" @@ -7445,144 +9052,167 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" - -retry-axios@0.3.2, retry-axios@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-0.3.2.tgz#5757c80f585b4cc4c4986aa2ffd47a60c6d35e13" - -retry-request@^3.0.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-3.3.2.tgz#fd8e0079e7b0dfc7056e500b6f089437db0da4df" - dependencies: - request "^2.81.0" - through2 "^2.0.0" + integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== retry-request@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.0.0.tgz#5c366166279b3e10e9d7aa13274467a05cb69290" + version "4.1.1" + resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-4.1.1.tgz#f676d0db0de7a6f122c048626ce7ce12101d2bd8" + integrity sha512-BINDzVtLI2BDukjWmjAIRZ0oglnCAkpP2vQjM3jdLhmT62h0xnQgciPwBRDAvHqpkPT2Wo1XuUyLyn6nbGrZQQ== dependencies: - through2 "^2.0.0" + debug "^4.1.1" + through2 "^3.0.1" -retry@0.12.0: +retry@0.12.0, retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" + integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -rimraf@^2.6.1: - version "2.6.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" - dependencies: - glob "^7.0.5" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== +rimraf@2, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== dependencies: glob "^7.1.3" -rsvp@^3.3.3: - version "3.6.2" - resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-3.6.2.tgz#2e96491599a96cde1b515d5674a8f7a91452926a" +rsvp@^4.8.4: + version "4.8.5" + resolved "https://registry.yarnpkg.com/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" + integrity sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA== run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + version "2.4.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8" + integrity sha512-xJTbh/d7Lm7SBhc1tNvTpeCHaEzoyxPrqNlvSdMfBTYwaY++UJFyXUOxAtsRUXjlqOfj8luNaR9vjCh4KeV+pg== dependencies: is-promise "^2.1.0" +run-parallel@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.1.9.tgz#c9dd3a7cf9f4b2c4b6244e173a6ed866e61dd679" + integrity sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q== + rxjs@^5.0.0-beta.11: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" + integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== dependencies: symbol-observable "1.0.1" -rxjs@^6.4.0: - version "6.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.2.tgz#2e35ce815cd46d84d02a209fb4e5921e051dbec7" - integrity sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg== +rxjs@^6.5.3: + version "6.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.4.tgz#e0777fe0d184cec7872df147f303572d414e211c" + integrity sha512-naMQXcgEo3csAEGvw/NydRA0fuS2nDZJiw1YUWFKU7aPPAPGZEsD4Iimit96qwCieH6y614MCLYwdkrWx7z/7Q== dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1, safe-buffer@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== safe-regex@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e" + integrity sha1-QKNmnzsHfR6UPURinhV91IAjvy4= dependencies: ret "~0.1.10" "safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== -sane@^2.0.0: - version "2.5.2" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.5.2.tgz#b4dc1861c21b427e929507a3e751e2a2cb8ab3fa" +sane@^4.0.3: + version "4.1.0" + resolved "https://registry.yarnpkg.com/sane/-/sane-4.1.0.tgz#ed881fd922733a6c461bc189dc2b6c006f3ffded" + integrity sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA== dependencies: + "@cnakazawa/watch" "^1.0.3" anymatch "^2.0.0" - capture-exit "^1.2.0" - exec-sh "^0.2.0" + capture-exit "^2.0.0" + exec-sh "^0.3.2" + execa "^1.0.0" fb-watchman "^2.0.0" micromatch "^3.1.4" minimist "^1.1.1" walker "~1.0.5" - watch "~0.18.0" - optionalDependencies: - fsevents "^1.2.3" saslprep@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.2.tgz#da5ab936e6ea0bbae911ffec77534be370c9f52d" + version "1.0.3" + resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226" + integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag== dependencies: sparse-bitfield "^3.0.3" sax@1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.1.tgz#7b8e656190b228e81a66aea748480d828cd2d37a" + integrity sha1-e45lYZCyKOgaZq6nSEgNgozS03o= -sax@>=0.6.0, sax@^1.2.1, sax@^1.2.4: +sax@>=0.6.0, sax@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== -secure-keys@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/secure-keys/-/secure-keys-1.0.0.tgz#f0c82d98a3b139a8776a8808050b824431087fca" - integrity sha1-8MgtmKOxOah3aogIBQuCRDEIf8o= +seek-bzip@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc" + integrity sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w= + dependencies: + commander "~2.8.1" -semver-diff@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" - integrity sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY= +semver-diff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-3.1.1.tgz#05f77ce59f325e00e2706afd67bb506ddb1ca32b" + integrity sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg== dependencies: - semver "^5.0.3" + semver "^6.3.0" -"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.5.0: - version "5.6.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" +"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== -semver@6.0.0, semver@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.0.0.tgz#05e359ee571e5ad7ed641a6eec1e547ba52dea65" +semver@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.1.3.tgz#e4345ce73071c53f336445cfc19efb1c311df2a6" + integrity sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA== -semver@^5.0.3, semver@^5.5.1: - version "5.7.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" - integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^6.1.0, semver@^6.1.2: - version "6.2.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.2.0.tgz#4d813d9590aaf8a9192693d6c85b9344de5901db" - integrity sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -send@0.16.2: - version "0.16.2" - resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" +send@0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" + integrity sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg== dependencies: debug "2.6.9" depd "~1.1.2" @@ -7591,90 +9221,99 @@ send@0.16.2: escape-html "~1.0.3" etag "~1.8.1" fresh "0.5.2" - http-errors "~1.6.2" - mime "1.4.1" - ms "2.0.0" + http-errors "~1.7.2" + mime "1.6.0" + ms "2.1.1" on-finished "~2.3.0" - range-parser "~1.2.0" - statuses "~1.4.0" + range-parser "~1.2.1" + statuses "~1.5.0" -serve-static@1.13.2: - version "1.13.2" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" +serve-static@1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.14.1.tgz#666e636dc4f010f7ef29970a88a674320898b2f9" + integrity sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg== dependencies: encodeurl "~1.0.2" escape-html "~1.0.3" - parseurl "~1.3.2" - send "0.16.2" + parseurl "~1.3.3" + send "0.17.1" -set-blocking@^2.0.0, set-blocking@~2.0.0: +set-blocking@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= -set-value@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-0.4.3.tgz#7db08f9d3d22dc7f78e53af3c3bf4666ecdfccf1" - dependencies: - extend-shallow "^2.0.1" - is-extendable "^0.1.1" - is-plain-object "^2.0.1" - to-object-path "^0.3.0" +set-immediate-shim@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E= -set-value@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.0.tgz#71ae4a88f0feefbbf52d1ea604f3fb315ebb6274" +set-value@^2.0.0, set-value@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b" + integrity sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw== dependencies: extend-shallow "^2.0.1" is-extendable "^0.1.1" is-plain-object "^2.0.3" split-string "^3.0.1" -setprototypeof@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" +setimmediate@~1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" integrity sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw== +sha.js@^2.4.11: + version "2.4.11" + resolved "https://registry.yarnpkg.com/sha.js/-/sha.js-2.4.11.tgz#37a5cf0b81ecbc6943de109ba2960d1b26584ae7" + integrity sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ== + dependencies: + inherits "^2.0.1" + safe-buffer "^5.0.1" + sha256@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/sha256/-/sha256-0.2.0.tgz#73a0b418daab7035bff86e8491e363412fc2ab05" + integrity sha1-c6C0GNqrcDW/+G6EkeNjQS/CqwU= dependencies: convert-hex "~0.1.0" convert-string "~0.1.0" -shallow-clone@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-0.1.2.tgz#5909e874ba77106d73ac414cfec1ffca87d97060" - integrity sha1-WQnodLp3EG1zrEFM/sH/yofZcGA= - dependencies: - is-extendable "^0.1.1" - kind-of "^2.0.1" - lazy-cache "^0.2.3" - mixin-object "^2.0.1" - shebang-command@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= dependencies: shebang-regex "^1.0.0" +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= -shell-quote@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767" - dependencies: - array-filter "~0.0.0" - array-map "~0.0.0" - array-reduce "~0.0.0" - jsonify "~0.0.0" +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@1.7.2, shell-quote@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" + integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@0.8.3: +shelljs@0.8.3, shelljs@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.3.tgz#a7f3319520ebf09ee81275b2368adb286659b097" integrity sha512-fc0BKlAWiLpwZljmOvAOTE/gXawtCoNrP5oaY7KIaQbbyHeQVg01pSEuEGvGh3HEdBU4baCD7wQBwADmM/7f7A== @@ -7683,61 +9322,88 @@ shelljs@0.8.3: interpret "^1.0.0" rechoir "^0.6.2" -shellwords@^0.1.0, shellwords@^0.1.1: +shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" + integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== + +side-channel@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.2.tgz#df5d1abadb4e4bf4af1cd8852bf132d2f7876947" + integrity sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA== + dependencies: + es-abstract "^1.17.0-next.1" + object-inspect "^1.7.0" + +sift@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/sift/-/sift-7.0.1.tgz#47d62c50b159d316f1372f8b53f9c10cd21a4b08" + integrity sha512-oqD7PMJ+uO6jV9EQCl0LrRw1OwsiPsiFQR5AR30heR+4Dl7jBBbDLnNvWiak20tzZlSE1H7RB30SX/1j/YYT7g== signal-exit@^3.0.0, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= sinon@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.2.2.tgz#388ecabd42fa93c592bfc71d35a70894d5a0ca07" + version "7.5.0" + resolved "https://registry.yarnpkg.com/sinon/-/sinon-7.5.0.tgz#e9488ea466070ea908fd44a3d6478fd4923c67ec" + integrity sha512-AoD0oJWerp0/rY9czP/D6hDTTUYGpObhZjMpd7Cl/A6+j0xBE+ayL/ldfggkBXUs0IkvIiM1ljM8+WkOc5k78Q== dependencies: - "@sinonjs/commons" "^1.2.0" - "@sinonjs/formatio" "^3.1.0" - "@sinonjs/samsam" "^3.0.2" + "@sinonjs/commons" "^1.4.0" + "@sinonjs/formatio" "^3.2.1" + "@sinonjs/samsam" "^3.3.3" diff "^3.5.0" - lolex "^3.0.0" - nise "^1.4.7" + lolex "^4.2.0" + nise "^1.5.2" supports-color "^5.5.0" +sisteransi@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.4.tgz#386713f1ef688c7c0304dc4c0632898941cad2e3" + integrity sha512-/ekMoM4NJ59ivGSfKapeG+FWtrmWvA1p6FBZwXrqojw90vJu8lBmrTxCMuBCydKtkaUe2zt4PlxeTKpjwMbyig== + slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + integrity sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU= slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" integrity sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A== +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + slice-ansi@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + integrity sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU= sliced@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/sliced/-/sliced-1.0.1.tgz#0b3a662b5d04c3177b1926bea82b03f837a2ef41" + integrity sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E= slug@^0.9.2: - version "0.9.3" - resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.3.tgz#8c9c773d79367c0188733316cf49fd2b8db40f6a" + version "0.9.4" + resolved "https://registry.yarnpkg.com/slug/-/slug-0.9.4.tgz#fad5f1ef33150830c7688cd8500514576eccabd8" + integrity sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g== dependencies: unicode ">= 0.3.1" -smart-buffer@4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.0.2.tgz#5207858c3815cc69110703c6b94e46c15634395d" - integrity sha512-JDhEpTKzXusOqXZ0BUIdH+CjFdO/CR3tLlf5CN34IypI+xMmXW1uB16OOY8z3cICbJlDAVJzNbwBhNO0wt9OAw== - snakeize@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/snakeize/-/snakeize-0.1.0.tgz#10c088d8b58eb076b3229bb5a04e232ce126422d" + integrity sha1-EMCI2LWOsHazIpu1oE4jLOEmQi0= snapdragon-node@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b" + integrity sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw== dependencies: define-property "^1.0.0" isobject "^3.0.0" @@ -7746,12 +9412,14 @@ snapdragon-node@^2.0.1: snapdragon-util@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/snapdragon-util/-/snapdragon-util-3.0.1.tgz#f956479486f2acd79700693f6f7b805e45ab56e2" + integrity sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ== dependencies: kind-of "^3.2.0" snapdragon@^0.8.1: version "0.8.2" resolved "https://registry.yarnpkg.com/snapdragon/-/snapdragon-0.8.2.tgz#64922e7c565b0e14204ba1aa7d6964278d25182d" + integrity sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg== dependencies: base "^0.11.1" debug "^2.2.0" @@ -7762,260 +9430,72 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" -snyk-config@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/snyk-config/-/snyk-config-2.2.1.tgz#bdacf79193158ec659bdcc4194140fd8d3772f9d" - integrity sha512-eCsFKHHE4J2DpD/1NzAtCmkmVDK310OXRtmoW0RlLnld1ESprJ5A/QRJ5Zxx1JbA8gjuwERY5vfUFA8lEJeopA== - dependencies: - debug "^3.1.0" - lodash "^4.17.11" - nconf "^0.10.0" - -snyk-docker-plugin@1.25.1: - version "1.25.1" - resolved "https://registry.yarnpkg.com/snyk-docker-plugin/-/snyk-docker-plugin-1.25.1.tgz#3f97dda88adfac2e1938151372d07905767bc8a1" - integrity sha512-n/LfA7VXjPEcSz2ZfZonT/DPSC89Zs1/HD0inPFN4RLQT3WiQnjqJUXct+D0nWwEVfhLWNc+Y7PLcTjpnZ9R3Q== - dependencies: - debug "^4.1.1" - dockerfile-ast "0.0.16" - semver "^6.1.0" - tslib "^1" - -snyk-go-parser@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/snyk-go-parser/-/snyk-go-parser-1.3.1.tgz#427387507578baf008a3e73828e0e53ed8c796f3" - integrity sha512-jrFRfIk6yGHFeipGD66WV9ei/A/w/lIiGqI80w1ndMbg6D6M5pVNbK7ngDTmo4GdHrZDYqx/VBGBsUm2bol3Rg== - dependencies: - toml "^3.0.0" - tslib "^1.9.3" - -snyk-go-plugin@1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/snyk-go-plugin/-/snyk-go-plugin-1.10.2.tgz#520ace2d84df4d3c5088d2cbc776ef2d5ac0f236" - integrity sha512-k+f/0XgiAfnqK36L3t3EBYyMy8/vVFAU9ctHO5BztaXZXMfkYZpRsJGbvR3c7cVE4n4ruwYQhlKLM8bCuai8SQ== - dependencies: - debug "^4.1.1" - graphlib "^2.1.1" - snyk-go-parser "1.3.1" - tmp "0.0.33" - -snyk-gradle-plugin@2.12.5: - version "2.12.5" - resolved "https://registry.yarnpkg.com/snyk-gradle-plugin/-/snyk-gradle-plugin-2.12.5.tgz#6da1c9135b4cee2d6cd32653e569a1f56977d173" - integrity sha512-AmiQQUL0nlY3SjWUSMSmmbp273ETJzsqvk1E8jf+G/Q3mRl9xZ6BkPMebweD/y5d/smoQmr6rKL57OG+OXoi3w== - dependencies: - "@types/debug" "^4.1.4" - chalk "^2.4.2" - clone-deep "^0.3.0" - debug "^4.1.1" - tmp "0.0.33" - tslib "^1.9.3" - -snyk-module@1.9.1, snyk-module@^1.6.0, snyk-module@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/snyk-module/-/snyk-module-1.9.1.tgz#b2a78f736600b0ab680f1703466ed7309c980804" - integrity sha512-A+CCyBSa4IKok5uEhqT+hV/35RO6APFNLqk9DRRHg7xW2/j//nPX8wTSZUPF8QeRNEk/sX+6df7M1y6PBHGSHA== - dependencies: - debug "^3.1.0" - hosted-git-info "^2.7.1" +socket.io-adapter@~1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz#ab3f0d6f66b8fc7fca3959ab5991f82221789be9" + integrity sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g== -snyk-mvn-plugin@2.3.0: +socket.io-client@2.3.0: version "2.3.0" - resolved "https://registry.yarnpkg.com/snyk-mvn-plugin/-/snyk-mvn-plugin-2.3.0.tgz#a76cfc0041ecc4333f2c6f6f72138f1e4621da24" - integrity sha512-LOSiJu+XUPVqKCXcnQPLhlyTGm3ikDwjvYw5fpiEnvjMWkMDd8IfzZqulqreebJDmadUpP7Cn0fabfx7TszqxA== - dependencies: - lodash "4.17.11" - tslib "1.9.3" - -snyk-nodejs-lockfile-parser@1.13.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/snyk-nodejs-lockfile-parser/-/snyk-nodejs-lockfile-parser-1.13.0.tgz#f3c81fd9a1870fdb5f71370e510d760326f3ee21" - integrity sha512-fC1o9SJ+iM+IYeBUYtvCIYh005WAvWMzqhEH3hI4zGPdCYQqGYIfVpXf29aCOKoorkTR345k5g6Etx54+BbrTQ== - dependencies: - "@yarnpkg/lockfile" "^1.0.2" - graphlib "^2.1.5" - lodash "^4.17.11" - source-map-support "^0.5.7" - tslib "^1.9.3" - uuid "^3.3.2" - -snyk-nuget-plugin@1.10.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/snyk-nuget-plugin/-/snyk-nuget-plugin-1.10.0.tgz#58aba49e37ca4ac99afcecb3d3c3917188daad84" - integrity sha512-V69AIWcHw4KrgEFC8kNWoqHo54wZkWGfqyVv+kJjQxARWYmQqV4YL/vxfLAoZ7mDsNXgjPn5M4ZEaeHFCeWcyA== - dependencies: - debug "^3.1.0" - jszip "^3.1.5" - lodash "^4.17.10" - snyk-paket-parser "1.4.3" - xml2js "^0.4.17" - -snyk-paket-parser@1.4.3: - version "1.4.3" - resolved "https://registry.yarnpkg.com/snyk-paket-parser/-/snyk-paket-parser-1.4.3.tgz#380ae8c5fb598f81c110f6b645c728c9cc50b7a5" - integrity sha512-6m736zGVoeT/zS9KEtlmqTSPEPjAfLe8iYoQ3AwbyxDhzuLY49lTaV67MyZtGwjhi1x4KBe+XOgeWwyf6Avf/A== - dependencies: - tslib "^1.9.3" - -snyk-php-plugin@1.6.2: - version "1.6.2" - resolved "https://registry.yarnpkg.com/snyk-php-plugin/-/snyk-php-plugin-1.6.2.tgz#f5ad6f081d2afc6dfc496cbce68165bdcd2e87ed" - integrity sha512-6QM7HCmdfhuXSNGFgNOVC+GVT1Y2UfBoO+TAeV1uM1CdRGPJziz12F79a1Qyc9YGuiAwmm5DtdatUgKraC8gdA== - dependencies: - "@snyk/composer-lockfile-parser" "1.0.2" - -snyk-policy@1.13.5: - version "1.13.5" - resolved "https://registry.yarnpkg.com/snyk-policy/-/snyk-policy-1.13.5.tgz#c5cf262f759879a65ab0810dd58d59c8ec7e9e47" - integrity sha512-KI6GHt+Oj4fYKiCp7duhseUj5YhyL/zJOrrJg0u6r59Ux9w8gmkUYT92FHW27ihwuT6IPzdGNEuy06Yv2C9WaQ== - dependencies: - debug "^3.1.0" - email-validator "^2.0.4" - js-yaml "^3.13.1" - lodash.clonedeep "^4.5.0" - semver "^6.0.0" - snyk-module "^1.9.1" - snyk-resolve "^1.0.1" - snyk-try-require "^1.3.1" - then-fs "^2.0.0" - -snyk-python-plugin@1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/snyk-python-plugin/-/snyk-python-plugin-1.10.2.tgz#e89548a47d4cfe98351604ed8a3372bfd9fbebbd" - integrity sha512-dLswHfVI9Ax8+Ia/onhv1p9S5y+Ie/oELOfpfNApbb0BPTJ5k1c2CQ7WcgQ5/nDRMUOgoKn4VTObaAGmD5or9A== - dependencies: - tmp "0.0.33" - -snyk-resolve-deps@4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/snyk-resolve-deps/-/snyk-resolve-deps-4.0.3.tgz#f44389430c3712af8f574952e9ff188c6448dbd7" - integrity sha512-GP3VBrkz1iDDw2q8ftTqppHqzIAxmsUIoXR+FRWDKcipkKHXHJyUmtEo11QVT5fNRV0D0RCsssk2S5CTxTCu6A== - dependencies: - ansicolors "^0.3.2" - debug "^3.2.5" - lodash.assign "^4.2.0" - lodash.assignin "^4.2.0" - lodash.clone "^4.5.0" - lodash.flatten "^4.4.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lru-cache "^4.0.0" - semver "^5.5.1" - snyk-module "^1.6.0" - snyk-resolve "^1.0.0" - snyk-tree "^1.0.0" - snyk-try-require "^1.1.1" - then-fs "^2.0.0" - -snyk-resolve@1.0.1, snyk-resolve@^1.0.0, snyk-resolve@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/snyk-resolve/-/snyk-resolve-1.0.1.tgz#eaa4a275cf7e2b579f18da5b188fe601b8eed9ab" - integrity sha512-7+i+LLhtBo1Pkth01xv+RYJU8a67zmJ8WFFPvSxyCjdlKIcsps4hPQFebhz+0gC5rMemlaeIV6cqwqUf9PEDpw== - dependencies: - debug "^3.1.0" - then-fs "^2.0.0" - -snyk-sbt-plugin@2.5.5: - version "2.5.5" - resolved "https://registry.yarnpkg.com/snyk-sbt-plugin/-/snyk-sbt-plugin-2.5.5.tgz#b7839bc297f41aadc64b32d61e8dbaa609303967" - integrity sha512-oSybTDLw8VF2nOdlbL7GRHafCxsM6ydTH6hKacvpN6mYDbNaohscAWB/FjLIPCCimVorWldEdSdotSCukq2eYg== - dependencies: - child_process "1.0.2" - fs "0.0.1-security" - path "0.12.7" - semver "^6.1.2" - tmp "^0.1.0" - tree-kill "^1.2.1" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-2.3.0.tgz#14d5ba2e00b9bcd145ae443ab96b3f86cbcc1bb4" + integrity sha512-cEQQf24gET3rfhxZ2jJ5xzAOo/xhZwK+mOqtGRg5IowZsMgwvHwnf/mCRapAAkadhM26y+iydgwsXGObBB5ZdA== + dependencies: + backo2 "1.0.2" + base64-arraybuffer "0.1.5" + component-bind "1.0.0" + component-emitter "1.2.1" + debug "~4.1.0" + engine.io-client "~3.4.0" + has-binary2 "~1.0.2" + has-cors "1.1.0" + indexof "0.0.1" + object-component "0.0.3" + parseqs "0.0.5" + parseuri "0.0.5" + socket.io-parser "~3.3.0" + to-array "0.1.4" -snyk-tree@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/snyk-tree/-/snyk-tree-1.0.0.tgz#0fb73176dbf32e782f19100294160448f9111cc8" - integrity sha1-D7cxdtvzLngvGRAClBYESPkRHMg= +socket.io-parser@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.3.0.tgz#2b52a96a509fdf31440ba40fed6094c7d4f1262f" + integrity sha512-hczmV6bDgdaEbVqhAeVMM/jfUfzuEZHsQg6eOmLgJht6G3mPKMxYm75w2+qhAQZ+4X+1+ATZ+QFKeOZD5riHng== dependencies: - archy "^1.0.0" - -snyk-try-require@1.3.1, snyk-try-require@^1.1.1, snyk-try-require@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/snyk-try-require/-/snyk-try-require-1.3.1.tgz#6e026f92e64af7fcccea1ee53d524841e418a212" - integrity sha1-bgJvkuZK9/zM6h7lPVJIQeQYohI= - dependencies: - debug "^3.1.0" - lodash.clonedeep "^4.3.0" - lru-cache "^4.0.0" - then-fs "^2.0.0" - -snyk@^1.192.4: - version "1.192.4" - resolved "https://registry.yarnpkg.com/snyk/-/snyk-1.192.4.tgz#ed1d484d42525cef7e7520ffd21c32182c2b4b50" - integrity sha512-5gXOqNl2ehyt5LDP3sh10vObCboNlQ38dkmMAOdYKqE2ZojpOUYk8OQdL8tK5MEPgEWt76FeGv1nFNg5XTLUSQ== - dependencies: - "@snyk/dep-graph" "1.8.1" - "@snyk/gemfile" "1.2.0" - "@types/agent-base" "^4.2.0" - abbrev "^1.1.1" - ansi-escapes "^4.1.0" - chalk "^2.4.2" - configstore "^3.1.2" - debug "^3.1.0" - diff "^4.0.1" - git-url-parse "11.1.2" - glob "^7.1.3" - inquirer "^6.2.2" - lodash "^4.17.11" - needle "^2.2.4" - opn "^5.5.0" - os-name "^3.0.0" - proxy-agent "^3.1.0" - proxy-from-env "^1.0.0" - semver "^6.0.0" - snyk-config "^2.2.1" - snyk-docker-plugin "1.25.1" - snyk-go-plugin "1.10.2" - snyk-gradle-plugin "2.12.5" - snyk-module "1.9.1" - snyk-mvn-plugin "2.3.0" - snyk-nodejs-lockfile-parser "1.13.0" - snyk-nuget-plugin "1.10.0" - snyk-php-plugin "1.6.2" - snyk-policy "1.13.5" - snyk-python-plugin "1.10.2" - snyk-resolve "1.0.1" - snyk-resolve-deps "4.0.3" - snyk-sbt-plugin "2.5.5" - snyk-tree "^1.0.0" - snyk-try-require "1.3.1" - source-map-support "^0.5.11" - strip-ansi "^5.2.0" - tempfile "^2.0.0" - then-fs "^2.0.0" - update-notifier "^2.5.0" - uuid "^3.3.2" + component-emitter "1.2.1" + debug "~3.1.0" + isarray "2.0.1" -socks-proxy-agent@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" - integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== +socket.io-parser@~3.4.0: + version "3.4.1" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-3.4.1.tgz#b06af838302975837eab2dc980037da24054d64a" + integrity sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A== dependencies: - agent-base "~4.2.1" - socks "~2.3.2" + component-emitter "1.2.1" + debug "~4.1.0" + isarray "2.0.1" -socks@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/socks/-/socks-2.3.2.tgz#ade388e9e6d87fdb11649c15746c578922a5883e" - integrity sha512-pCpjxQgOByDHLlNqlnh/mNSAxIUkyBBuwwhTcV+enZGbDaClPvHdvm6uvOwZfFJkam7cGhBNbb4JxiP8UZkRvQ== +socket.io@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/socket.io/-/socket.io-2.3.0.tgz#cd762ed6a4faeca59bc1f3e243c0969311eb73fb" + integrity sha512-2A892lrj0GcgR/9Qk81EaY2gYhCBxurV0PfmmESO6p27QPrUK1J3zdns+5QPqvUYK2q657nSj0guoIil9+7eFg== dependencies: - ip "^1.1.5" - smart-buffer "4.0.2" + debug "~4.1.0" + engine.io "~3.4.0" + has-binary2 "~1.0.2" + socket.io-adapter "~1.1.0" + socket.io-client "2.3.0" + socket.io-parser "~3.4.0" sorted-array-functions@^1.0.0: version "1.2.0" resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.2.0.tgz#43265b21d6e985b7df31621b1c11cc68d8efc7c3" + integrity sha512-sWpjPhIZJtqO77GN+LD8dDsDKcWZ9GCOJNqKzi1tvtjGIzwfoyuRH8S0psunmc6Z5P+qfDqztSbwYR5X/e1UTg== source-map-resolve@^0.5.0: - version "0.5.2" - resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.2.tgz#72e2cc34095543e43b2c62b2c4c10d4a9054f259" + version "0.5.3" + resolved "https://registry.yarnpkg.com/source-map-resolve/-/source-map-resolve-0.5.3.tgz#190866bece7553e1f8f267a2ee82c606b5509a1a" + integrity sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw== dependencies: - atob "^2.1.1" + atob "^2.1.2" decode-uri-component "^0.2.0" resolve-url "^0.2.1" source-map-url "^0.4.0" @@ -8024,20 +9504,14 @@ source-map-resolve@^0.5.0: source-map-support@^0.4.15: version "0.4.18" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + integrity sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA== dependencies: source-map "^0.5.6" -source-map-support@^0.5.0, source-map-support@^0.5.5, source-map-support@^0.5.6: - version "0.5.9" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.9.tgz#41bc953b2534267ea2d605bccfa7bfa3111ced5f" - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.11, source-map-support@^0.5.7: - version "0.5.12" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.12.tgz#b4f3b10d51857a5af0138d3ce8003b201613d599" - integrity sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ== +source-map-support@^0.5.0, source-map-support@^0.5.12, source-map-support@^0.5.5, source-map-support@^0.5.6: + version "0.5.16" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.16.tgz#0ae069e7fe3ba7538c64c98515e35339eac5a042" + integrity sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -8045,24 +9519,29 @@ source-map-support@^0.5.11, source-map-support@^0.5.7: source-map-url@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.4.0.tgz#3e935d7ddd73631b97659956d55128e87b5084a3" + integrity sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM= -source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7: +source-map@^0.5.0, source-map@^0.5.6, source-map@^0.5.7: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== sparse-bitfield@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz#ff4ae6e68656056ba4b3e792ab3334d38273ca11" + integrity sha1-/0rm5oZWBWuks+eSqzM004JzyhE= dependencies: memory-pager "^1.0.2" spdx-correct@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -8070,34 +9549,25 @@ spdx-correct@^3.0.0: spdx-exceptions@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== spdx-expression-parse@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.3.tgz#81c0ce8f21474756148bbb5f3bfc0f36bf15d76e" - -split-array-stream@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/split-array-stream/-/split-array-stream-1.0.3.tgz#d2b75a8e5e0d824d52fdec8b8225839dc2e35dfa" - dependencies: - async "^2.4.0" - is-stream-ended "^0.1.0" - -split-array-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/split-array-stream/-/split-array-stream-2.0.0.tgz#85a4f8bfe14421d7bca7f33a6d176d0c076a53b1" - dependencies: - is-stream-ended "^0.1.4" + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== split-string@^3.0.1, split-string@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2" + integrity sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw== dependencies: extend-shallow "^3.0.0" @@ -8118,10 +9588,19 @@ split@^1.0.0: sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +ssf@^0.10.2: + version "0.10.3" + resolved "https://registry.yarnpkg.com/ssf/-/ssf-0.10.3.tgz#8eae1fc29c90a552e7921208f81892d6f77acb2b" + integrity sha512-pRuUdW0WwyB2doSqqjWyzwCD6PkfxpHAHdZp39K3dp/Hq7f+xfMwNAWIi16DyrRg4gg9c/RvLYkJTSawTPTm1w== + dependencies: + frac "~1.1.2" sshpk@^1.7.0: - version "1.16.0" - resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.0.tgz#1d4963a2fbffe58050aa9084ca20be81741c07de" + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== dependencies: asn1 "~0.2.3" assert-plus "^1.0.0" @@ -8136,55 +9615,57 @@ sshpk@^1.7.0: stack-utils@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" + integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== staged-git-files@0.0.4: version "0.0.4" resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" + integrity sha1-15fhtVHKemOd7AI33G60u5vhfTU= static-extend@^0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6" + integrity sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY= dependencies: define-property "^0.2.5" object-copy "^0.1.0" -"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2": +"statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= -statuses@~1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" - -stealthy-require@^1.1.0: +stealthy-require@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= -stream-events@^1.0.1, stream-events@^1.0.4: +stream-events@^1.0.1, stream-events@^1.0.4, stream-events@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/stream-events/-/stream-events-1.0.5.tgz#bbc898ec4df33a4902d892333d47da9bf1c406d5" + integrity sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg== dependencies: stubs "^3.0.0" stream-shift@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" + version "1.0.1" + resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d" + integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ== stream-to-observable@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.1.0.tgz#45bf1d9f2d7dc09bed81f1c307c430e68b84cffe" + integrity sha1-Rb8dny19wJvtgfHDB8Qw5ouEz/4= streamsearch@0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a" - -string-format-obj@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string-format-obj/-/string-format-obj-1.1.1.tgz#c7612ca4e2ad923812a81db192dc291850aa1f65" + integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo= string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" + integrity sha1-1A27aGo6zpYMHP/KVivyxF+DY+0= dependencies: astral-regex "^1.0.0" strip-ansi "^4.0.0" @@ -8192,78 +9673,129 @@ string-length@^2.0.0: string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= dependencies: code-point-at "^1.0.0" is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== dependencies: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string-width@^4.0.0, string-width@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5" + integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.0" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + string_decoder@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d" + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - safe-buffer "~5.1.0" + safe-buffer "~5.2.0" string_decoder@~0.10.x: version "0.10.31" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== dependencies: safe-buffer "~5.1.0" -stringifier@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/stringifier/-/stringifier-1.4.0.tgz#d704581567f4526265d00ed8ecb354a02c3fec28" - dependencies: - core-js "^2.0.0" - traverse "^0.6.6" - type-name "^2.0.1" - strip-ansi@^3.0.0, strip-ansi@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= dependencies: ansi-regex "^2.0.0" strip-ansi@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= dependencies: ansi-regex "^3.0.0" -strip-ansi@^5.1.0, strip-ansi@^5.2.0: +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== dependencies: ansi-regex "^4.1.0" -strip-bom@3.0.0, strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" +strip-ansi@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.0.tgz#0b1571dd7669ccd4f3e06e14ef1eed26225ae532" + integrity sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w== + dependencies: + ansi-regex "^5.0.0" strip-bom@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e" + integrity sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4= dependencies: is-utf8 "^0.2.0" +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-dirs@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5" + integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g== + dependencies: + is-natural-number "^4.0.1" + strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" + integrity sha1-u0P/VZim6wXYm1n80SnJgzE2Br8= strip-indent@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" + integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= dependencies: get-stdin "^4.0.1" @@ -8272,27 +9804,39 @@ strip-indent@^2.0.0: resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + strip-json-comments@^2.0.0, strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= strip@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip/-/strip-3.0.0.tgz#750fc933152a7d35af0b7420e651789b914cc35e" + integrity sha1-dQ/JMxUqfTWvC3Qg5lF4m5FMw14= stubs@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/stubs/-/stubs-3.0.0.tgz#e8d2ba1fa9c90570303c030b6900f7d5f89abe5b" + integrity sha1-6NK6H6nJBXAwPAMLaQD31fiavls= subarg@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/subarg/-/subarg-1.0.0.tgz#f62cf17581e996b48fc965699f54c06ae268b8d2" + integrity sha1-9izxdYHplrSPyWVpn1TAauJouNI= dependencies: minimist "^1.1.0" -subscriptions-transport-ws@^0.9.11: - version "0.9.15" - resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.15.tgz#68a8b7ba0037d8c489fb2f5a102d1494db297d0d" +subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16: + version "0.9.16" + resolved "https://registry.yarnpkg.com/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.16.tgz#90a422f0771d9c32069294c08608af2d47f596ec" + integrity sha512-pQdoU7nC+EpStXnCfh/+ho0zE0Z+ma+i7xvj7bkXKb1dvYHSZxgRPaU6spRP+Bjzow67c/rRDoix5RT0uU9omw== dependencies: backo2 "^1.0.2" eventemitter3 "^3.1.0" @@ -8300,66 +9844,94 @@ subscriptions-transport-ws@^0.9.11: symbol-observable "^1.0.4" ws "^5.2.0" -supports-color@6.1.0: +superagent@6.1.0: version "6.1.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" - integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== + resolved "https://registry.yarnpkg.com/superagent/-/superagent-6.1.0.tgz#09f08807bc41108ef164cfb4be293cebd480f4a6" + integrity sha512-OUDHEssirmplo3F+1HWKUrUjvnQuA+nZI6i/JJBdXb5eq9IyEQwPyPpqND+SSsxf6TygpBEkUjISVRN4/VOpeg== dependencies: - has-flag "^3.0.0" + component-emitter "^1.3.0" + cookiejar "^2.1.2" + debug "^4.1.1" + fast-safe-stringify "^2.0.7" + form-data "^3.0.0" + formidable "^1.2.2" + methods "^1.1.2" + mime "^2.4.6" + qs "^6.9.4" + readable-stream "^3.6.0" + semver "^7.3.2" + +supertest@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/supertest/-/supertest-5.0.0.tgz#771aedfeb0a95466cc5d100d5d11288736fd25da" + integrity sha512-2JAWpPrUOZF4hHH5ZTCN2xjKXvJS3AEwPNXl0HUseHsfcXFvMy9kcsufIHCNAmQ5hlGCvgeAqaR5PBEouN3hlQ== + dependencies: + methods "1.1.2" + superagent "6.1.0" + +supports-color@7.1.0, supports-color@^7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" + integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== + dependencies: + has-flag "^4.0.0" supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + integrity sha1-U10EXOa2Nj+kARcIRimZXp3zJMc= -supports-color@^3.1.2: - version "3.2.3" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6" - dependencies: - has-flag "^1.0.0" - -supports-color@^5.0.0, supports-color@^5.3.0, supports-color@^5.5.0: +supports-color@^5.3.0, supports-color@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== dependencies: has-flag "^3.0.0" -supports-hyperlinks@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-1.0.1.tgz#71daedf36cc1060ac5100c351bb3da48c29c0ef7" +supports-color@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3" + integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ== dependencies: - has-flag "^2.0.0" - supports-color "^5.0.0" + has-flag "^3.0.0" symbol-observable@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" + integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= symbol-observable@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" + integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== -symbol-tree@^3.2.1, symbol-tree@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" +symbol-tree@^3.2.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" + integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== -tar@^4: - version "4.4.8" - resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d" +tar-stream@^1.5.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555" + integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A== dependencies: - chownr "^1.1.1" - fs-minipass "^1.2.5" - minipass "^2.3.4" - minizlib "^1.1.1" - mkdirp "^0.5.0" - safe-buffer "^5.1.2" - yallist "^3.0.2" + bl "^1.0.0" + buffer-alloc "^1.2.0" + end-of-stream "^1.0.0" + fs-constants "^1.0.0" + readable-stream "^2.3.0" + to-buffer "^1.1.1" + xtend "^4.0.0" -teeny-request@^3.11.3: - version "3.11.3" - resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-3.11.3.tgz#335c629f7645e5d6599362df2f3230c4cbc23a55" +teeny-request@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/teeny-request/-/teeny-request-6.0.2.tgz#da0a6ba6fce4a9dab772ef84b04e417157c56fe7" + integrity sha512-B6fxA0fSnY/bul06NggdN1nywtr5U5Uvt96pHfTi8pi4MNe6++VUWcAAFBrcMeha94s+gULwA5WvagoSZ+AcYg== dependencies: - https-proxy-agent "^2.2.1" + http-proxy-agent "^4.0.0" + https-proxy-agent "^5.0.0" node-fetch "^2.2.0" + stream-events "^1.0.5" uuid "^3.3.2" temp-dir@^1.0.0: @@ -8367,24 +9939,26 @@ temp-dir@^1.0.0: resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -tempfile@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tempfile/-/tempfile-2.0.0.tgz#6b0446856a9b1114d1856ffcbe509cccb0977265" - integrity sha1-awRGhWqbERTRhW/8vlCczLCXcmU= +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== dependencies: + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" temp-dir "^1.0.0" - uuid "^3.0.1" + uuid "^3.3.2" -term-size@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69" - integrity sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk= - dependencies: - execa "^0.7.0" +term-size@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/term-size/-/term-size-2.2.0.tgz#1f16adedfe9bdc18800e1776821734086fcc6753" + integrity sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw== test-exclude@^4.2.1: version "4.2.3" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.3.tgz#a9a5e64474e4398339245a0a769ad7c2f4a97c20" + integrity sha512-SYbXgY64PT+4GAL2ocI3HwPa4Q4TBKm0cwAVeKOt/Aoc0gSpNRjJX8w0pA1LMKZ3LBmd8pYBqApFNQLII9kavA== dependencies: arrify "^1.0.1" micromatch "^2.3.11" @@ -8392,62 +9966,47 @@ test-exclude@^4.2.1: read-pkg-up "^1.0.1" require-main-filename "^1.0.1" -text-encoding@^0.6.4: - version "0.6.4" - resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" - -text-extensions@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-2.0.0.tgz#43eabd1b495482fae4a2bf65e5f56c29f69220f6" - integrity sha512-F91ZqLgvi1E0PdvmxMgp+gcf6q8fMH7mhdwWfzXnl1k+GbpQDmi8l7DzLC5JTASKbwpY3TfxajAUzAXcv2NmsQ== - -then-fs@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/then-fs/-/then-fs-2.0.0.tgz#72f792dd9d31705a91ae19ebfcf8b3f968c81da2" - integrity sha1-cveS3Z0xcFqRrhnr/Piz+WjIHaI= +test-exclude@^5.2.3: + version "5.2.3" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-5.2.3.tgz#c3d3e1e311eb7ee405e092dac10aefd09091eac0" + integrity sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g== dependencies: - promise ">=3.2 <8" + glob "^7.1.3" + minimatch "^3.0.4" + read-pkg-up "^4.0.0" + require-main-filename "^2.0.0" + +text-extensions@^1.0.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/text-extensions/-/text-extensions-1.9.0.tgz#1853e45fee39c945ce6f6c36b2d659b5aabc2a26" + integrity sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ== throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a" + integrity sha1-iQN8vJLFarGJJua6TLsgDhVnKmo= -through2@^2.0.0, through2@^2.0.2, through2@^2.0.3: +through2@^2.0.0, through2@^2.0.2: version "2.0.5" resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== dependencies: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^3.0.0: +through2@^3.0.0, through2@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" + integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== dependencies: readable-stream "2 || 3" -through@2, "through@>=2.2.7 <3", through@^2.3.6: +through@2, "through@>=2.2.7 <3", through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= -thunkify@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/thunkify/-/thunkify-2.1.2.tgz#faa0e9d230c51acc95ca13a361ac05ca7e04553d" - integrity sha1-+qDp0jDFGsyVyhOjYawFyn4EVT0= - -timed-out@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" - integrity sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8= - -tmp@0.0.31: - version "0.0.31" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" - integrity sha1-jzirlDjhcxXl29izZX6L+yd65Kc= - dependencies: - os-tmpdir "~1.0.1" - -tmp@0.0.33, tmp@^0.0.33: +tmp@^0.0.33: version "0.0.33" resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== @@ -8464,14 +10023,32 @@ tmp@^0.1.0: tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" + integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE= + +to-array@0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/to-array/-/to-array-0.1.4.tgz#17e6c11f73dd4f3d74cda7a4ff3238e9ad9bf890" + integrity sha1-F+bBH3PdTz10zaek/zI46a2b+JA= + +to-buffer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80" + integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg== to-fast-properties@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc= + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= to-object-path@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/to-object-path/-/to-object-path-0.3.0.tgz#297588b7b0e7e0ac08e04e672f85c1f4999e17af" + integrity sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68= dependencies: kind-of "^3.0.2" @@ -8483,13 +10060,22 @@ to-readable-stream@^1.0.0: to-regex-range@^2.1.0: version "2.1.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-2.1.1.tgz#7c80c17b9dfebe599e27367e0d4dd5590141db38" + integrity sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg= dependencies: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" + integrity sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw== dependencies: define-property "^2.0.2" extend-shallow "^3.0.2" @@ -8499,54 +10085,48 @@ to-regex@^3.0.1, to-regex@^3.0.2: toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" + integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -toml@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/toml/-/toml-3.0.0.tgz#342160f1af1904ec9d204d03a5d61222d762c5ee" - integrity sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w== - -tough-cookie@>=2.3.3, tough-cookie@^2.3.2, tough-cookie@^2.3.4: +tough-cookie@^2.3.3, tough-cookie@^2.3.4, tough-cookie@~2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== dependencies: psl "^1.1.28" punycode "^2.1.1" -tough-cookie@~2.4.3: - version "2.4.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781" - dependencies: - psl "^1.1.24" - punycode "^1.4.1" - tr46@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk= dependencies: punycode "^2.1.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - -traverse@^0.6.6: - version "0.6.6" - resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137" +"traverse@>=0.3.0 <0.4": + version "0.3.9" + resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.3.9.tgz#717b8f220cc0bb7b44e40514c22b2e8bbc70d8b9" + integrity sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk= tree-kill@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.1.tgz#5398f374e2f292b9dcc7b2e71e30a5c3bb6c743a" - integrity sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q== + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== trim-newlines@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" + integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= trim-newlines@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= +trim-newlines@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" + integrity sha512-C4+gOpvmxaSMKuEf9Qc134F1ZuOHVXKRbtEflf4NTtuuJDEIJ9p5PXsalL8SkeRw+qit1Mo+yuvMPAKwWg/1hA== + trim-off-newlines@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz#9f9ba9d9efa8764c387698bcbfeb2c848f11adb3" @@ -8555,10 +10135,19 @@ trim-off-newlines@^1.0.0: trim-right@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + integrity sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM= + +ts-invariant@^0.4.0: + version "0.4.4" + resolved "https://registry.yarnpkg.com/ts-invariant/-/ts-invariant-0.4.4.tgz#97a523518688f93aafad01b0e80eb803eb2abd86" + integrity sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA== + dependencies: + tslib "^1.9.3" ts-jest@^22.0.0: version "22.4.6" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-22.4.6.tgz#a5d7f5e8b809626d1f4143209d301287472ec344" + integrity sha512-kYQ6g1G1AU+bOO9rv+SSQXg4WTcni6Wx3AM48iHni0nP1vIuhdNRjKTE9Cxx36Ix/IOV7L85iKu07dgXJzH2pQ== dependencies: babel-core "^6.26.3" babel-plugin-istanbul "^4.1.6" @@ -8573,29 +10162,33 @@ ts-jest@^22.0.0: yargs "^11.0.0" ts-node-dev@^1.0.0-pre.32: - version "1.0.0-pre.32" - resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.32.tgz#aa3bb9056c002713cfc393b2c324459020db41dc" + version "1.0.0-pre.44" + resolved "https://registry.yarnpkg.com/ts-node-dev/-/ts-node-dev-1.0.0-pre.44.tgz#2f4d666088481fb9c4e4f5bc8f15995bd8b06ecb" + integrity sha512-M5ZwvB6FU3jtc70i5lFth86/6Qj5XR5nMMBwVxZF4cZhpO7XcbWw6tbNiJo22Zx0KfjEj9py5DANhwLOkPPufw== dependencies: dateformat "~1.0.4-1.2.3" dynamic-dedupe "^0.3.0" filewatcher "~3.0.0" minimist "^1.1.3" mkdirp "^0.5.1" - node-notifier "^4.0.2" + node-notifier "^5.4.0" resolve "^1.0.0" rimraf "^2.6.1" + source-map-support "^0.5.12" + tree-kill "^1.2.1" ts-node "*" tsconfig "^7.0.0" ts-node@*: - version "8.0.2" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.0.2.tgz#9ecdf8d782a0ca4c80d1d641cbb236af4ac1b756" + version "8.6.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.6.2.tgz#7419a01391a818fbafa6f826a33c1a13e9464e35" + integrity sha512-4mZEbofxGqLL2RImpe3zMJukvEvcO1XP8bj8ozBPySdCUXEcU5cIRwR0aM3R+VoZq7iXc8N86NC0FspGRqP4gg== dependencies: arg "^4.1.0" - diff "^3.1.0" + diff "^4.0.1" make-error "^1.1.1" source-map-support "^0.5.6" - yn "^3.0.0" + yn "3.1.1" ts-node@8.0.3: version "8.0.3" @@ -8611,226 +10204,281 @@ ts-node@8.0.3: tsconfig@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-7.0.0.tgz#84538875a4dc216e5c4a5432b3a4dec3d54e91b7" + integrity sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw== dependencies: "@types/strip-bom" "^3.0.0" "@types/strip-json-comments" "0.0.30" strip-bom "^3.0.0" strip-json-comments "^2.0.0" +tslib@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@1.9.0: version "1.9.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.0.tgz#e37a86fda8cbbaf23a057f473c9f4dc64e5fc2e8" + integrity sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ== -tslib@1.9.3, tslib@^1.8.0, tslib@^1.8.1: - version "1.9.3" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" - -tslib@^1, tslib@^1.9.0, tslib@^1.9.3: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== +tslib@^1.10.0, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.11.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" + integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== tslint-config-prettier@^1.1.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz#946ed6117f98f3659a65848279156d87628c33dc" + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== tslint-config-standard@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/tslint-config-standard/-/tslint-config-standard-7.1.0.tgz#6bcc435a179478e365f6cc62312a561221985760" + integrity sha512-cETzxZcEQ1RKjwtEScGryAtqwiRFc55xBxhZP6bePyOfXmo6i1/QKQrTgFKBiM4FjCvcqTjJq20/KGrh+TzTfQ== dependencies: tslint-eslint-rules "^5.3.1" tslint-eslint-rules@^5.3.1: version "5.4.0" resolved "https://registry.yarnpkg.com/tslint-eslint-rules/-/tslint-eslint-rules-5.4.0.tgz#e488cc9181bf193fe5cd7bfca213a7695f1737b5" + integrity sha512-WlSXE+J2vY/VPgIcqQuijMQiel+UtmXS+4nvK4ZzlDiqBfXse8FAvkNnTcYhnQyOTW5KFM+uRRGXxYhFpuBc6w== dependencies: doctrine "0.7.2" tslib "1.9.0" tsutils "^3.0.0" tslint@^5.8.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.12.0.tgz#47f2dba291ed3d580752d109866fb640768fca36" + version "5.20.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d" + integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg== dependencies: - babel-code-frame "^6.22.0" + "@babel/code-frame" "^7.0.0" builtin-modules "^1.1.1" chalk "^2.3.0" commander "^2.12.1" - diff "^3.2.0" + diff "^4.0.1" glob "^7.1.1" - js-yaml "^3.7.0" + js-yaml "^3.13.1" minimatch "^3.0.4" + mkdirp "^0.5.1" resolve "^1.3.2" semver "^5.3.0" tslib "^1.8.0" - tsutils "^2.27.2" + tsutils "^2.29.0" -tsutils@^2.27.2: +tsutils@^2.29.0: version "2.29.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.29.0.tgz#32b488501467acbedd4b85498673a0812aca0b99" + integrity sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA== dependencies: tslib "^1.8.1" tsutils@^3.0.0: - version "3.5.2" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.5.2.tgz#6fd3c2d5a731e83bb21b070a173ec0faf3a8f6d3" + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== dependencies: tslib "^1.8.1" tunnel-agent@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= dependencies: safe-buffer "^5.0.1" tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" - -twit@^2.2.9: - version "2.2.11" - resolved "https://registry.yarnpkg.com/twit/-/twit-2.2.11.tgz#554343d1cf343ddf503280db821f61be5ab407c3" - dependencies: - bluebird "^3.1.5" - mime "^1.3.4" - request "^2.68.0" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= type-check@~0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= dependencies: prelude-ls "~1.1.2" type-detect@4.0.8: version "4.0.8" resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" - integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== +type-fest@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" + integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== + +type-fest@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" + integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== + +type-fest@^0.3.0: + version "0.3.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" + integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== -type-is@^1.6.16, type-is@~1.6.16: - version "1.6.16" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" +type-fest@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" + integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== + +type-fest@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" + integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== + +type-is@^1.6.16, type-is@~1.6.17, type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== dependencies: media-typer "0.3.0" - mime-types "~2.1.18" + mime-types "~2.1.24" -type-name@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/type-name/-/type-name-2.0.2.tgz#efe7d4123d8ac52afff7f40c7e4dec5266008fb4" +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^2.9.2: - version "2.9.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c" +typescript@^3.6.4: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== uglify-js@^3.1.4: - version "3.4.9" - resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + version "3.8.0" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.8.0.tgz#f3541ae97b2f048d7e7e3aa4f39fd8a1f5d7a805" + integrity sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ== dependencies: - commander "~2.17.1" + commander "~2.20.3" source-map "~0.6.1" +unbzip2-stream@^1.0.9: + version "1.3.3" + resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a" + integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg== + dependencies: + buffer "^5.2.1" + through "^2.3.8" + underscore@^1.8.3: - version "1.9.1" - resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.1.tgz#06dce34a0e68a7babc29b365b8e74b8925203961" + version "1.9.2" + resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.9.2.tgz#0c8d6f536d6f378a5af264a72f7bec50feb7cf2f" + integrity sha512-D39qtimx0c1fI3ya1Lnhk3E9nONswSKhnffBI0gME9C99fYOkNi04xs8K6pePLhvl1frbDemkaBQ5ikWllR2HQ== "unicode@>= 0.3.1": - version "11.0.1" - resolved "https://registry.yarnpkg.com/unicode/-/unicode-11.0.1.tgz#735bd422ec75cf28d396eb224d535d168d5f1db6" + version "12.1.0" + resolved "https://registry.yarnpkg.com/unicode/-/unicode-12.1.0.tgz#7ee53a7a0ca5539b353419432823d8da58bbbf33" + integrity sha512-Ty6+Ew21DiYTWLYtd05RF/X4c1ekOvOgANyHbBj0h3MaXpfaGr2Rdmc0hMFuGQLyPLb9cU4ArNxl0bTF5HSzXw== union-value@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.0.tgz#5c71c34cb5bad5dcebe3ea0cd08207ba5aa1aea4" + version "1.0.1" + resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847" + integrity sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg== dependencies: arr-union "^3.1.0" get-value "^2.0.6" is-extendable "^0.1.1" - set-value "^0.4.3" + set-value "^2.0.1" -unique-string@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a" +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== dependencies: - crypto-random-string "^1.0.0" + crypto-random-string "^2.0.0" -universal-deep-strict-equal@^1.2.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/universal-deep-strict-equal/-/universal-deep-strict-equal-1.2.2.tgz#0da4ac2f73cff7924c81fa4de018ca562ca2b0a7" +universal-user-agent@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" + integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== dependencies: - array-filter "^1.0.0" - indexof "0.0.1" - object-keys "^1.0.0" + os-name "^3.1.0" -universal-user-agent@^2.0.0, universal-user-agent@^2.0.1: - version "2.1.0" - resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.1.0.tgz#5abfbcc036a1ba490cb941f8fd68c46d3669e8e4" - integrity sha512-8itiX7G05Tu3mGDTdNY2fB4KJ8MgZLS54RdG6PkkfwMAavrXu1mV/lls/GABx9O3Rw4PnTtasxrvbMQoBYY92Q== +universal-user-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" + integrity sha512-B5TPtzZleXyPrUMKCpEHFmVhMN6EhmJYjG5PQna9s7mXeSqGTLap4OpqLl5FCEFUI3UBmllkETwKf/db66Y54Q== dependencies: - os-name "^3.0.0" + os-name "^3.1.0" universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" + integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= unset-value@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559" + integrity sha1-g3aHP30jNRef+x5vw6jtDfyKtVk= dependencies: has-value "^0.3.1" isobject "^3.0.0" -unzip-response@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" - integrity sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c= +unzipper@^0.9.11: + version "0.9.15" + resolved "https://registry.yarnpkg.com/unzipper/-/unzipper-0.9.15.tgz#97d99203dad17698ee39882483c14e4845c7549c" + integrity sha512-2aaUvO4RAeHDvOCuEtth7jrHFaCKTSXPqUkXwADaLBzGbgZGzUDccoEdJ5lW+3RmfpOZYNx0Rw6F6PUzM6caIA== + dependencies: + big-integer "^1.6.17" + binary "~0.3.0" + bluebird "~3.4.1" + buffer-indexof-polyfill "~1.0.0" + duplexer2 "~0.1.4" + fstream "^1.0.12" + listenercount "~1.0.1" + readable-stream "~2.3.6" + setimmediate "~1.0.4" -update-notifier@2.5.0, update-notifier@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-2.5.0.tgz#d0744593e13f161e406acb1d9408b72cad08aff6" - integrity sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw== +update-notifier@4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/update-notifier/-/update-notifier-4.1.0.tgz#4866b98c3bc5b5473c020b1250583628f9a328f3" + integrity sha512-w3doE1qtI0/ZmgeoDoARmI5fjDoT93IfKgEGqm26dGUOh8oNpaSTsGNdYRN/SjOuo10jcJGwkEL3mroKzktkew== dependencies: - boxen "^1.2.1" - chalk "^2.0.1" - configstore "^3.0.0" + boxen "^4.2.0" + chalk "^3.0.0" + configstore "^5.0.1" + has-yarn "^2.1.0" import-lazy "^2.1.0" - is-ci "^1.0.10" - is-installed-globally "^0.1.0" - is-npm "^1.0.0" - latest-version "^3.0.0" - semver-diff "^2.0.0" - xdg-basedir "^3.0.0" + is-ci "^2.0.0" + is-installed-globally "^0.3.1" + is-npm "^4.0.0" + is-yarn-global "^0.3.0" + latest-version "^5.0.0" + pupa "^2.0.1" + semver-diff "^3.1.1" + xdg-basedir "^4.0.0" uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== dependencies: punycode "^2.1.0" urix@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/urix/-/urix-0.1.0.tgz#da937f7a62e21fec1fd18d49b35c2935067a6c72" + integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= -url-join@4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.0.tgz#4d3340e807d3773bda9991f8305acdcc2a665d2a" - integrity sha1-TTNA6AfTdzvamZH4MFrNzCpmXSo= - -url-parse-lax@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" - integrity sha1-evjzA2Rem9eaJy56FKxovAYJ2nM= - dependencies: - prepend-http "^1.0.1" +url-join@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/url-join/-/url-join-4.0.1.tgz#b642e21a2646808ffa178c4c5fda39844e12cde7" + integrity sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA== url-parse-lax@^3.0.0: version "3.0.0" @@ -8839,13 +10487,18 @@ url-parse-lax@^3.0.0: dependencies: prepend-http "^2.0.0" -url-template@^2.0.8: - version "2.0.8" - resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" +url-parse@~1.4.3: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" url@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/url/-/url-0.10.3.tgz#021e4d9c7705f21bbf37d03ceb58767402774c64" + integrity sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ= dependencies: punycode "1.3.2" querystring "0.2.0" @@ -8853,40 +10506,60 @@ url@0.10.3: use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" + integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ== util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= util.promisify@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.0.tgz#440f7165a459c9a16dc145eb8e72f35687097030" - dependencies: - define-properties "^1.1.2" - object.getownpropertydescriptors "^2.0.3" - -util@^0.10.3: - version "0.10.4" - resolved "https://registry.yarnpkg.com/util/-/util-0.10.4.tgz#3aa0125bfe668a4672de58857d3ace27ecb76901" - integrity sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A== + version "1.0.1" + resolved "https://registry.yarnpkg.com/util.promisify/-/util.promisify-1.0.1.tgz#6baf7774b80eeb0f7520d8b81d07982a59abbaee" + integrity sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA== dependencies: - inherits "2.0.3" + define-properties "^1.1.3" + es-abstract "^1.17.2" + has-symbols "^1.0.1" + object.getownpropertydescriptors "^2.1.0" utils-merge@1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" +uuid-by-string@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/uuid-by-string/-/uuid-by-string-3.0.2.tgz#40e7a2049224715f566a13169f1f178aee42ac00" + integrity sha512-XA0BuWgMte7RcwEdM+AJOtQWVogkpQkWGX6bEgnYFWSkp4yebZTeapNvGZWZUX+T1/lo6kL+8nY7AczLI+Bjfw== + dependencies: + js-md5 "^0.7.3" + js-sha1 "^0.6.0" -uuid@3.3.2, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1, uuid@^3.3.2: +uuid@3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" + integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== + +uuid@3.4.0, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.3.3: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +uuid@7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.1.tgz#95ed6ff3d8c881cbf85f0f05cc3915ef994818ef" + integrity sha512-yqjRXZzSJm9Dbl84H2VDHpM3zMjzSJQ+hn6C4zqd5ilW+7P4ZmLEEqwho9LjP+tGuZlF4xrHQXT0h9QZUS/pWA== + +uuid@^8.3.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" + integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== dependencies: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" @@ -8894,47 +10567,46 @@ validate-npm-package-license@^3.0.1: validator@^9.0.0: version "9.4.1" resolved "https://registry.yarnpkg.com/validator/-/validator-9.4.1.tgz#abf466d398b561cd243050112c6ff1de6cc12663" + integrity sha512-YV5KjzvRmSyJ1ee/Dm5UED0G+1L4GZnLN3w6/T+zZm8scVua4sOhYKWTUrKa0H/tMiJyO9QLHMPN+9mB/aMunA== vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= dependencies: assert-plus "^1.0.0" core-util-is "1.0.2" extsprintf "^1.2.0" -vscode-languageserver-types@^3.5.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" - integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== +vm2@^3.9.2: + version "3.9.2" + resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.2.tgz#a4085d2d88a808a1b3c06d5478c2db3222a9cc30" + integrity sha512-nzyFmHdy2FMg7mYraRytc2jr4QBaUY3TEGe3q3bK8EgS9WC98wxn2jrPxS/ruWm+JGzrEIIeufKweQzVoQEd+Q== w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045" + integrity sha1-gqwr/2PZUOqeMYmlimViX+3xkEU= dependencies: browser-process-hrtime "^0.1.2" -walkdir@^0.3.0, walkdir@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.3.2.tgz#ac8437a288c295656848ebc19981ebc677a5f590" +walkdir@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/walkdir/-/walkdir-0.4.1.tgz#dc119f83f4421df52e3061e514228a2db20afa39" + integrity sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ== -walker@~1.0.5: +walker@^1.0.7, walker@~1.0.5: version "1.0.7" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.7.tgz#2f7f9b8fd10d677262b18a884e28d19618e028fb" + integrity sha1-L3+bj9ENZ3JisYqITijRlhjgKPs= dependencies: makeerror "1.0.x" -watch@~0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/watch/-/watch-0.18.0.tgz#28095476c6df7c90c963138990c0a5423eb4b986" - dependencies: - exec-sh "^0.2.0" - minimist "^1.2.0" - wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" @@ -8942,80 +10614,101 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - -webidl-conversions@^4.0.0, webidl-conversions@^4.0.2: +webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== websocket-driver@>=0.5.1: - version "0.7.0" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb" + version "0.7.3" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" + integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== dependencies: - http-parser-js ">=0.4.0" + http-parser-js ">=0.4.0 <0.4.11" + safe-buffer ">=5.1.0" websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" + integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3: version "1.0.5" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz#5abacf777c32166a51d085d6b4f3e7d27113ddb0" + integrity sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw== dependencies: iconv-lite "0.4.24" whatwg-mimetype@^2.1.0, whatwg-mimetype@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz#3d4b1e0312d2079879f826aff18dbeeca5960fbf" - -whatwg-url@^4.3.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" + integrity sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g== whatwg-url@^6.4.1: version "6.5.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-6.5.0.tgz#f2df02bff176fd65070df74ad5ccbb5a199965a8" + integrity sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" webidl-conversions "^4.0.2" whatwg-url@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.0.0.tgz#fde926fa54a599f3adf82dff25a9f7be02dc6edd" + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== dependencies: lodash.sortby "^4.7.0" tr46 "^1.0.1" webidl-conversions "^4.0.2" +which-boxed-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.1.tgz#cbe8f838ebe91ba2471bb69e9edbda67ab5a5ec1" + integrity sha512-7BT4TwISdDGBgaemWU0N0OU7FeAEJ9Oo2P1PHRm/FCWoEi2VLWC9b6xvxAA3C/NMpxg3HXVgi0sMmGbNUbNepQ== + dependencies: + is-bigint "^1.0.0" + is-boolean-object "^1.0.0" + is-number-object "^1.0.3" + is-string "^1.0.4" + is-symbol "^1.0.2" + +which-collection@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.1.tgz#70eab71ebbbd2aefaf32f917082fc62cdcb70906" + integrity sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A== + dependencies: + is-map "^2.0.1" + is-set "^2.0.1" + is-weakmap "^2.0.1" + is-weakset "^2.0.1" + which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= -which@^1.0.5, which@^1.2.10, which@^1.2.12, which@^1.2.9, which@^1.3.0: +which@^1.2.10, which@^1.2.9, which@^1.3.0: version "1.3.1" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: - string-width "^1.0.2 || 2" + isexe "^2.0.0" -widest-line@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-2.0.1.tgz#7438764730ec7ef4381ce4df82fb98a53142a3fc" - integrity sha512-Ba5m9/Fa4Xt9eb2ELXt77JxVDV8w7qQrH0zS/TWSJdLyAwQjWoOzpzj5lwVftDz6n/EOu3tNACS84v509qwnJA== +widest-line@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/widest-line/-/widest-line-3.1.0.tgz#8292333bbf66cb45ff0de1603b136b7ae1496eca" + integrity sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg== dependencies: - string-width "^2.1.1" + string-width "^4.0.0" window-size@1.1.1: version "1.1.1" @@ -9025,10 +10718,6 @@ window-size@1.1.1: define-property "^1.0.0" is-number "^3.0.0" -window-size@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" - windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -9036,83 +10725,127 @@ windows-release@^3.1.0: dependencies: execa "^1.0.0" -wordwrap@~0.0.2: - version "0.0.3" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" +word-wrap@~1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wordwrap@~1.0.0: +wordwrap@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -worker-farm@^1.3.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.6.0.tgz#aecc405976fab5a95526180846f0dba288f3a4a0" - dependencies: - errno "~0.1.7" +wordwrap@~0.0.2: + version "0.0.3" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" + integrity sha1-o9XabNXAvAAI03I0u68b7WMFkQc= wrap-ansi@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" + integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= dependencies: string-width "^1.0.1" strip-ansi "^3.0.1" +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.2.tgz#a7181706dfba17855d221140a9c06e15fcdd87b9" +write-file-atomic@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.4.1.tgz#d0b05463c188ae804396fd5ab2a370062af87529" + integrity sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg== dependencies: graceful-fs "^4.1.11" imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-2.3.0.tgz#1ff61575c2e2a4e8e510d6fa4e243cce183999ab" +write-file-atomic@^3.0.0: + version "3.0.3" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" + integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== dependencies: - graceful-fs "^4.1.11" imurmurhash "^0.1.4" + is-typedarray "^1.0.0" signal-exit "^3.0.2" + typedarray-to-buffer "^3.1.5" ws@^5.2.0: version "5.2.2" resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.2.tgz#dffef14866b8e8dc9133582514d1befaf96e980f" + integrity sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA== dependencies: async-limiter "~1.0.0" ws@^6.0.0: - version "6.1.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8" + version "6.2.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb" + integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA== dependencies: async-limiter "~1.0.0" -xdg-basedir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-3.0.0.tgz#496b2cc109eca8dbacfe2dc72b603c17c5870ad4" +ws@^7.1.2: + version "7.3.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" + integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== -xlsx-populate@^1.14.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/xlsx-populate/-/xlsx-populate-1.17.0.tgz#af48ab54f83badd81d6114f3d4f7b0af6a0d839d" +ws@~6.1.0: + version "6.1.4" + resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.4.tgz#5b5c8800afab925e94ccb29d153c8d02c1776ef9" + integrity sha512-eqZfL+NE/YQc1/ZynhojeV8q+H050oR8AZ2uIev7RU10svA9ZnJUddHcOUZTJLinZ9yEfdA2kSATS2qZK5fhJA== dependencies: - cfb "^1.0.7" - jszip "^3.1.5" - lodash "^4.17.10" + async-limiter "~1.0.0" + +x-xss-protection@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/x-xss-protection/-/x-xss-protection-1.3.0.tgz#3e3a8dd638da80421b0e9fff11a2dbe168f6d52c" + integrity sha512-kpyBI9TlVipZO4diReZMAHWtS0MMa/7Kgx8hwG/EuZLiA6sg4Ah/4TRdASHhRRN3boobzcYgFRUFSgHRge6Qhg== + +xdg-basedir@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-4.0.0.tgz#4bc8d9984403696225ef83a1573cbbcb4e79db13" + integrity sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q== + +xlsx-populate@^1.20.1: + version "1.21.0" + resolved "https://registry.yarnpkg.com/xlsx-populate/-/xlsx-populate-1.21.0.tgz#f6cd02401f4cd3d055e81f2b6983ddfeeb60ffe6" + integrity sha512-8v2Gm8BehXo6LU7KT802QoXTPkYY1SKk5V8g/UuYZnNB3JzXqud/P99Pxr2yXeKyt+sKlCatmidz6jQNie1hRw== + dependencies: + cfb "^1.1.3" + jszip "^3.2.2" + lodash "^4.17.15" sax "^1.2.4" -xml-name-validator@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" +xlsx-stream-reader@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/xlsx-stream-reader/-/xlsx-stream-reader-1.1.0.tgz#77305347eb1eec7199dac7d2ece4bb92f4c90f8e" + integrity sha512-SLX5YF9wn4WogWKSTSRjGitPIoK+ZtdkV1S60T0nwyKBdbIG7+PhREsend2VTL/ydWEur5LpTjwZklXmIPEVhw== + dependencies: + sax "^1.2.4" + ssf "^0.10.2" + tmp "^0.1.0" + unzipper "^0.9.11" xml-name-validator@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" + integrity sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw== -xml2js@0.4.19, xml2js@^0.4.17: +xml2js@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.19.tgz#686c20f213209e94abf0d1bcf1efaa291c7827a7" + integrity sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q== dependencies: sax ">=0.6.0" xmlbuilder "~9.0.1" @@ -9120,61 +10853,87 @@ xml2js@0.4.19, xml2js@^0.4.17: xmlbuilder@~9.0.1: version "9.0.7" resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-9.0.7.tgz#132ee63d2ec5565c557e20f4c22df9aca686b10d" + integrity sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0= -xmlhttprequest@1.8.0: - version "1.8.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz#67fe075c5c24fef39f9d65f5f7b7fe75171968fc" +xmlhttprequest-ssl@~1.5.4: + version "1.5.5" + resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz#c2876b06168aadc40e57d97e81191ac8f4398b3e" + integrity sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4= -xregexp@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943" - integrity sha1-UqY+VsoLhKfzpfPWGHLxJq16WUM= +xss@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.6.tgz#eaf11e9fc476e3ae289944a1009efddd8a124b51" + integrity sha512-6Q9TPBeNyoTRxgZFk5Ggaepk/4vUOYdOsIUYvLehcsIZTFjaavbVnsuAkLA5lIFuug5hw8zxcB9tm01gsjph2A== + dependencies: + commander "^2.9.0" + cssfilter "0.0.10" xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" + version "4.0.2" + resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" + integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.0, y18n@^3.2.1: +y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" + integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== yallist@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= -yallist@^3.0.0, yallist@^3.0.2: - version "3.0.3" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.3.tgz#b4b049e314be545e3ce802236d6cd22cd91c3de9" +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== -yargs-parser@13.0.0: - version "13.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b" - integrity sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw== +yargs-parser@17.0.0: + version "17.0.0" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-17.0.0.tgz#cb2586e6fd445b17ced264dd4a38c60bdb35b7ec" + integrity sha512-Fl4RBJThsWeJl3cRZeGuolcuH78/foVUAYIUpKn8rkCnjn23ilZvJyEZJjnlzoG/+EJKPb1RggD4xS/Jie2nxg== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-7.0.0.tgz#8d0ac42f16ea55debd332caf4c4038b3e3f5dfd9" +yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== dependencies: - camelcase "^4.1.0" + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-parser@^18.1.3: + version "18.1.3" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" + integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" yargs-parser@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-9.0.2.tgz#9ccf6a43460fe4ed40a9bb68f48d43b8a68cc077" + integrity sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc= dependencies: camelcase "^4.1.0" yargs@^11.0.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.0.tgz#90b869934ed6e871115ea2ff58b03f4724ed2d77" + version "11.1.1" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766" + integrity sha512-PRU7gJrJaXv3q3yQZ/+/X6KBswZiaQ+zOmdprZcouPYtQgvNU35i+68M4b1ZHLZtYFT5QObFLV+ZkmJYcwKdiw== dependencies: cliui "^4.0.0" decamelize "^1.1.1" find-up "^2.1.0" get-caller-file "^1.0.1" - os-locale "^2.0.0" + os-locale "^3.1.0" require-directory "^2.1.1" require-main-filename "^1.0.1" set-blocking "^2.0.0" @@ -9183,46 +10942,49 @@ yargs@^11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" -yargs@^3.10.0, yargs@^3.19.0: - version "3.32.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995" - dependencies: - camelcase "^2.0.1" - cliui "^3.0.3" - decamelize "^1.1.1" - os-locale "^1.4.0" - string-width "^1.0.1" - window-size "^0.1.4" - y18n "^3.2.0" - -yargs@^9.0.0: - version "9.0.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-9.0.1.tgz#52acc23feecac34042078ee78c0c007f5085db4c" +yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== dependencies: - camelcase "^4.1.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^2.0.0" - read-pkg-up "^2.0.0" + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" require-directory "^2.1.1" - require-main-filename "^1.0.1" + require-main-filename "^2.0.0" set-blocking "^2.0.0" - string-width "^2.0.0" + string-width "^3.0.0" which-module "^2.0.0" - y18n "^3.2.1" - yargs-parser "^7.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" -yn@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.0.0.tgz#0073c6b56e92aed652fbdfd62431f2d6b9a7a091" +yauzl@^2.4.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" + integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk= + dependencies: + buffer-crc32 "~0.2.3" + fd-slicer "~1.1.0" + +yeast@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" + integrity sha1-AI4G2AlDIMNy28L47XagymyKxBk= -zen-observable-ts@^0.8.13: - version "0.8.13" - resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.13.tgz#ae1fd77c84ef95510188b1f8bca579d7a5448fc2" +yn@3.1.1, yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + +zen-observable-ts@^0.8.20: + version "0.8.20" + resolved "https://registry.yarnpkg.com/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz#44091e335d3fcbc97f6497e63e7f57d5b516b163" + integrity sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA== dependencies: + tslib "^1.9.3" zen-observable "^0.8.0" zen-observable@^0.8.0: - version "0.8.11" - resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.11.tgz#d3415885eeeb42ee5abb9821c95bb518fcd6d199" + version "0.8.15" + resolved "https://registry.yarnpkg.com/zen-observable/-/zen-observable-0.8.15.tgz#96415c512d8e3ffd920afd3889604e30b9eaac15" + integrity sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==
- -
@@ -173,7 +170,7 @@ font-size: 12px; font-family: Helvetica Neue; font-weight: 300;" > - Copyright © 2019 + Copyright © 2020 erxes Inc. All rights reserved.