diff --git a/.github/actions/deploy-service/action.yaml b/.github/actions/deploy-service/action.yaml index 922d93eefd..f21c4e915c 100644 --- a/.github/actions/deploy-service/action.yaml +++ b/.github/actions/deploy-service/action.yaml @@ -14,6 +14,16 @@ inputs: description: To which cloud cluster to deploy required: true + prioritized: + description: Is the service listening on prioritized queues? + required: false + default: 'false' + + cloud_env: + description: Which cloud environment are we deploying to? + required: false + default: 'default' + runs: using: composite steps: @@ -25,10 +35,38 @@ runs: AWS_SECRET_ACCESS_KEY: ${{ env.AWS_SECRET_ACCESS_KEY }} AWS_REGION: ${{ env.AWS_REGION }} - - name: Deploy image + - name: Deploy image (non prioritized) + if: inputs.prioritized == 'false' shell: bash run: kubectl set image deployments/${{ inputs.service }}-dpl ${{ inputs.service }}=${{ inputs.image }} + - name: Deploy image (prioritized - production) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'prod' + shell: bash + run: | + kubectl set image deployments/${{ inputs.service }}-system-dpl ${{ inputs.service }}-system=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-high-dpl ${{ inputs.service }}-high=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-urgent-dpl ${{ inputs.service }}-urgent=${{ inputs.image }} + + - name: Deploy image (prioritized - lfx production) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'lfx_prod' + shell: bash + run: | + kubectl set image deployments/${{ inputs.service }}-system-dpl ${{ inputs.service }}-system=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + kubectl set image deployments/${{ inputs.service }}-high-dpl ${{ inputs.service }}-high=${{ inputs.image }} + + - name: Deploy image (prioritized - staging) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'staging' + shell: bash + run: kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + + - name: Deploy image (prioritized - lfx staging) + if: inputs.prioritized == 'true' && inputs.cloud_env == 'lfx_staging' + shell: bash + run: kubectl set image deployments/${{ inputs.service }}-normal-dpl ${{ inputs.service }}-normal=${{ inputs.image }} + - uses: ./.github/actions/slack-notify with: message: 'Service *${{ inputs.service }}* was just deployed using docker image `${{ inputs.image }}`' diff --git a/.github/workflows/CI-node.yaml b/.github/workflows/CI-node.yaml index b8d6c61b3d..9a42771aec 100644 --- a/.github/workflows/CI-node.yaml +++ b/.github/workflows/CI-node.yaml @@ -59,50 +59,3 @@ jobs: - name: Check app linting, format and typescript run: ./scripts/lint_apps.sh - tests-main: - needs: lint-format - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install dependencies - run: cd .. && corepack enable && pnpm i --frozen-lockfile - - - name: Run tests - working-directory: ./backend - run: SERVICE=test pnpm test -- --testPathIgnorePatterns=serverless - - tests-serverless: - needs: lint-format - runs-on: ubuntu-latest - defaults: - run: - shell: bash - working-directory: ./backend - - steps: - - name: Check out repository code - uses: actions/checkout@v2 - - - name: Setup Node - uses: actions/setup-node@v3 - with: - node-version: 16 - - - name: Install dependencies - run: cd .. && corepack enable && pnpm i --frozen-lockfile - - - name: Run tests - working-directory: ./backend - run: SERVICE=test pnpm test -- --testPathPattern="serverless\/" diff --git a/.github/workflows/lf-production-deploy-new.yaml b/.github/workflows/lf-production-deploy-new.yaml index 4ce5052b2c..649a90d6f5 100644 --- a/.github/workflows/lf-production-deploy-new.yaml +++ b/.github/workflows/lf-production-deploy-new.yaml @@ -8,9 +8,9 @@ on: required: true type: boolean deploy_search_sync_api: - description: Deploy search-sync-api service? - required: true - type: boolean + description: Deploy search-sync-api service? + required: true + type: boolean deploy_integration_sync_worker: description: Deploy integration-sync-worker service? required: true @@ -64,7 +64,7 @@ jobs: - name: Set docker image output id: image run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - + build-and-push-search-sync-api: runs-on: ubuntu-latest if: ${{ inputs.deploy_search_sync_api }} @@ -208,6 +208,8 @@ jobs: service: search-sync-worker image: ${{ needs.build-and-push-search-sync-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-search-sync-api: needs: build-and-push-search-sync-api @@ -244,6 +246,8 @@ jobs: service: integration-sync-worker image: ${{ needs.build-and-push-integration-sync-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-webhook-api: needs: build-and-push-webhook-api diff --git a/.github/workflows/lf-production-deploy-original.yaml b/.github/workflows/lf-production-deploy-original.yaml index 3e5fd30a4a..6e1b38125a 100644 --- a/.github/workflows/lf-production-deploy-original.yaml +++ b/.github/workflows/lf-production-deploy-original.yaml @@ -237,6 +237,8 @@ jobs: service: nodejs-worker image: ${{ needs.build-and-push-backend.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-discord-ws: needs: build-and-push-backend @@ -291,6 +293,8 @@ jobs: service: integration-run-worker image: ${{ needs.build-and-push-integration-run-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-integration-stream-worker: needs: build-and-push-integration-stream-worker @@ -309,6 +313,8 @@ jobs: service: integration-stream-worker image: ${{ needs.build-and-push-integration-stream-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-integration-data-worker: needs: build-and-push-integration-data-worker @@ -327,6 +333,8 @@ jobs: service: integration-data-worker image: ${{ needs.build-and-push-integration-data-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-data-sink-worker: needs: build-and-push-data-sink-worker @@ -345,6 +353,8 @@ jobs: service: data-sink-worker image: ${{ needs.build-and-push-data-sink-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_prod + prioritized: true deploy-frontend: needs: build-and-push-frontend diff --git a/.github/workflows/lf-staging-deploy-backend.yaml b/.github/workflows/lf-staging-deploy-backend.yaml index 7cee20ac98..d39f330fbc 100644 --- a/.github/workflows/lf-staging-deploy-backend.yaml +++ b/.github/workflows/lf-staging-deploy-backend.yaml @@ -76,6 +76,8 @@ jobs: service: nodejs-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true deploy-job-generator: needs: build-and-push diff --git a/.github/workflows/lf-staging-deploy-data-sink-worker.yaml b/.github/workflows/lf-staging-deploy-data-sink-worker.yaml index 3aa92424ba..6dabe492a4 100644 --- a/.github/workflows/lf-staging-deploy-data-sink-worker.yaml +++ b/.github/workflows/lf-staging-deploy-data-sink-worker.yaml @@ -58,3 +58,5 @@ jobs: service: data-sink-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true diff --git a/.github/workflows/lf-staging-deploy-integration-data-worker.yaml b/.github/workflows/lf-staging-deploy-integration-data-worker.yaml index a059404c0c..74325f88d6 100644 --- a/.github/workflows/lf-staging-deploy-integration-data-worker.yaml +++ b/.github/workflows/lf-staging-deploy-integration-data-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-data-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true diff --git a/.github/workflows/lf-staging-deploy-integration-run-worker.yaml b/.github/workflows/lf-staging-deploy-integration-run-worker.yaml index c26285a4e8..a18be6d287 100644 --- a/.github/workflows/lf-staging-deploy-integration-run-worker.yaml +++ b/.github/workflows/lf-staging-deploy-integration-run-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-run-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true diff --git a/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml b/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml index e69c76ced3..1bc1cbb0fc 100644 --- a/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml +++ b/.github/workflows/lf-staging-deploy-integration-stream-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-stream-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true diff --git a/.github/workflows/lf-staging-deploy-search-sync-worker.yaml b/.github/workflows/lf-staging-deploy-search-sync-worker.yaml index 022c152bd6..391f0a01ab 100644 --- a/.github/workflows/lf-staging-deploy-search-sync-worker.yaml +++ b/.github/workflows/lf-staging-deploy-search-sync-worker.yaml @@ -58,3 +58,5 @@ jobs: service: search-sync-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: lfx_staging + prioritized: true diff --git a/.github/workflows/production-deploy-new.yaml b/.github/workflows/production-deploy-new.yaml index 25baf153c2..9628bc5e1b 100644 --- a/.github/workflows/production-deploy-new.yaml +++ b/.github/workflows/production-deploy-new.yaml @@ -64,7 +64,7 @@ jobs: - name: Set docker image output id: image run: echo "IMAGE=${{ steps.image-builder.outputs.image }}" >> $GITHUB_OUTPUT - + build-and-push-search-sync-api: runs-on: ubuntu-latest if: ${{ inputs.deploy_search_sync_api }} @@ -208,6 +208,8 @@ jobs: service: search-sync-worker image: ${{ needs.build-and-push-search-sync-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-search-sync-api: needs: build-and-push-search-sync-api @@ -244,6 +246,8 @@ jobs: service: integration-sync-worker image: ${{ needs.build-and-push-integration-sync-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-webhook-api: needs: build-and-push-webhook-api diff --git a/.github/workflows/production-deploy-original.yaml b/.github/workflows/production-deploy-original.yaml index 195cf84c20..c681cc82ad 100644 --- a/.github/workflows/production-deploy-original.yaml +++ b/.github/workflows/production-deploy-original.yaml @@ -237,6 +237,8 @@ jobs: service: nodejs-worker image: ${{ needs.build-and-push-backend.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-discord-ws: needs: build-and-push-backend @@ -291,6 +293,8 @@ jobs: service: integration-run-worker image: ${{ needs.build-and-push-integration-run-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-integration-stream-worker: needs: build-and-push-integration-stream-worker @@ -309,6 +313,8 @@ jobs: service: integration-stream-worker image: ${{ needs.build-and-push-integration-stream-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-integration-data-worker: needs: build-and-push-integration-data-worker @@ -327,6 +333,8 @@ jobs: service: integration-data-worker image: ${{ needs.build-and-push-integration-data-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-data-sink-worker: needs: build-and-push-data-sink-worker @@ -345,6 +353,8 @@ jobs: service: data-sink-worker image: ${{ needs.build-and-push-data-sink-worker.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: prod + prioritized: true deploy-frontend: needs: build-and-push-frontend diff --git a/.github/workflows/staging-deploy-backend.yaml b/.github/workflows/staging-deploy-backend.yaml index b8c6f73d6d..a71dde5657 100644 --- a/.github/workflows/staging-deploy-backend.yaml +++ b/.github/workflows/staging-deploy-backend.yaml @@ -76,6 +76,8 @@ jobs: service: nodejs-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + prioritized: true + cloud_env: staging deploy-job-generator: needs: build-and-push diff --git a/.github/workflows/staging-deploy-data-sink-worker.yaml b/.github/workflows/staging-deploy-data-sink-worker.yaml index 3f9e32557e..716aa7d184 100644 --- a/.github/workflows/staging-deploy-data-sink-worker.yaml +++ b/.github/workflows/staging-deploy-data-sink-worker.yaml @@ -58,3 +58,5 @@ jobs: service: data-sink-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.github/workflows/staging-deploy-integration-data-worker.yaml b/.github/workflows/staging-deploy-integration-data-worker.yaml index 74b9383984..e5a5b5d586 100644 --- a/.github/workflows/staging-deploy-integration-data-worker.yaml +++ b/.github/workflows/staging-deploy-integration-data-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-data-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.github/workflows/staging-deploy-integration-run-worker.yaml b/.github/workflows/staging-deploy-integration-run-worker.yaml index 2f733da9bd..b066cf936f 100644 --- a/.github/workflows/staging-deploy-integration-run-worker.yaml +++ b/.github/workflows/staging-deploy-integration-run-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-run-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.github/workflows/staging-deploy-integration-stream-worker.yaml b/.github/workflows/staging-deploy-integration-stream-worker.yaml index d7500f1d3a..bb993f8f7f 100644 --- a/.github/workflows/staging-deploy-integration-stream-worker.yaml +++ b/.github/workflows/staging-deploy-integration-stream-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-stream-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.github/workflows/staging-deploy-integration-sync-worker.yaml b/.github/workflows/staging-deploy-integration-sync-worker.yaml index e8674313c6..2085a4a796 100644 --- a/.github/workflows/staging-deploy-integration-sync-worker.yaml +++ b/.github/workflows/staging-deploy-integration-sync-worker.yaml @@ -58,3 +58,5 @@ jobs: service: integration-sync-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.github/workflows/staging-deploy-search-sync-worker.yaml b/.github/workflows/staging-deploy-search-sync-worker.yaml index 21e59a4da7..d74c4cbd36 100644 --- a/.github/workflows/staging-deploy-search-sync-worker.yaml +++ b/.github/workflows/staging-deploy-search-sync-worker.yaml @@ -58,3 +58,5 @@ jobs: service: search-sync-worker image: ${{ needs.build-and-push.outputs.image }} cluster: ${{ env.CROWD_CLUSTER }} + cloud_env: staging + prioritized: true diff --git a/.gitignore b/.gitignore index fc6c23bc76..c45d95311e 100644 --- a/.gitignore +++ b/.gitignore @@ -51,4 +51,4 @@ docker/volume services/libs/*/dist -**/.cubestore \ No newline at end of file +**/.cubestore diff --git a/backend/.env.dist.composed b/backend/.env.dist.composed index 3f548385e6..75bcfb7a47 100644 --- a/backend/.env.dist.composed +++ b/backend/.env.dist.composed @@ -1,8 +1,7 @@ # SQS settings CROWD_SQS_HOST="sqs" CROWD_SQS_ENDPOINT=http://sqs:9324 -CROWD_SQS_NODEJS_WORKER_QUEUE="http://sqs:9324/000000000000/nodejs-worker.fifo" -CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE=http://sqs:9324/000000000000/nodejs-worker +CROWD_SQS_NODEJS_WORKER_QUEUE="http://sqs:9324/000000000000/nodejs-worker-normal.fifo" CROWD_SQS_PYTHON_WORKER_QUEUE="http://sqs:9324/000000000000/python-worker.fifo" # Redis settings diff --git a/backend/.env.dist.local b/backend/.env.dist.local index 7d9a3815a2..d891c9b341 100755 --- a/backend/.env.dist.local +++ b/backend/.env.dist.local @@ -2,6 +2,7 @@ KUBE_MODE=1 CROWD_EDITION=community TENANT_MODE=multi +QUEUE_PRIORITY_LEVEL=normal # API settings CROWD_API_URL=https://localhost/api @@ -15,7 +16,7 @@ CROWD_SQS_HOST=localhost CROWD_SQS_PORT=9324 CROWD_SQS_ENDPOINT=http://localhost:9324 CROWD_SQS_NODEJS_WORKER_QUEUE=http://localhost:9324/000000000000/nodejs-worker.fifo -CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE=http://localhost:9324/000000000000/nodejs-worker +CROWD_SQS_NODEJS_WORKER_PRIORITY_QUEUE=http://localhost:9324/000000000000/nodejs-worker-normal.fifo CROWD_SQS_PYTHON_WORKER_QUEUE=http://localhost:9324/000000000000/python-worker.fifo CROWD_SQS_AWS_ACCOUNT_ID=000000000000 CROWD_SQS_AWS_ACCESS_KEY_ID=x diff --git a/backend/config/custom-environment-variables.json b/backend/config/custom-environment-variables.json index 185844c2f3..19e0ff9663 100644 --- a/backend/config/custom-environment-variables.json +++ b/backend/config/custom-environment-variables.json @@ -17,7 +17,7 @@ "host": "CROWD_SQS_HOST", "port": "CROWD_SQS_PORT", "nodejsWorkerQueue": "CROWD_SQS_NODEJS_WORKER_QUEUE", - "nodejsWorkerDelayableQueue": "CROWD_SQS_NODEJS_WORKER_DELAYABLE_QUEUE", + "nodejsWorkerPriorityQueue": "CROWD_SQS_NODEJS_WORKER_PRIORITY_QUEUE", "integrationRunWorkerQueue": "CROWD_SQS_INTEGRATION_RUN_WORKER_QUEUE", "pythonWorkerQueue": "CROWD_SQS_PYTHON_WORKER_QUEUE", "aws": { diff --git a/backend/docker-compose.test.yaml b/backend/docker-compose.test.yaml deleted file mode 100644 index c33870e511..0000000000 --- a/backend/docker-compose.test.yaml +++ /dev/null @@ -1,57 +0,0 @@ -version: '3.1' - -services: - db-test: - image: postgres:13.6-alpine - environment: - POSTGRES_PASSWORD: example - POSTGRES_DB: crowd-web - ports: - - 5433:5432 - networks: - - crowd-bridge-test - - sqs: - build: - context: ../scripts/scaffold/sqs - ports: - - 9325:9324 - - 9326:9325 - networks: - - crowd-bridge-test - - open-search-test: - image: opensearchproject/opensearch:2.7.0 - environment: - - discovery.type=single-node - - bootstrap.memory_lock=true - ulimits: - memlock: - soft: -1 - hard: -1 - ports: - - 9201:9200 - - 9601:9600 - networks: - - crowd-bridge-test - - redis-test: - image: redis - ports: - - 6380:6379 - networks: - - crowd-bridge-test - - temporal-test: - build: - context: ../scripts/scaffold/temporal - restart: always - ports: - - '7234:7233' - - '8234:8233' - networks: - - crowd-bridge-test - -networks: - crowd-bridge-test: - external: true diff --git a/backend/jest.config.js b/backend/jest.config.js deleted file mode 100644 index 6fee5b31b9..0000000000 --- a/backend/jest.config.js +++ /dev/null @@ -1,41 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ - -const tsconfig = require('./tsconfig.json') - -const fromPairs = (pairs) => pairs.reduce((res, [key, value]) => ({ ...res, [key]: value }), {}) - -/** - * tsconfig の paths の設定から moduleNameMapper を生成する - * {"@app/*": ["src/*"]} -> {"@app/(.*)": "/src/$1"} - */ -function moduleNameMapperFromTSPaths(tsconf) { - return fromPairs( - Object.entries(tsconf.compilerOptions.paths).map(([k, [v]]) => [ - k.replace(/\*/, '(.*)'), - `/${tsconf.compilerOptions.baseUrl}/${v.replace(/\*/, '$1')}`, - ]), - ) -} - -/** @type {import('ts-jest/dist/types').JestConfigWithTsJest} */ -module.exports = { - preset: 'ts-jest', - transform: { - '^.+\\.(ts|tsx)$': [ - 'ts-jest', - { - babelConfig: true, - isolatedModules: true, - }, - ], - }, - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - testEnvironment: 'node', - testPathIgnorePatterns: ['/dist'], - testTimeout: 90000, - testRegex: ['__tests__/.*tests?.ts$'], - bail: false, - roots: [''], - moduleNameMapper: moduleNameMapperFromTSPaths(tsconfig), - transformIgnorePatterns: ['node_modules/(?!(axios|@crowd/))/'], -} diff --git a/backend/package.json b/backend/package.json index 9194823c55..a295f03576 100644 --- a/backend/package.json +++ b/backend/package.json @@ -15,7 +15,6 @@ "start:discord-ws:dev": "nodemon --watch \"src/**/*.ts\" --watch ../services/libs -e ts,json --exec \"pnpm run start:discord-ws\"", "start:discord-ws:dev:local": "set -a && . ./.env.dist.local && . ./.env.override.local && set +a && pnpm run start:discord-ws:dev", "build": "tsc && pnpm run build:documentation && cp package*json dist/ && cp .sequelizerc dist/.sequelizerc ", - "test": "../scripts/cli scaffold up-test && jest --clearCache && set -a && . ./.env.dist.local && . ./.env.test && set +a && NODE_ENV=test SERVICE=test jest --runInBand --verbose --forceExit", "build:documentation": "copyfiles --flat ./src/documentation/openapi.json ./dist/documentation/", "db:create:test": "npx ts-node ./src/database/initializers/create test", "db:create:dev:source": "ts-node ./src/database/initializers/create dev", @@ -31,22 +30,21 @@ "format": "prettier --write .", "format-check": "prettier --check .", "tsc-check": "tsc --noEmit", - "script:process-integration": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-integration.ts", - "script:process-stream": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-stream.ts", "script:continue-run": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/continue-run.ts", "script:change-tenant-plan": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/change-tenant-plan.ts", - "script:process-webhook": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/process-webhook.ts", "script:trigger-webhook": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/trigger-webhook.ts", "script:send-weekly-analytics-email": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/send-weekly-analytics-email.ts", "script:unleash-init": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/unleash-init.ts", "script:enrich-members-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/enrich-members-and-organizations.ts", "script:enrich-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/enrich-organizations-synchronous.ts", "script:generate-merge-suggestions": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/generate-merge-suggestions.ts", + "script:generate-merge-suggestions-synchronous": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/generate-merge-suggestions-synchronous.ts", "script:merge-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/merge-organizations.ts", "script:get-member-enrichment-data": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/get-member-enrichment-data.ts", "script:get-organization-enrichment-data": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/get-organization-enrichment-data.ts", "script:refresh-materialized-views": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/refresh-materialized-views.ts", - "script:unmerge-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/unmerge-members.ts" + "script:unmerge-members": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/unmerge-members.ts", + "script:merge-similar-organizations": "SERVICE=script TS_NODE_TRANSPILE_ONLY=true node -r tsconfig-paths/register -r ts-node/register src/bin/scripts/merge-similar-organizations.ts" }, "dependencies": { "@aws-sdk/client-comprehend": "^3.159.0", @@ -57,6 +55,7 @@ "@aws-sdk/util-format-url": "^3.226.0", "@crowd/alerting": "file:../services/libs/alerting", "@crowd/common": "file:../services/libs/common", + "@crowd/common_services": "file:../services/libs/common_services", "@crowd/cubejs": "file:../services/libs/cubejs", "@crowd/feature-flags": "file:../services/libs/feature-flags", "@crowd/integrations": "file:../services/libs/integrations", @@ -99,7 +98,7 @@ "cron-time-generator": "^1.3.0", "crowd-sentiment": "^1.1.7", "crypto-js": "^4.1.1", - "discord.js": "^14.7.1", + "discord.js": "^14.14.1", "dotenv": "8.2.0", "dotenv-expand": "^8.0.3", "emoji-dictionary": "^1.0.11", @@ -156,7 +155,6 @@ "@types/config": "^3.3.0", "@types/cron": "^2.0.0", "@types/html-to-text": "^8.1.1", - "@types/jest": "^29.5.1", "@types/node": "~18.0.4", "@types/sanitize-html": "^2.6.2", "@types/superagent": "^4.1.15", @@ -171,13 +169,11 @@ "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-openapi": "^0.0.4", - "jest": "^29.5.0", "node-mocks-http": "1.9.0", "nodemon": "2.0.4", "prettier": "^2.5.1", "rdme": "^7.2.0", "supertest": "^6.2.2", - "ts-jest": "^29.1.0", "ts-node": "10.6.0", "typescript": "^5.2.2" } diff --git a/backend/src/api/member/memberFind.ts b/backend/src/api/member/memberFind.ts index e95dfc4cfe..671ae22b25 100644 --- a/backend/src/api/member/memberFind.ts +++ b/backend/src/api/member/memberFind.ts @@ -1,8 +1,8 @@ -import { isFeatureEnabled } from '@crowd/feature-flags' import { FeatureFlag } from '@crowd/types' import Permissions from '../../security/permissions' import MemberService from '../../services/memberService' import PermissionChecker from '../../services/user/permissionChecker' +import isFeatureEnabled from '../../feature-flags/isFeatureEnabled' /** * GET /tenant/{tenantId}/member/{id} @@ -34,7 +34,14 @@ export default async (req, res) => { } } - const payload = await new MemberService(req).findById(req.params.id, true, true, segmentId) + let payload + if (await isFeatureEnabled(FeatureFlag.SERVE_PROFILES_OPENSEARCH, req)) { + payload = await new MemberService(req).findByIdOpensearch(req.params.id, segmentId) + // temp flag to notifiy the client that this is an opensearch response + payload.fromOpensearch = true + } else { + payload = await new MemberService(req).findById(req.params.id, true, true, segmentId) + } await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationFind.ts b/backend/src/api/organization/organizationFind.ts index 79888a0d38..c0c1d4ee43 100644 --- a/backend/src/api/organization/organizationFind.ts +++ b/backend/src/api/organization/organizationFind.ts @@ -1,8 +1,8 @@ import { FeatureFlag } from '@crowd/types' -import isFeatureEnabled from '@/feature-flags/isFeatureEnabled' import Permissions from '../../security/permissions' import OrganizationService from '../../services/organizationService' import PermissionChecker from '../../services/user/permissionChecker' +import isFeatureEnabled from '../../feature-flags/isFeatureEnabled' /** * GET /tenant/{tenantId}/organization/{id} @@ -34,7 +34,14 @@ export default async (req, res) => { } } - const payload = await new OrganizationService(req).findById(req.params.id, segmentId) + let payload + if (await isFeatureEnabled(FeatureFlag.SERVE_PROFILES_OPENSEARCH, req)) { + payload = await new OrganizationService(req).findByIdOpensearch(req.params.id, segmentId) + // temp flag to notifiy the client that this is an opensearch response + payload.fromOpensearch = true + } else { + payload = await new OrganizationService(req).findById(req.params.id, segmentId) + } await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/organization/organizationUpdate.ts b/backend/src/api/organization/organizationUpdate.ts index 2bfed8211e..42509dd842 100644 --- a/backend/src/api/organization/organizationUpdate.ts +++ b/backend/src/api/organization/organizationUpdate.ts @@ -21,7 +21,13 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.organizationEdit) - const payload = await new OrganizationService(req).update(req.params.id, req.body, true) + const payload = await new OrganizationService(req).update( + req.params.id, + req.body, + true, + true, + true, + ) await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/premium/enrichment/memberEnrich.ts b/backend/src/api/premium/enrichment/memberEnrich.ts index 73488b9438..abe1bbd663 100644 --- a/backend/src/api/premium/enrichment/memberEnrich.ts +++ b/backend/src/api/premium/enrichment/memberEnrich.ts @@ -1,6 +1,6 @@ import { RedisCache } from '@crowd/redis' import { getServiceLogger } from '@crowd/logging' -import { FeatureFlagRedisKey } from '@crowd/types' +import { FeatureFlagRedisKey, SyncMode } from '@crowd/types' import { getSecondsTillEndOfMonth } from '../../../utils/timing' import Permissions from '../../../security/permissions' import identifyTenant from '../../../segment/identifyTenant' @@ -29,7 +29,10 @@ const log = getServiceLogger() export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.memberEdit) - const payload = await new MemberEnrichmentService(req).enrichOne(req.params.id) + const payload = await new MemberEnrichmentService(req).enrichOne( + req.params.id, + SyncMode.SYNCHRONOUS, + ) track('Single member enrichment', { memberId: req.params.id }, { ...req }) diff --git a/backend/src/api/premium/enrichment/memberEnrichBulk.ts b/backend/src/api/premium/enrichment/memberEnrichBulk.ts index c385fa6204..6485d93ccb 100644 --- a/backend/src/api/premium/enrichment/memberEnrichBulk.ts +++ b/backend/src/api/premium/enrichment/memberEnrichBulk.ts @@ -5,11 +5,11 @@ import { FeatureFlag, FeatureFlagRedisKey } from '@crowd/types' import { getSecondsTillEndOfMonth } from '../../../utils/timing' import Permissions from '../../../security/permissions' import identifyTenant from '../../../segment/identifyTenant' -import { sendBulkEnrichMessage } from '../../../serverless/utils/nodeWorkerSQS' import PermissionChecker from '../../../services/user/permissionChecker' import track from '../../../segment/track' import { PLAN_LIMITS } from '../../../feature-flags/isFeatureEnabled' import SequelizeRepository from '../../../database/repositories/sequelizeRepository' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' const log = getServiceLogger() @@ -54,7 +54,8 @@ export default async (req, res) => { ) // send the message - await sendBulkEnrichMessage(tenant, membersToEnrich, segmentIds) + const emitter = await getNodejsWorkerEmitter() + await emitter.bulkEnrich(tenant, membersToEnrich, segmentIds) // update enrichment count, we'll also check failed enrichments and deduct these from grand total in bulkEnrichmentWorker const secondsRemainingUntilEndOfMonth = getSecondsTillEndOfMonth() diff --git a/backend/src/bin/discord-ws.ts b/backend/src/bin/discord-ws.ts index 98cda5c245..ac94e2ebce 100644 --- a/backend/src/bin/discord-ws.ts +++ b/backend/src/bin/discord-ws.ts @@ -140,25 +140,24 @@ async function spawnClient( }) // listen to discord events - client.on(Events.GuildMemberAdd, async (m) => { - const member = m as any + client.on(Events.GuildMemberAdd, async (member) => { + // discord.js is cruel. member object here is typed, + // but it has custom toString and toJSON methods + // and they you print and JSON.stringify it + // the structure turns out to be different await executeIfNotExists( - `member-${member.userId}`, + `discord-ws-member-${member.user.id}-${member.guild.id}`, cache, async () => { logger.debug( { member: member.displayName, - guildId: member.guildId ?? member.guild.id, - userId: member.userId, + guildId: member.guild.id, + userId: member.user.id, }, 'Member joined guild!', ) - await processPayload( - DiscordWebsocketEvent.MEMBER_ADDED, - member, - member.guildId ?? member.guild.id, - ) + await processPayload(DiscordWebsocketEvent.MEMBER_ADDED, member, member.guild.id) }, delayMilliseconds, ) @@ -167,7 +166,7 @@ async function spawnClient( client.on(Events.MessageCreate, async (message) => { if (message.type === MessageType.Default || message.type === MessageType.Reply) { await executeIfNotExists( - `msg-${message.id}`, + `discord-ws-msg-${message.id}`, cache, async () => { logger.debug( diff --git a/backend/src/bin/jobs/checkStuckIntegrationRuns.ts b/backend/src/bin/jobs/checkStuckIntegrationRuns.ts index 3c859c29ec..9b26846fcc 100644 --- a/backend/src/bin/jobs/checkStuckIntegrationRuns.ts +++ b/backend/src/bin/jobs/checkStuckIntegrationRuns.ts @@ -1,20 +1,15 @@ -import { processPaginated } from '@crowd/common' import { Logger, getChildLogger, getServiceChildLogger } from '@crowd/logging' +import { IntegrationRunState } from '@crowd/types' import cronGenerator from 'cron-time-generator' import moment from 'moment' -import { IntegrationRunState } from '@crowd/types' import { INTEGRATION_PROCESSING_CONFIG } from '../../conf' -import IncomingWebhookRepository from '../../database/repositories/incomingWebhookRepository' import IntegrationRepository from '../../database/repositories/integrationRepository' import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' import IntegrationStreamRepository from '../../database/repositories/integrationStreamRepository' import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' import { IntegrationRun } from '../../types/integrationRunTypes' import { IntegrationStreamState } from '../../types/integrationStreamTypes' import { CrowdJob } from '../../types/jobTypes' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' -import { WebhookProcessor } from '../../serverless/integrations/services/webhookProcessor' const log = getServiceChildLogger('checkStuckIntegrationRuns') @@ -221,32 +216,6 @@ export const checkRuns = async (): Promise => { } } -export const checkStuckWebhooks = async (): Promise => { - const dbOptions = await SequelizeRepository.getDefaultIRepositoryOptions() - const repo = new IncomingWebhookRepository(dbOptions) - - // update retryable error state webhooks to pending state - let errorWebhooks = await repo.findError(1, 20, WebhookProcessor.MAX_RETRY_LIMIT) - - while (errorWebhooks.length > 0) { - await repo.markAllPending(errorWebhooks.map((w) => w.id)) - errorWebhooks = await repo.findError(1, 20, WebhookProcessor.MAX_RETRY_LIMIT) - } - - await processPaginated( - async (page) => repo.findPending(page, 20), - async (webhooks) => { - for (const webhook of webhooks) { - log.warn({ id: webhook.id }, 'Found stuck webhook! Restarting it!') - await sendNodeWorkerMessage( - webhook.tenantId, - new NodeWorkerProcessWebhookMessage(webhook.tenantId, webhook.id), - ) - } - }, - ) -} - const job: CrowdJob = { name: 'Detect & Fix Stuck Integration Runs', cronTime: cronGenerator.every(90).minutes(), @@ -254,7 +223,7 @@ const job: CrowdJob = { if (!running) { running = true try { - await Promise.all([checkRuns(), checkStuckIntegrations(), checkStuckWebhooks()]) + await Promise.all([checkRuns(), checkStuckIntegrations()]) } finally { running = false } diff --git a/backend/src/bin/jobs/integrationDataChecker.ts b/backend/src/bin/jobs/integrationDataChecker.ts index 8568b2e1fe..32a1600db9 100644 --- a/backend/src/bin/jobs/integrationDataChecker.ts +++ b/backend/src/bin/jobs/integrationDataChecker.ts @@ -1,8 +1,6 @@ +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import SequelizeRepository from '../../database/repositories/sequelizeRepository' import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' const job: CrowdJob = { name: 'Integration Data Checker', @@ -17,13 +15,9 @@ const job: CrowdJob = { }, }) + const emitter = await getNodejsWorkerEmitter() for (const integration of integrations) { - await sendNodeWorkerMessage(integration.id, { - tenantId: integration.tenantId, - type: NodeWorkerMessageType.NODE_MICROSERVICE, - integrationId: integration.id, - service: 'integration-data-checker', - } as NodeWorkerMessageBase) + await emitter.integrationDataChecker(integration.tenantId, integration.id) } }, } diff --git a/backend/src/bin/jobs/mergeSuggestions.ts b/backend/src/bin/jobs/mergeSuggestions.ts index 8cbc69b1cf..e646cb10fa 100644 --- a/backend/src/bin/jobs/mergeSuggestions.ts +++ b/backend/src/bin/jobs/mergeSuggestions.ts @@ -1,10 +1,8 @@ -import cronGenerator from 'cron-time-generator' import { timeout } from '@crowd/common' +import cronGenerator from 'cron-time-generator' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import TenantService from '../../services/tenantService' import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' const job: CrowdJob = { name: 'Merge suggestions', @@ -12,12 +10,10 @@ const job: CrowdJob = { cronTime: cronGenerator.every(12).hours(), onTrigger: async () => { const tenants = await TenantService._findAndCountAllForEveryUser({}) + const emitter = await getNodejsWorkerEmitter() + for (const tenant of tenants.rows) { - await sendNodeWorkerMessage(tenant.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenant.id, - service: 'merge-suggestions', - } as NodeWorkerMessageBase) + await emitter.mergeSuggestions(tenant.id) await timeout(300) } diff --git a/backend/src/bin/jobs/organizationEnricher.ts b/backend/src/bin/jobs/organizationEnricher.ts index 4d0eb48ffa..a9a4b8b357 100644 --- a/backend/src/bin/jobs/organizationEnricher.ts +++ b/backend/src/bin/jobs/organizationEnricher.ts @@ -1,11 +1,9 @@ -import cronGenerator from 'cron-time-generator' import { getServiceLogger } from '@crowd/logging' +import cronGenerator from 'cron-time-generator' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' import TenantRepository from '../../database/repositories/tenantRepository' +import { CrowdJob } from '../../types/jobTypes' const job: CrowdJob = { name: 'organization enricher', @@ -18,14 +16,10 @@ async function sendWorkerMessage() { const log = getServiceLogger() const tenants = await TenantRepository.getPayingTenantIds(options) log.info(tenants) + + const emitter = await getNodejsWorkerEmitter() for (const { id } of tenants) { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'enrich-organizations', - tenantId: id, - } as NodeWorkerMessageBase - log.info({ payload }, 'enricher worker payload') - await sendNodeWorkerMessage(id, payload) + await emitter.enrichOrganizations(id) } } diff --git a/backend/src/bin/jobs/refreshSampleData.ts b/backend/src/bin/jobs/refreshSampleData.ts index efed37c975..62e16bad3e 100644 --- a/backend/src/bin/jobs/refreshSampleData.ts +++ b/backend/src/bin/jobs/refreshSampleData.ts @@ -1,17 +1,13 @@ +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import { CrowdJob } from '../../types/jobTypes' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' const job: CrowdJob = { name: 'Refresh sample data', // every day cronTime: '0 0 * * *', onTrigger: async () => { - await sendNodeWorkerMessage('refresh-sample-data', { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'refresh-sample-data', - } as NodeWorkerMessageBase) + const emitter = await getNodejsWorkerEmitter() + await emitter.refreshSampleData() }, } diff --git a/backend/src/bin/nodejs-worker.ts b/backend/src/bin/nodejs-worker.ts index ce29a53699..11b90aa6ec 100644 --- a/backend/src/bin/nodejs-worker.ts +++ b/backend/src/bin/nodejs-worker.ts @@ -1,33 +1,27 @@ import { timeout } from '@crowd/common' import { Logger, getChildLogger, getServiceLogger } from '@crowd/logging' +import { RedisClient, getRedisClient } from '@crowd/redis' import { SqsDeleteMessageRequest, SqsMessage, SqsReceiveMessageRequest, deleteMessage, receiveMessage, - sendMessage, } from '@crowd/sqs' -import { SpanStatusCode, getServiceTracer } from '@crowd/tracing' -import moment from 'moment' -import { getRedisClient, RedisClient } from '@crowd/redis' -import { Sequelize, QueryTypes } from 'sequelize' import fs from 'fs' import path from 'path' +import { QueryTypes, Sequelize } from 'sequelize' import telemetry from '@crowd/telemetry' +import { SQS_CLIENT, getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' +import { databaseInit } from '@/database/databaseConnection' import { REDIS_CONFIG, SQS_CONFIG } from '../conf' import { processDbOperationsMessage } from '../serverless/dbOperations/workDispatcher' import { processNodeMicroserviceMessage } from '../serverless/microservices/nodejs/workDispatcher' import { NodeWorkerMessageType } from '../serverless/types/workerTypes' -import { sendNodeWorkerMessage } from '../serverless/utils/nodeWorkerSQS' import { NodeWorkerMessageBase } from '../types/mq/nodeWorkerMessageBase' -import { processIntegration, processWebhook } from './worker/integrations' -import { SQS_CLIENT } from '@/serverless/utils/serviceSQS' -import { databaseInit } from '@/database/databaseConnection' /* eslint-disable no-constant-condition */ -const tracer = getServiceTracer() const serviceLogger = getServiceLogger() let exiting = false @@ -39,12 +33,9 @@ process.on('SIGTERM', async () => { exiting = true }) -const receive = async (delayed?: boolean): Promise => { +const receive = async (queue: string): Promise => { const params: SqsReceiveMessageRequest = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, - MessageAttributeNames: !delayed - ? undefined - : ['remainingDelaySeconds', 'tenantId', 'targetQueueUrl'], + QueueUrl: queue, } const messages = await receiveMessage(SQS_CLIENT(), params) @@ -56,95 +47,18 @@ const receive = async (delayed?: boolean): Promise => { return undefined } -const removeFromQueue = (receiptHandle: string, delayed?: boolean): Promise => { +const removeFromQueue = (queue: string, receiptHandle: string): Promise => { const params: SqsDeleteMessageRequest = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, + QueueUrl: queue, ReceiptHandle: receiptHandle, } return deleteMessage(SQS_CLIENT(), params) } -async function handleDelayedMessages() { - const delayedHandlerLogger = getChildLogger('delayedMessages', serviceLogger, { - queue: SQS_CONFIG.nodejsWorkerDelayableQueue, - }) - delayedHandlerLogger.info('Listing for delayed messages!') - - // noinspection InfiniteLoopJS - while (!exiting) { - const message = await receive(true) - - if (message) { - await tracer.startActiveSpan('ProcessDelayedMessage', async (span) => { - try { - const msg: NodeWorkerMessageBase = JSON.parse(message.Body) - const messageLogger = getChildLogger('messageHandler', serviceLogger, { - messageId: message.MessageId, - type: msg.type, - }) - - if (message.MessageAttributes && message.MessageAttributes.remainingDelaySeconds) { - // re-delay - const newDelay = parseInt( - message.MessageAttributes.remainingDelaySeconds.StringValue, - 10, - ) - const tenantId = message.MessageAttributes.tenantId.StringValue - messageLogger.debug({ newDelay, tenantId }, 'Re-delaying message!') - await sendNodeWorkerMessage(tenantId, msg, newDelay) - } else { - // just emit to the normal queue for processing - const tenantId = message.MessageAttributes.tenantId.StringValue - - if (message.MessageAttributes.targetQueueUrl) { - const targetQueueUrl = message.MessageAttributes.targetQueueUrl.StringValue - messageLogger.debug({ tenantId, targetQueueUrl }, 'Successfully delayed a message!') - await sendMessage(SQS_CLIENT(), { - QueueUrl: targetQueueUrl, - MessageGroupId: tenantId, - MessageDeduplicationId: `${tenantId}-${moment().valueOf()}`, - MessageBody: JSON.stringify(msg), - }) - } else { - messageLogger.debug({ tenantId }, 'Successfully delayed a message!') - await sendNodeWorkerMessage(tenantId, msg) - } - } - - await removeFromQueue(message.ReceiptHandle, true) - span.setStatus({ - code: SpanStatusCode.OK, - }) - } catch (err) { - span.setStatus({ - code: SpanStatusCode.ERROR, - message: err, - }) - } finally { - span.end() - } - }) - } else { - delayedHandlerLogger.trace('No message received!') - } - } - - delayedHandlerLogger.warn('Exiting!') -} - -let processingMessages = 0 -const isWorkerAvailable = (): boolean => processingMessages <= 3 -const addWorkerJob = (): void => { - processingMessages++ -} -const removeWorkerJob = (): void => { - processingMessages-- -} - -async function handleMessages() { +async function handleMessages(queue: string) { const handlerLogger = getChildLogger('messages', serviceLogger, { - queue: SQS_CONFIG.nodejsWorkerQueue, + queue, }) handlerLogger.info('Listening for messages!') @@ -164,30 +78,21 @@ async function handleMessages() { messageLogger.warn( 'Skipping enrich_member_organizations message! Purging the queue because they are not needed anymore!', ) - await removeFromQueue(message.ReceiptHandle) + await removeFromQueue(queue, message.ReceiptHandle) return } - messageLogger.debug( - { messageType: msg.type, messagePayload: JSON.stringify(msg) }, - 'Received a new queue message!', - ) + messageLogger.debug({ messageType: msg.type }, 'Received a new queue message!') let processFunction: (msg: NodeWorkerMessageBase, logger?: Logger) => Promise switch (msg.type) { - case NodeWorkerMessageType.INTEGRATION_PROCESS: - processFunction = processIntegration - break case NodeWorkerMessageType.NODE_MICROSERVICE: processFunction = processNodeMicroserviceMessage break case NodeWorkerMessageType.DB_OPERATIONS: processFunction = processDbOperationsMessage break - case NodeWorkerMessageType.PROCESS_WEBHOOK: - processFunction = processWebhook - break default: messageLogger.error('Error while parsing queue message! Invalid type.') @@ -198,7 +103,7 @@ async function handleMessages() { 'nodejs_worker.process_message', async () => { // remove the message from the queue as it's about to be processed - await removeFromQueue(message.ReceiptHandle) + await removeFromQueue(queue, message.ReceiptHandle) messagesInProgress.set(message.MessageId, msg) try { await processFunction(msg, messageLogger) @@ -212,16 +117,30 @@ async function handleMessages() { type: msg.type, }, ) + } else { + messageLogger.error( + { messageType: msg.type }, + 'Error while parsing queue message! Invalid type.', + ) } } catch (err) { messageLogger.error(err, { payload: msg }, 'Error while processing queue message!') } } + let processingMessages = 0 + const isWorkerAvailable = (): boolean => processingMessages <= 3 + const addWorkerJob = (): void => { + processingMessages++ + } + const removeWorkerJob = (): void => { + processingMessages-- + } + // noinspection InfiniteLoopJS while (!exiting) { if (isWorkerAvailable()) { - const message = await receive() + const message = await receive(queue) if (message) { addWorkerJob() @@ -262,8 +181,12 @@ const initRedisSeq = async () => { setImmediate(async () => { await initRedisSeq() - const promises = [handleMessages(), handleDelayedMessages()] - await Promise.all(promises) + + await getNodejsWorkerEmitter() + await Promise.all([ + handleMessages(SQS_CONFIG.nodejsWorkerQueue), + handleMessages(SQS_CONFIG.nodejsWorkerPriorityQueue), + ]) }) const liveFilePath = path.join(__dirname, 'tmp/nodejs-worker-live.tmp') diff --git a/backend/src/bin/scripts/continue-run.ts b/backend/src/bin/scripts/continue-run.ts deleted file mode 100644 index af5dd5be49..0000000000 --- a/backend/src/bin/scripts/continue-run.ts +++ /dev/null @@ -1,103 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import { IntegrationRunState } from '@crowd/types' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'run', - alias: 'r', - typeLabel: '{underline runId}', - type: String, - description: - 'The unique ID of integration run that you would like to continue processing. Use comma delimiter when sending multiple integration runs.', - }, - { - name: 'disableFiringCrowdWebhooks', - alias: 'd', - typeLabel: '{underline disableFiringCrowdWebhooks}', - type: Boolean, - defaultOption: false, - description: 'Should it disable firing outgoing crowd webhooks?', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Continue Processing Integration Run', - content: 'Trigger processing of integration run.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help && !parameters.run) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const fireCrowdWebhooks = !parameters.disableFiringCrowdWebhooks - - const runRepo = new IntegrationRunRepository(options) - - const runIds = parameters.run.split(',') - for (const runId of runIds) { - const run = await runRepo.findById(runId) - - if (!run) { - log.error({ runId }, 'Integration run not found!') - process.exit(1) - } else { - await log.info({ runId }, 'Integration run found - triggering SQS message!') - - if (run.state !== IntegrationRunState.PENDING) { - log.warn( - { currentState: run.state }, - `Setting integration state to ${IntegrationRunState.PENDING}!`, - ) - await runRepo.restart(run.id) - } - - if (!fireCrowdWebhooks) { - log.info( - 'fireCrowdWebhooks is false - This continue-run will not trigger outgoing crowd webhooks!', - ) - } - - await sendNodeWorkerMessage( - run.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, null, fireCrowdWebhooks), - ) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/enrich-members-and-organizations.ts b/backend/src/bin/scripts/enrich-members-and-organizations.ts index 0943cc3045..ea89bbf40e 100644 --- a/backend/src/bin/scripts/enrich-members-and-organizations.ts +++ b/backend/src/bin/scripts/enrich-members-and-organizations.ts @@ -1,16 +1,14 @@ +import { getServiceLogger } from '@crowd/logging' import commandLineArgs from 'command-line-args' import commandLineUsage from 'command-line-usage' import * as fs from 'fs' import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '@/database/repositories/sequelizeRepository' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import MemberRepository from '@/database/repositories/memberRepository' -import { sendBulkEnrichMessage, sendNodeWorkerMessage } from '@/serverless/utils/nodeWorkerSQS' import OrganizationRepository from '@/database/repositories/organizationRepository' -import { NodeWorkerMessageType } from '@/serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '@/types/mq/nodeWorkerMessageBase' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' import getUserContext from '@/database/utils/getUserContext' -import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import SegmentService from '@/services/segmentService' /* eslint-disable no-console */ @@ -80,6 +78,7 @@ if (parameters.help || (!parameters.tenant && (!parameters.organization || !para const enrichMembers = parameters.member const enrichOrganizations = parameters.organization const limit = 1000 + const emitter = await getNodejsWorkerEmitter() for (const tenantId of tenantIds) { const options = await SequelizeRepository.getDefaultIRepositoryOptions() @@ -112,7 +111,7 @@ if (parameters.help || (!parameters.tenant && (!parameters.organization || !para if (enrichMembers) { if (parameters.memberIds) { const memberIds = parameters.memberIds.split(',') - await sendBulkEnrichMessage(tenantId, memberIds, segmentIds, false, true) + await emitter.bulkEnrich(tenantId, memberIds, segmentIds, false, true) log.info( { tenantId }, `Enrichment message for ${memberIds.length} sent to nodejs-worker!`, @@ -134,7 +133,7 @@ if (parameters.help || (!parameters.tenant && (!parameters.organization || !para optionsWithTenant, ) - await sendBulkEnrichMessage(tenantId, memberIds, segmentIds, false, true) + await emitter.bulkEnrich(tenantId, memberIds, segmentIds, false, true) offset += limit } while (totalMembers > offset) @@ -150,16 +149,7 @@ if (parameters.help || (!parameters.tenant && (!parameters.organization || !para log.info({ tenantId }, `Total organizations found in the tenant: ${totalOrganizations}`) - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'enrich-organizations', - tenantId, - // Since there is no pagination implemented for the organizations enrichment, - // we set a limit of 10,000 to ensure all organizations are included when enriched in bulk. - maxEnrichLimit: 10000, - } as NodeWorkerMessageBase - - await sendNodeWorkerMessage(tenantId, payload) + await emitter.enrichOrganizations(tenantId, 10000) log.info( { tenantId }, `Organizations enrichment operation finished for tenant ${tenantId}`, diff --git a/backend/src/bin/scripts/generate-merge-suggestions-synchronous.ts b/backend/src/bin/scripts/generate-merge-suggestions-synchronous.ts new file mode 100644 index 0000000000..3b5d4e614f --- /dev/null +++ b/backend/src/bin/scripts/generate-merge-suggestions-synchronous.ts @@ -0,0 +1,102 @@ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import { getOpensearchClient } from '@crowd/opensearch' +import { OrganizationMergeSuggestionType } from '@crowd/types' +import * as fs from 'fs' +import path from 'path' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import getUserContext from '@/database/utils/getUserContext' +import SegmentService from '@/services/segmentService' +import { OPENSEARCH_CONFIG } from '@/conf' +import OrganizationService from '@/services/organizationService' +import TenantService from '@/services/tenantService' + +/* eslint-disable no-console */ + +const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') + +const options = [ + { + name: 'tenant', + alias: 't', + type: String, + description: + 'The unique ID of that tenant that you would like to generate merge suggestions for.', + }, + { + name: 'plan', + alias: 'p', + type: String, + description: + 'Comma separated plans - works with allTenants flag. Only generate suggestions for tenants with specific plans. Available plans: Growth, Scale, Enterprise', + }, + { + name: 'allTenants', + alias: 'a', + type: Boolean, + defaultValue: false, + description: 'Set this flag to merge similar organizations for all tenants.', + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] +const sections = [ + { + content: banner, + raw: true, + }, + { + header: 'Generate merge suggestions for a tenant', + content: 'Generate merge suggestions for a tenant', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help || (!parameters.tenant && !parameters.allTenants)) { + console.log(usage) +} else { + setImmediate(async () => { + let tenantIds + + if (parameters.allTenants) { + tenantIds = (await TenantService._findAndCountAllForEveryUser({})).rows + if (parameters.plan) { + tenantIds = tenantIds.filter((tenant) => parameters.plan.split(',').includes(tenant.plan)) + } + tenantIds = tenantIds.map((t) => t.id) + } else if (parameters.tenant) { + tenantIds = parameters.tenant.split(',') + } else { + tenantIds = [] + } + + for (const tenantId of tenantIds) { + const userContext: IRepositoryOptions = await getUserContext(tenantId) + const segmentService = new SegmentService(userContext) + const { rows: segments } = await segmentService.querySubprojects({}) + userContext.currentSegments = segments + userContext.opensearch = getOpensearchClient(OPENSEARCH_CONFIG) + + console.log(`Generating organization merge suggestions for tenant ${tenantId}!`) + + const organizationService = new OrganizationService(userContext) + await organizationService.generateMergeSuggestions( + OrganizationMergeSuggestionType.BY_IDENTITY, + ) + + console.log(`Done generating organization merge suggestions for tenant ${tenantId}!`) + } + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/generate-merge-suggestions.ts b/backend/src/bin/scripts/generate-merge-suggestions.ts index 911f5fc184..4d96f1a3e1 100644 --- a/backend/src/bin/scripts/generate-merge-suggestions.ts +++ b/backend/src/bin/scripts/generate-merge-suggestions.ts @@ -2,9 +2,7 @@ import commandLineArgs from 'command-line-args' import commandLineUsage from 'command-line-usage' import * as fs from 'fs' import path from 'path' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerMessageType } from '../../serverless/types/workerTypes' -import { NodeWorkerMessageBase } from '@/types/mq/nodeWorkerMessageBase' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' /* eslint-disable no-console */ @@ -48,13 +46,9 @@ if (parameters.help || !parameters.tenant) { } else { setImmediate(async () => { const tenantIds = parameters.tenant.split(',') - + const emitter = await getNodejsWorkerEmitter() for (const tenantId of tenantIds) { - await sendNodeWorkerMessage(tenantId, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant: tenantId, - service: 'merge-suggestions', - } as NodeWorkerMessageBase) + await emitter.mergeSuggestions(tenantId) } process.exit(0) diff --git a/backend/src/bin/scripts/merge-similar-organizations.ts b/backend/src/bin/scripts/merge-similar-organizations.ts new file mode 100644 index 0000000000..5199f4c4ab --- /dev/null +++ b/backend/src/bin/scripts/merge-similar-organizations.ts @@ -0,0 +1,177 @@ +import commandLineArgs from 'command-line-args' +import commandLineUsage from 'command-line-usage' +import { QueryTypes } from 'sequelize' +import * as fs from 'fs' +import path from 'path' +import SequelizeRepository from '../../database/repositories/sequelizeRepository' +import TenantService from '@/services/tenantService' +import OrganizationService from '@/services/organizationService' +import getUserContext from '@/database/utils/getUserContext' +import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' +import { + MergeActionState, + MergeActionType, + MergeActionsRepository, +} from '@/database/repositories/mergeActionsRepository' + +/* eslint-disable no-console */ + +const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') + +const options = [ + { + name: 'tenant', + alias: 't', + type: String, + description: 'The unique ID of tenant', + }, + { + name: 'allTenants', + alias: 'a', + type: Boolean, + defaultValue: false, + description: 'Set this flag to merge similar organizations for all tenants.', + }, + { + name: 'similarityThreshold', + alias: 's', + type: String, + defaultValue: false, + description: + 'Similarity threshold of organization merge suggestions. Suggestions lower than this value will not be merged. Defaults to 0.95', + }, + { + name: 'hardLimit', + alias: 'l', + type: String, + defaultValue: false, + description: `Hard limit for # of organizations that'll be merged. Mostly a flag for testing purposes.`, + }, + { + name: 'help', + alias: 'h', + type: Boolean, + description: 'Print this usage guide.', + }, +] +const sections = [ + { + content: banner, + raw: true, + }, + { + header: 'Merge organizations with similarity higher than given threshold.', + content: 'Merge organizations with similarity higher than given threshold.', + }, + { + header: 'Options', + optionList: options, + }, +] + +const usage = commandLineUsage(sections) +const parameters = commandLineArgs(options) + +if (parameters.help || (!parameters.tenant && !parameters.allTenants)) { + console.log(usage) +} else { + setImmediate(async () => { + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + + let tenantIds + + if (parameters.allTenants) { + tenantIds = (await TenantService._findAndCountAllForEveryUser({})).rows.map((t) => t.id) + } else if (parameters.tenant) { + tenantIds = parameters.tenant.split(',') + } else { + tenantIds = [] + } + + for (const tenantId of tenantIds) { + const userContext: IRepositoryOptions = await getUserContext(tenantId) + const orgService = new OrganizationService(userContext) + + let hasMoreData = true + let counter = 0 + + while (hasMoreData) { + // find organization merge suggestions of tenant + const result = await options.database.sequelize.query( + ` + SELECT + "ot"."organizationId", + "ot"."toMergeId", + "ot".similarity, + "ot".status, + "org1"."displayName" AS "orgDisplayName", + "org2"."displayName" AS "mergeDisplayName" + FROM + "organizationToMerge" "ot" + LEFT JOIN + "organizations" "org1" + ON + "ot"."organizationId" = "org1"."id" + LEFT JOIN + "organizations" "org2" + ON + "ot"."toMergeId" = "org2"."id" + WHERE + ("ot".similarity > :similarityThreshold) AND + ("org1"."displayName" ilike "org2"."displayName") AND + ("org1"."tenantId" = :tenantId) AND + ("org2"."tenantId" = :tenantId) + ORDER BY + "ot".similarity DESC + LIMIT 100 + OFFSET :offset;`, + { + replacements: { + similarityThreshold: parameters.similarityThreshold || 0.95, + offset: 0, + tenantId, + }, + type: QueryTypes.SELECT, + }, + ) + + if (result.length === 0) { + hasMoreData = false + } else { + for (const row of result) { + try { + console.log( + `Merging [${row.organizationId}] "${row.orgDisplayName}" into ${row.toMergeId} "${row.mergeDisplayName}"...`, + ) + await MergeActionsRepository.add( + MergeActionType.ORG, + row.organizationId, + row.toMergeId, + userContext, + ) + await orgService.mergeSync(row.organizationId, row.toMergeId) + } catch (err) { + console.log('Error merging organizations - continuing with the rest', err) + await MergeActionsRepository.setState( + MergeActionType.ORG, + row.organizationId, + row.toMergeId, + MergeActionState.ERROR, + userContext, + ) + } + + if (parameters.hardLimit && counter >= parameters.hardLimit) { + console.log(`Hard limit of ${parameters.hardLimit} reached. Exiting...`) + process.exit(0) + } + + counter += 1 + } + } + } + } + + process.exit(0) + }) +} diff --git a/backend/src/bin/scripts/process-integration.ts b/backend/src/bin/scripts/process-integration.ts deleted file mode 100644 index 499571d6bf..0000000000 --- a/backend/src/bin/scripts/process-integration.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { processPaginated, singleOrDefault } from '@crowd/common' -import { INTEGRATION_SERVICES } from '@crowd/integrations' -import { getServiceLogger } from '@crowd/logging' -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { IntegrationRunState } from '@crowd/types' -import IntegrationRepository from '../../database/repositories/integrationRepository' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { getIntegrationRunWorkerEmitter } from '../../serverless/utils/serviceSQS' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'integration', - alias: 'i', - typeLabel: '{underline integrationId}', - type: String, - description: - 'The unique ID of integration that you would like to process. Use comma delimiter when sending multiple integrations.', - }, - { - name: 'onboarding', - alias: 'o', - description: 'Process integration as if it was onboarding.', - type: Boolean, - defaultValue: false, - }, - { - name: 'disableFiringCrowdWebhooks', - alias: 'd', - typeLabel: '{underline disableFiringCrowdWebhooks}', - type: Boolean, - defaultOption: false, - description: 'Should it disable firing outgoing crowd webhooks?', - }, - { - name: 'platform', - alias: 'p', - description: 'The platform for which we should run all integrations.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process Integration', - content: 'Trigger processing of integrations.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -const triggerIntegrationRun = async ( - runRepo: IntegrationRunRepository, - tenantId: string, - integrationId: string, - onboarding: boolean, - fireCrowdWebhooks: boolean, -) => { - const existingRun = await runRepo.findLastProcessingRun(integrationId) - - if (existingRun && existingRun.onboarding) { - log.error('Integration is already processing, skipping!') - return - } - - log.info( - { integrationId, onboarding }, - 'Integration found - creating a new run in the old framework!', - ) - const run = await runRepo.create({ - integrationId, - tenantId, - onboarding, - state: IntegrationRunState.PENDING, - }) - - log.info( - { integrationId, onboarding }, - 'Triggering SQS message for the old framework integration!', - ) - await sendNodeWorkerMessage( - tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, null, fireCrowdWebhooks), - ) -} - -const triggerNewIntegrationRun = async ( - tenantId: string, - integrationId: string, - platform: string, - onboarding: boolean, -) => { - log.info( - { integrationId, onboarding }, - 'Triggering SQS message for the new framework integration!', - ) - - const emitter = await getIntegrationRunWorkerEmitter() - await emitter.triggerIntegrationRun(tenantId, platform, integrationId, onboarding) -} - -if (parameters.help || (!parameters.integration && !parameters.platform)) { - console.log(usage) -} else { - setImmediate(async () => { - const onboarding = parameters.onboarding - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const fireCrowdWebhooks = !parameters.disableFiringCrowdWebhooks - - const runRepo = new IntegrationRunRepository(options) - - if (parameters.platform) { - let inNewFramework = false - - if (singleOrDefault(INTEGRATION_SERVICES, (s) => s.type === parameters.platform)) { - inNewFramework = true - } - - await processPaginated( - async (page) => IntegrationRepository.findAllActive(parameters.platform, page, 10), - async (integrations) => { - for (const i of integrations) { - const integration = i as any - - if (inNewFramework) { - await triggerNewIntegrationRun( - integration.tenantId, - integration.id, - integration.platform, - onboarding, - ) - } else { - await triggerIntegrationRun( - runRepo, - integration.tenantId, - integration.id, - onboarding, - fireCrowdWebhooks, - ) - } - } - }, - ) - } else { - const integrationIds = parameters.integration.split(',') - for (const integrationId of integrationIds) { - const integration = await options.database.integration.findOne({ - where: { id: integrationId }, - }) - - if (!integration) { - log.error({ integrationId }, 'Integration not found!') - process.exit(1) - } else { - log.info({ integrationId, onboarding }, 'Integration found - triggering SQS message!') - - let inNewFramework = false - - if (singleOrDefault(INTEGRATION_SERVICES, (s) => s.type === integration.platform)) { - inNewFramework = true - } - - if (inNewFramework) { - await triggerNewIntegrationRun( - integration.tenantId, - integration.id, - integration.platform, - onboarding, - ) - } else { - await triggerIntegrationRun( - runRepo, - integration.tenantId, - integration.id, - onboarding, - fireCrowdWebhooks, - ) - } - } - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/process-stream.ts b/backend/src/bin/scripts/process-stream.ts deleted file mode 100644 index a207c14e6f..0000000000 --- a/backend/src/bin/scripts/process-stream.ts +++ /dev/null @@ -1,82 +0,0 @@ -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { getServiceLogger } from '@crowd/logging' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { sendNodeWorkerMessage } from '../../serverless/utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../database/repositories/integrationStreamRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'stream', - alias: 's', - typeLabel: '{underline streamId}', - type: String, - description: - 'The unique ID of integration stream that you would like to process. Use comma delimiter when sending multiple integration streams.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process integration stream', - content: 'Trigger processing of integration stream.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help && !parameters.stream) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - - const streamRepo = new IntegrationStreamRepository(options) - const runRepo = new IntegrationRunRepository(options) - - const streamIds = parameters.stream.split(',') - for (const streamId of streamIds) { - const stream = await streamRepo.findById(streamId) - - if (!stream) { - log.error({ streamId }, 'Integration stream not found!') - process.exit(1) - } else { - log.info({ streamId }, 'Integration stream found! Triggering SQS message!') - - const run = await runRepo.findById(stream.runId) - - await sendNodeWorkerMessage( - run.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id, stream.id), - ) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/process-webhook.ts b/backend/src/bin/scripts/process-webhook.ts deleted file mode 100644 index 8026b15b9a..0000000000 --- a/backend/src/bin/scripts/process-webhook.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { getServiceLogger } from '@crowd/logging' -import { getRedisClient } from '@crowd/redis' -import commandLineArgs from 'command-line-args' -import commandLineUsage from 'command-line-usage' -import * as fs from 'fs' -import path from 'path' -import { QueryTypes } from 'sequelize' -import { IntegrationProcessor } from '@/serverless/integrations/services/integrationProcessor' -import { REDIS_CONFIG } from '../../conf' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' - -/* eslint-disable no-console */ - -const banner = fs.readFileSync(path.join(__dirname, 'banner.txt'), 'utf8') - -const log = getServiceLogger() - -const options = [ - { - name: 'webhook', - alias: 'w', - typeLabel: '{underline webhookId}', - type: String, - description: - 'The unique ID of webhook that you would like to process. Use comma delimiter when sending multiple webhooks.', - }, - { - name: 'tenant', - alias: 't', - typeLabel: '{underline tenantId}', - type: String, - description: - 'The unique ID of tenant that you would like to process. Use in combination with type.', - }, - { - name: 'type', - alias: 'p', - typeLabel: '{underline type}', - type: String, - description: 'The webhook type to process. Use in combination with tenant.', - }, - { - name: 'help', - alias: 'h', - type: Boolean, - description: 'Print this usage guide.', - }, -] -const sections = [ - { - content: banner, - raw: true, - }, - { - header: 'Process Webhook', - content: 'Trigger processing of webhooks.', - }, - { - header: 'Options', - optionList: options, - }, -] - -const usage = commandLineUsage(sections) -const parameters = commandLineArgs(options) - -if (parameters.help || (!parameters.webhook && (!parameters.tenant || !parameters.type))) { - console.log(usage) -} else { - setImmediate(async () => { - const options = await SequelizeRepository.getDefaultIRepositoryOptions() - const redisEmitter = await getRedisClient(REDIS_CONFIG) - const integrationProcessorInstance = new IntegrationProcessor(options, redisEmitter) - - if (parameters.webhook) { - const webhookIds = parameters.webhook.split(',') - - for (const webhookId of webhookIds) { - log.info({ webhookId }, 'Webhook found - processing!') - await integrationProcessorInstance.processWebhook(webhookId, true, true) - } - } else if (parameters.tenant && parameters.type) { - const seq = SequelizeRepository.getSequelize(options) - - let ids = ( - await seq.query( - ` - select id from "incomingWebhooks" - where state in ('PENDING', 'ERROR') - and "tenantId" = :tenantId and type = :type - order by id - limit 100 - `, - { - type: QueryTypes.SELECT, - replacements: { - tenantId: parameters.tenant, - type: parameters.type, - }, - }, - ) - ).map((r) => (r as any).id) - - while (ids.length > 0) { - for (const webhookId of ids) { - log.info({ webhookId }, 'Webhook found - processing!') - await integrationProcessorInstance.processWebhook(webhookId, true, true) - } - - ids = ( - await seq.query( - ` - select id from "incomingWebhooks" - where state in ('PENDING', 'ERROR') - and "tenantId" = :tenantId and type = :type - and id > :id - order by id - limit 100 - `, - { - type: QueryTypes.SELECT, - replacements: { - tenantId: parameters.tenant, - type: parameters.type, - id: ids[ids.length - 1], - }, - }, - ) - ).map((r) => (r as any).id) - } - } - - process.exit(0) - }) -} diff --git a/backend/src/bin/scripts/unleash-init.ts b/backend/src/bin/scripts/unleash-init.ts index cadbc8f818..502d944198 100644 --- a/backend/src/bin/scripts/unleash-init.ts +++ b/backend/src/bin/scripts/unleash-init.ts @@ -243,6 +243,24 @@ const constaintConfiguration = { }, ], ], + + [FeatureFlag.SERVE_PROFILES_OPENSEARCH]: [ + [ + { + values: [ + Plans.values.scale, + Plans.values.eagleEye, + Plans.values.enterprise, + Plans.values.essential, + Plans.values.growth, + ], + inverted: false, + operator: 'IN', + contextName: 'plan', + caseInsensitive: false, + }, + ], + ], } let seq: any diff --git a/backend/src/bin/worker/integrations.ts b/backend/src/bin/worker/integrations.ts deleted file mode 100644 index 387de96fa8..0000000000 --- a/backend/src/bin/worker/integrations.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { getRedisClient } from '@crowd/redis' -import { Logger } from '@crowd/logging' -import { REDIS_CONFIG } from '../../conf' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { IntegrationProcessor } from '../../serverless/integrations/services/integrationProcessor' -import { IServiceOptions } from '../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../types/mq/nodeWorkerIntegrationProcessMessage' -import { NodeWorkerProcessWebhookMessage } from '../../types/mq/nodeWorkerProcessWebhookMessage' - -let integrationProcessorInstance: IntegrationProcessor - -async function getIntegrationProcessor(logger: Logger): Promise { - if (integrationProcessorInstance) return integrationProcessorInstance - - const options: IServiceOptions = { - ...(await SequelizeRepository.getDefaultIRepositoryOptions()), - log: logger, - } - - const redisEmitter = await getRedisClient(REDIS_CONFIG) - - integrationProcessorInstance = new IntegrationProcessor(options, redisEmitter) - - return integrationProcessorInstance -} - -export const processIntegration = async ( - msg: NodeWorkerIntegrationProcessMessage, - messageLogger: Logger, -): Promise => { - const processor = await getIntegrationProcessor(messageLogger) - await processor.process(msg) -} - -export const processWebhook = async ( - msg: NodeWorkerProcessWebhookMessage, - messageLogger: Logger, -): Promise => { - const processor = await getIntegrationProcessor(messageLogger) - await processor.processWebhook(msg.webhookId, msg.force, msg.fireCrowdWebhooks) -} diff --git a/backend/src/conf/configTypes.ts b/backend/src/conf/configTypes.ts index 87e91e4886..0016bea78b 100644 --- a/backend/src/conf/configTypes.ts +++ b/backend/src/conf/configTypes.ts @@ -23,7 +23,7 @@ export interface SQSConfiguration { host?: string port?: number nodejsWorkerQueue: string - nodejsWorkerDelayableQueue: string + nodejsWorkerPriorityQueue: string integrationRunWorkerQueue: string pythonWorkerQueue: string aws: AwsCredentials diff --git a/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts b/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts deleted file mode 100644 index dfe74294f0..0000000000 --- a/backend/src/database/initializers/twitterSourceIdsFixedTimestamps.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * This script is responsible for regenerating - * sourceIds for twitter follow activities that have timestamp > 1970-01-01 - */ - -import dotenv from 'dotenv' -import dotenvExpand from 'dotenv-expand' -import { getServiceLogger } from '@crowd/logging' -import { PlatformType } from '@crowd/types' -import ActivityService from '../../services/activityService' -import IntegrationService from '../../services/integrationService' -import TenantService from '../../services/tenantService' -import getUserContext from '../utils/getUserContext' -import { IntegrationServiceBase } from '../../serverless/integrations/services/integrationServiceBase' - -const path = require('path') - -const env = dotenv.config({ - path: path.resolve(__dirname, `../../../.env.staging`), -}) - -dotenvExpand.expand(env) - -const log = getServiceLogger() - -async function twitterFollowsFixSourceIdsWithTimestamp() { - const tenants = await TenantService._findAndCountAllForEveryUser({}) - - // for each tenant - for (const t of tenants.rows) { - const tenantId = t.id - // get user context - const userContext = await getUserContext(tenantId) - const integrationService = new IntegrationService(userContext) - - const twitterIntegration = ( - await integrationService.findAndCountAll({ filter: { platform: PlatformType.TWITTER } }) - ).rows[0] - - if (twitterIntegration) { - const actService = new ActivityService(userContext) - - // get activities where timestamp != 1970-01-01, we can query by > 2000-01-01 - const activities = await actService.findAndCountAll({ - filter: { type: 'follow', timestampRange: ['2000-01-01'] }, - }) - - for (const activity of activities.rows) { - log.info({ activity }, 'Activity') - // calculate sourceId with fixed timestamps - const sourceIdRegenerated = IntegrationServiceBase.generateSourceIdHash( - activity.communityMember.username.twitter, - 'follow', - '1970-01-01T00:00:00+00:00', - 'twitter', - ) - await actService.update(activity.id, { sourceId: sourceIdRegenerated }) - } - } - } -} - -twitterFollowsFixSourceIdsWithTimestamp() diff --git a/backend/src/database/migrations/R__cubejs-materialized-views.sql b/backend/src/database/migrations/R__cubejs-materialized-views.sql index efd002d274..280a7dc0f0 100644 --- a/backend/src/database/migrations/R__cubejs-materialized-views.sql +++ b/backend/src/database/migrations/R__cubejs-materialized-views.sql @@ -1,3 +1,4 @@ +-- Members DROP MATERIALIZED VIEW IF EXISTS mv_members_cube; CREATE MATERIALIZED VIEW IF NOT EXISTS mv_members_cube AS SELECT @@ -13,6 +14,11 @@ SELECT FROM members m ; +CREATE INDEX IF NOT EXISTS mv_members_cube_tenant ON mv_members_cube ("tenantId"); +CREATE UNIQUE INDEX IF NOT EXISTS mv_members_cube_id ON mv_members_cube (id); + + +-- Activities DROP MATERIALIZED VIEW IF EXISTS mv_activities_cube; CREATE MATERIALIZED VIEW IF NOT EXISTS mv_activities_cube AS SELECT @@ -37,6 +43,13 @@ FROM activities a WHERE a."deletedAt" IS NULL ; +CREATE INDEX IF NOT EXISTS mv_activities_cube_timestamp ON mv_activities_cube (timestamp); +CREATE INDEX IF NOT EXISTS mv_activities_cube_org_id ON mv_activities_cube ("organizationId"); +CREATE UNIQUE INDEX IF NOT EXISTS mv_activities_cube_id ON mv_activities_cube (id); +CREATE INDEX IF NOT EXISTS mv_activities_cube_tenantId_timestamp_idx ON mv_activities_cube ("tenantId", "timestamp"); + + +-- Organizations DROP MATERIALIZED VIEW IF EXISTS mv_organizations_cube; CREATE MATERIALIZED VIEW IF NOT EXISTS mv_organizations_cube AS SELECT @@ -51,6 +64,11 @@ JOIN activities a ON o.id = a."organizationId" GROUP BY o.id ; +CREATE UNIQUE INDEX IF NOT EXISTS mv_organizations_cube_id ON mv_organizations_cube (id); +CREATE INDEX IF NOT EXISTS mv_organizations_cube_tenantId ON mv_organizations_cube ("tenantId"); + + +-- Segments DROP MATERIALIZED VIEW IF EXISTS mv_segments_cube; CREATE MATERIALIZED VIEW IF NOT EXISTS mv_segments_cube AS SELECT @@ -59,11 +77,4 @@ SELECT FROM segments ; -CREATE INDEX IF NOT EXISTS mv_members_cube_tenant ON mv_members_cube ("tenantId"); -CREATE INDEX IF NOT EXISTS mv_activities_cube_timestamp ON mv_activities_cube (timestamp); -CREATE INDEX IF NOT EXISTS mv_activities_cube_org_id ON mv_activities_cube ("organizationId"); - -CREATE UNIQUE INDEX IF NOT EXISTS mv_members_cube_id ON mv_members_cube (id); -CREATE UNIQUE INDEX IF NOT EXISTS mv_activities_cube_id ON mv_activities_cube (id); -CREATE UNIQUE INDEX IF NOT EXISTS mv_organizations_cube_id ON mv_organizations_cube (id); CREATE UNIQUE INDEX IF NOT EXISTS mv_segments_cube_id ON mv_segments_cube (id); diff --git a/backend/src/database/migrations/U1701080323__tenant-priority-level.sql b/backend/src/database/migrations/U1701080323__tenant-priority-level.sql new file mode 100644 index 0000000000..595398663d --- /dev/null +++ b/backend/src/database/migrations/U1701080323__tenant-priority-level.sql @@ -0,0 +1,2 @@ +alter table tenants + drop column "priorityLevel"; \ No newline at end of file diff --git a/backend/src/database/migrations/U1702035391__track-manual-org-changes.sql b/backend/src/database/migrations/U1702035391__track-manual-org-changes.sql new file mode 100644 index 0000000000..da2fada1db --- /dev/null +++ b/backend/src/database/migrations/U1702035391__track-manual-org-changes.sql @@ -0,0 +1,2 @@ +alter table organizations + drop column "manuallyChangedFields"; diff --git a/backend/src/database/migrations/V1701080323__tenant-priority-level.sql b/backend/src/database/migrations/V1701080323__tenant-priority-level.sql new file mode 100644 index 0000000000..5b7461bdbe --- /dev/null +++ b/backend/src/database/migrations/V1701080323__tenant-priority-level.sql @@ -0,0 +1,2 @@ +alter table tenants + add column "priorityLevel" varchar(255); \ No newline at end of file diff --git a/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql b/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql new file mode 100644 index 0000000000..72653660ae --- /dev/null +++ b/backend/src/database/migrations/V1702035391__track-manual-org-changes.sql @@ -0,0 +1,2 @@ +alter table organizations + add column "manuallyChangedFields" text[] null; diff --git a/backend/src/database/models/organization.ts b/backend/src/database/models/organization.ts index f9002bcd3e..9dfae4b65a 100644 --- a/backend/src/database/models/organization.ts +++ b/backend/src/database/models/organization.ts @@ -221,6 +221,11 @@ export default (sequelize) => { type: DataTypes.JSONB, allowNull: true, }, + manuallyChangedFields: { + type: DataTypes.ARRAY(DataTypes.TEXT), + allowNull: true, + default: [], + }, }, { indexes: [ diff --git a/backend/src/database/repositories/__tests__/activityRepository.test.ts b/backend/src/database/repositories/__tests__/activityRepository.test.ts deleted file mode 100644 index 1dc727737f..0000000000 --- a/backend/src/database/repositories/__tests__/activityRepository.test.ts +++ /dev/null @@ -1,1765 +0,0 @@ -import { Error404 } from '@crowd/common' -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import ActivityRepository from '../activityRepository' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import TaskRepository from '../taskRepository' -import MemberAttributeSettingsRepository from '../memberAttributeSettingsRepository' -import MemberAttributeSettingsService from '../../../services/memberAttributeSettingsService' -import { DEFAULT_MEMBER_ATTRIBUTES, UNKNOWN_ACTIVITY_TYPE_DISPLAY } from '@crowd/integrations' -import OrganizationRepository from '../organizationRepository' - -const db = null - -describe('ActivityRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: activity.attributes, - body: 'Here', - type: 'activity', - title: 'Title', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - tasks: [], - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create a bare-bones activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - member: memberCreated.id, - username: 'test', - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: null, - title: null, - url: null, - channel: null, - sentiment: {}, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: false, - score: 2, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activityCreated.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should throw error when no platform given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no type given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - platform: 'activity', - timestamp: '2020-05-27T15:13:30Z', - attributes: { - replies: 12, - }, - username: 'test', - body: 'Here', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no timestamp given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - platform: PlatformType.GITHUB, - type: 'activity', - attributes: { - replies: 12, - }, - username: 'test', - body: 'Here', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await expect(() => - ActivityRepository.create(activity, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when sentiment is incorrect', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - // Incomplete Object - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 1, - sentiment: 'positive', - score: 1, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // No score - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.8, - negative: 0.2, - mixed: 0, - neutral: 0, - sentiment: 'positive', - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // Wrong Sentiment field - await expect(() => - ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.3, - negative: 0.2, - neutral: 0.5, - mixed: 0, - score: 0.1, - sentiment: 'smth', - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // Works with empty object - const created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: {}, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - expect(created.sentiment).toStrictEqual({}) - }) - - it('Should leave allowed HTML tags in body and title', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: '

This is some HTML

', - title: '

This is some Title HTML

', - url: 'https://github.com', - channel: 'channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: '

This is some HTML

', - type: 'activity', - title: '

This is some Title HTML

', - url: 'https://github.com', - channel: 'channel', - sentiment: {}, - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - tasks: [], - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should remove script tags in body and title', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: "

Malicious

", - title: "

Malicious title

", - url: 'https://github.com', - channel: 'channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: {}, - body: '

Malicious

', - type: 'activity', - title: '

Malicious title

', - url: 'https://github.com', - channel: 'channel', - sentiment: {}, - tasks: [], - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create an activity with tasks succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - tasks: [tasks1.id, task2.id], - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityCreated.tasks.length).toBe(2) - }) - - it('Should create an activity with an organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - organizationId: org1.id, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityCreated.organizationId).toEqual(org1.id) - }) - }) - - describe('findById method', () => { - it('Should successfully find created activity by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - const expectedActivityFound = { - id: activityCreated.id, - attributes: {}, - body: null, - title: null, - url: null, - channel: null, - sentiment: {}, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activity.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - const activityFound = await ActivityRepository.findById( - activityCreated.id, - mockIRepositoryOptions, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityFound.createdAt = activityFound.createdAt.toISOString().split('T')[0] - activityFound.updatedAt = activityFound.updatedAt.toISOString().split('T')[0] - delete activityFound.member - delete activityFound.objectMember - - expect(activityFound).toStrictEqual(expectedActivityFound) - }) - - it('Should throw 404 error when no user found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ActivityRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created activity entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const activity2Returned = await ActivityRepository.create( - { - type: 'activity-2', - timestamp: '2020-06-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity1Returned.id, activity2Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([activity1Returned.id, activity2Returned.id]) - }) - - it('Should only return the ids of previously created activities and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity3Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity3Returned.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([activity3Returned.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity4Returned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - const mockIRepositoryOptionsIr = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ActivityRepository.filterIdsInTenant( - [activity4Returned.id], - mockIRepositoryOptionsIr, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('Activities findOne method', () => { - it('Should return the created activity for a simple query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const found = await ActivityRepository.findOne({ type: 'activity' }, mockIRepositoryOptions) - - expect(found.id).toStrictEqual(activityReturned.id) - }) - - it('Should return the activity for a complex query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - thread: true, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const found = await ActivityRepository.findOne( - { 'attributes.thread': true }, - mockIRepositoryOptions, - ) - - expect(found.id).toStrictEqual(activityReturned.id) - }) - - it('Should return null when non-existent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - expect( - await ActivityRepository.findOne({ type: 'notype' }, mockIRepositoryOptions), - ).toBeNull() - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created activity - simple', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - type: 'activity-new', - platform: PlatformType.GITHUB, - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedActivity.updatedAt.getTime()).toBeGreaterThan( - updatedActivity.createdAt.getTime(), - ) - - updatedActivity.createdAt = updatedActivity.createdAt.toISOString().split('T')[0] - updatedActivity.updatedAt = updatedActivity.updatedAt.toISOString().split('T')[0] - delete updatedActivity.member - delete updatedActivity.objectMember - - const expectedActivityUpdated = { - id: activityReturned.id, - body: activityReturned.body, - channel: null, - title: null, - sentiment: {}, - url: null, - attributes: activityReturned.attributes, - type: 'activity-new', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - tasks: [], - parent: null, - parentId: null, - sourceId: activityReturned.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(updatedActivity).toStrictEqual(expectedActivityUpdated) - }) - - it('Should succesfully update previously created activity - with member relation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const memberCreated2 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test2', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - type: 'activity-new', - platform: PlatformType.GITHUB, - body: 'There', - title: 'Title', - channel: 'Channel', - url: 'https://www.google.com', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test2', - member: memberCreated2.id, - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedActivity.updatedAt.getTime()).toBeGreaterThan( - updatedActivity.createdAt.getTime(), - ) - - updatedActivity.createdAt = updatedActivity.createdAt.toISOString().split('T')[0] - updatedActivity.updatedAt = updatedActivity.updatedAt.toISOString().split('T')[0] - delete updatedActivity.member - delete updatedActivity.objectMember - - const expectedActivityUpdated = { - id: activityReturned.id, - attributes: activityReturned.attributes, - body: updateFields.body, - channel: updateFields.channel, - title: updateFields.title, - sentiment: updateFields.sentiment, - url: updateFields.url, - type: 'activity-new', - timestamp: new Date('2020-05-27T15:13:30Z'), - tasks: [], - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test2', - objectMemberUsername: null, - memberId: memberCreated2.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parent: null, - parentId: null, - sourceId: activityReturned.sourceId, - sourceParentId: null, - conversationId: null, - display: UNKNOWN_ACTIVITY_TYPE_DISPLAY, - organizationId: null, - organization: null, - } - - expect(updatedActivity).toStrictEqual(expectedActivityUpdated) - }) - - it('Should succesfully update tasks of an activity', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - tasks: [tasks1.id, task2.id], - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.tasks).toHaveLength(2) - expect(updatedActivity.tasks[0].id).toBe(tasks1.id) - expect(updatedActivity.tasks[1].id).toBe(task2.id) - }) - - it('Should update body and title with allowed HTML tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - body: '

This is some HTML

', - title: '

This is some Title HTML

', - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.body).toBe('

This is some HTML

') - expect(updatedActivity.title).toBe('

This is some Title HTML

') - }) - - it('Should sanitize body and title from non-allowed HTML tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activityReturned = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const updateFields = { - body: "

Malicious

", - title: "

Malicious title

", - } - - const updatedActivity = await ActivityRepository.update( - activityReturned.id, - updateFields, - mockIRepositoryOptions, - ) - - expect(updatedActivity.body).toBe('

Malicious

') - expect(updatedActivity.title).toBe('

Malicious title

') - }) - - it('Should update an activity with an organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const org2 = await OrganizationRepository.create( - { - displayName: 'tesla', - }, - mockIRepositoryOptions, - ) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - organizationId: org1.id, - sourceId: '#sourceId1', - } - - const activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - const activityUpdated = await ActivityRepository.update( - activityCreated.id, - { organizationId: org2.id }, - mockIRepositoryOptions, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - expect(activityUpdated.organizationId).toEqual(org2.id) - }) - }) - - describe('filter tests', () => { - it('Positive sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { positiveSentimentRange: [0.6, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - // Filter by whether activities are positive or not - const filteredActivities2 = await ActivityRepository.findAndCountAll( - { filter: { sentimentLabel: 'positive' } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities2.count).toBe(1) - expect(filteredActivities2.rows[0].id).toBe(activityCreated1.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment.positive_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.positive).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.positive, - ) - }) - it('Negative sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - await ActivityRepository.create(activity1, mockIRepositoryOptions) - const activityCreated2 = await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { negativeSentimentRange: [0.5, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated2.id) - - // Filter by whether activities are positive or not - const filteredActivities2 = await ActivityRepository.findAndCountAll( - { filter: { sentimentLabel: 'negative' } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities2.count).toBe(1) - expect(filteredActivities2.rows[0].id).toBe(activityCreated2.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment.negative_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.negative).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.negative, - ) - }) - - it('Overall sentiment filter and sort', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberCreated = await MemberRepository.create( - { - username: { - github: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'test', - member: memberCreated.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by how positive activities are - const filteredActivities = await ActivityRepository.findAndCountAll( - { filter: { sentimentRange: [0.6, 1] } }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - // No filter, but sorting - const filteredActivities3 = await ActivityRepository.findAndCountAll( - { filter: {}, orderBy: 'sentiment_DESC' }, - mockIRepositoryOptions, - ) - expect(filteredActivities3.count).toBe(2) - expect(filteredActivities3.rows[0].sentiment.positive).toBeGreaterThan( - filteredActivities3.rows[1].sentiment.positive, - ) - }) - - it('Member related attributes filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(DEFAULT_MEMBER_ATTRIBUTES) - - const memberAttributeSettings = ( - await MemberAttributeSettingsRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows - - const memberCreated1 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Anil', - attributes: { - [MemberAttributeName.IS_TEAM_MEMBER]: { - default: true, - [PlatformType.CROWD]: true, - }, - [MemberAttributeName.LOCATION]: { - default: 'Berlin', - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.SLACK]: 'Turkey', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const memberCreated2 = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'Michael', - }, - displayName: 'Michael', - attributes: { - [MemberAttributeName.IS_TEAM_MEMBER]: { - default: false, - [PlatformType.CROWD]: false, - }, - [MemberAttributeName.LOCATION]: { - default: 'Scranton', - [PlatformType.GITHUB]: 'Scranton', - [PlatformType.SLACK]: 'New York', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const activity1 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - username: 'test', - member: memberCreated1.id, - sourceId: '#sourceId1', - } - - const activity2 = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'neutral', - sentiment: 0.55, - }, - username: 'Michael', - member: memberCreated2.id, - sourceId: '#sourceId2', - } - - const activityCreated1 = await ActivityRepository.create(activity1, mockIRepositoryOptions) - const activityCreated2 = await ActivityRepository.create(activity2, mockIRepositoryOptions) - - // Control - expect( - (await ActivityRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions)).count, - ).toBe(2) - - // Filter by member.isTeamMember - let filteredActivities = await ActivityRepository.findAndCountAll( - { - advancedFilter: { - member: { - isTeamMember: { - not: false, - }, - }, - }, - attributesSettings: memberAttributeSettings, - }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated1.id) - - filteredActivities = await ActivityRepository.findAndCountAll( - { - advancedFilter: { - member: { - 'attributes.location.slack': 'New York', - }, - }, - attributesSettings: memberAttributeSettings, - }, - mockIRepositoryOptions, - ) - - expect(filteredActivities.count).toBe(1) - expect(filteredActivities.rows[0].id).toBe(activityCreated2.id) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/conversationRepository.test.ts b/backend/src/database/repositories/__tests__/conversationRepository.test.ts deleted file mode 100644 index a87dde152e..0000000000 --- a/backend/src/database/repositories/__tests__/conversationRepository.test.ts +++ /dev/null @@ -1,715 +0,0 @@ -import moment from 'moment' -import { Error404 } from '@crowd/common' -import ConversationRepository from '../conversationRepository' -import ActivityRepository from '../activityRepository' -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import { populateSegments } from '../../utils/segmentTestUtils' -import { UNKNOWN_ACTIVITY_TYPE_DISPLAY } from '@crowd/integrations' - -const db = null - -describe('ConversationRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a conversation succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug' } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - conversationCreated.createdAt = conversationCreated.createdAt.toISOString().split('T')[0] - conversationCreated.updatedAt = conversationCreated.updatedAt.toISOString().split('T')[0] - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: false, - activities: [], - activityCount: 0, - channel: null, - platform: null, - lastActive: null, - conversationStarter: null, - memberCount: 0, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationCreated).toStrictEqual(conversationExpected) - }) - - it('Should create a conversation succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug', published: true } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - conversationCreated.createdAt = conversationCreated.createdAt.toISOString().split('T')[0] - conversationCreated.updatedAt = conversationCreated.updatedAt.toISOString().split('T')[0] - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: conversation2Add.published, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - platform: null, - channel: null, - lastActive: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationCreated).toStrictEqual(conversationExpected) - }) - - it('Should throw not null constraint error if no slug is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ title: 'some-title' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw not null constraint error if no title is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ slug: 'some-slug' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw validation error if title is empty', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ title: '' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw validation error if slug is empty', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(() => - ConversationRepository.create({ slug: '' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created conversation by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation2Add = { title: 'some-title', slug: 'some-slug' } - - const conversationCreated = await ConversationRepository.create( - conversation2Add, - mockIRepositoryOptions, - ) - - const conversationExpected = { - id: conversationCreated.id, - title: conversation2Add.title, - slug: conversation2Add.slug, - published: false, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - platform: null, - channel: null, - lastActive: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const conversationById = await ConversationRepository.findById( - conversationCreated.id, - mockIRepositoryOptions, - ) - - conversationById.createdAt = conversationById.createdAt.toISOString().split('T')[0] - conversationById.updatedAt = conversationById.updatedAt.toISOString().split('T')[0] - - expect(conversationById).toStrictEqual(conversationExpected) - }) - - it('Should throw 404 error when no conversation found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ConversationRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created conversation entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversation1Created = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - const conversation2Created = await ConversationRepository.create( - { title: 'some-title-2', slug: 'some-slug-2' }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversation1Created.id, conversation2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([conversation1Created.id, conversation2Created.id]) - }) - - it('Should only return the ids of previously created conversations and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversationCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([conversationCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'some-title-1', slug: 'some-slug-1' }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ConversationRepository.filterIdsInTenant( - [conversationCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all conversations, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const memberCreated = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - platform: PlatformType.SLACK, - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - let conversation1Created = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - const activity1Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Some Parent Activity', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - conversationId: conversation1Created.id, - score: 1, - sourceId: '#sourceId1', - }, - mockIRepositoryOptions, - ) - - const activity2Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-28T15:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Here', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - score: 1, - parent: activity1Created.id, - conversationId: conversation1Created.id, - sourceId: '#sourceId2', - }, - mockIRepositoryOptions, - ) - - const activity3Created = await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-29T16:13:30Z', - platform: PlatformType.SLACK, - attributes: { - replies: 12, - }, - body: 'Here', - channel: 'general', - isContribution: true, - member: memberCreated.id, - username: 'test', - score: 1, - parent: activity1Created.id, - conversationId: conversation1Created.id, - sourceId: '#sourceId3', - }, - mockIRepositoryOptions, - ) - - let conversation2Created = await ConversationRepository.create( - { title: 'a cool title 2', slug: 'a-cool-title-2' }, - mockIRepositoryOptions, - ) - - const activity4Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-02T15:13:30Z', - platform: PlatformType.DISCORD, - url: 'https://parent-id-url.com', - body: 'conversation activity 1', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation2Created.id, - sourceId: '#sourceId4', - }, - mockIRepositoryOptions, - ) - - const activity5Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-03T15:13:30Z', - platform: PlatformType.DISCORD, - body: 'conversation activity 2', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation2Created.id, - sourceId: '#sourceId5', - }, - mockIRepositoryOptions, - ) - let conversation3Created = await ConversationRepository.create( - { title: 'some other title', slug: 'some-other-title', published: true }, - mockIRepositoryOptions, - ) - - const activity6Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-05T15:13:30Z', - platform: PlatformType.SLACK, - url: 'https://parent-id-url.com', - body: 'conversation activity 1', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation3Created.id, - sourceId: '#sourceId6', - }, - mockIRepositoryOptions, - ) - - const activity7Created = await ActivityRepository.create( - { - type: 'message', - timestamp: '2020-06-07T15:13:30Z', - platform: PlatformType.SLACK, - url: 'https://parent-id-url.com', - body: 'conversation activity 7', - channel: 'Some-Channel', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation3Created.id, - sourceId: '#sourceId7', - }, - mockIRepositoryOptions, - ) - - // activities are not included in findandcountall - conversation1Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation1Created.id, mockIRepositoryOptions), - 'activities', - ) - conversation2Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation2Created.id, mockIRepositoryOptions), - 'activities', - ) - conversation3Created = SequelizeTestUtils.objectWithoutKey( - await ConversationRepository.findById(conversation3Created.id, mockIRepositoryOptions), - 'activities', - ) - - // filter by id - let conversations = await ConversationRepository.findAndCountAll( - { filter: { id: conversation1Created.id }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - - const memberReturnedWithinConversations = SequelizeTestUtils.objectWithoutKey(memberCreated, [ - 'activities', - 'activityCount', - 'averageSentiment', - 'lastActive', - 'lastActivity', - 'activityTypes', - 'noMerge', - 'notes', - 'organizations', - 'tags', - 'tasks', - 'toMerge', - 'activeOn', - 'identities', - 'activeDaysCount', - 'username', - 'numberOfOpenSourceContributions', - 'segments', - 'affiliations', - ]) - - const conversation1Expected = { - ...conversation1Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity1Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity2Created, ['tasks']), - parent: SequelizeTestUtils.objectWithoutKey(activity2Created.parent, ['display']), - member: memberReturnedWithinConversations, - }, - { - ...SequelizeTestUtils.objectWithoutKey(activity3Created, ['tasks']), - parent: SequelizeTestUtils.objectWithoutKey(activity3Created.parent, ['display']), - member: memberReturnedWithinConversations, - }, - ], - } - - const conversation2Expected = { - ...conversation2Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity4Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity5Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - ], - } - - const conversation3Expected = { - ...conversation3Created, - conversationStarter: { - ...SequelizeTestUtils.objectWithoutKey(activity6Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - lastReplies: [ - { - ...SequelizeTestUtils.objectWithoutKey(activity7Created, ['tasks']), - member: memberReturnedWithinConversations, - }, - ], - } - - expect(conversations.rows).toStrictEqual([conversation1Expected]) - - // filter by title - conversations = await ConversationRepository.findAndCountAll( - { filter: { title: 'a cool title' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation2Expected, conversation1Expected]) - - // filter by slug - conversations = await ConversationRepository.findAndCountAll( - { filter: { slug: 'a-cool-title-2' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // filter by published - conversations = await ConversationRepository.findAndCountAll( - { filter: { published: true }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation3Expected]) - - // filter by activityCount only start input - conversations = await ConversationRepository.findAndCountAll( - { filter: { activityCountRange: [2] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - expect(conversations.count).toEqual(3) - expect(conversations.rows).toStrictEqual([ - conversation3Expected, - conversation2Expected, - conversation1Expected, - ]) - - // filter by activityCount start and end inputs - conversations = await ConversationRepository.findAndCountAll( - { filter: { activityCountRange: [0, 1] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - expect(conversations.count).toEqual(0) - expect(conversations.rows).toStrictEqual([]) - - // filter by platform - conversations = await ConversationRepository.findAndCountAll( - { filter: { platform: PlatformType.DISCORD }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // filter by channel (channel) - conversations = await ConversationRepository.findAndCountAll( - { filter: { channel: 'Some-Channel' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation3Expected, conversation2Expected]) - - // filter by channel (repo) - conversations = await ConversationRepository.findAndCountAll( - { filter: { channel: 'general' }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation1Expected]) - - // filter by lastActive only start - conversations = await ConversationRepository.findAndCountAll( - { filter: { lastActiveRange: ['2020-06-03T15:13:30Z'] }, lazyLoad: ['activities'] }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(2) - expect(conversations.rows).toStrictEqual([conversation3Expected, conversation2Expected]) - - // filter by lastActive start and end - conversations = await ConversationRepository.findAndCountAll( - { - filter: { lastActiveRange: ['2020-06-03T15:13:30Z', '2020-06-04T15:13:30Z'] }, - lazyLoad: ['activities'], - }, - mockIRepositoryOptions, - ) - - expect(conversations.count).toEqual(1) - expect(conversations.rows).toStrictEqual([conversation2Expected]) - - // Test orderBy - conversations = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - lazyLoad: ['activities'], - }, - mockIRepositoryOptions, - ) - expect(moment(conversations.rows[0].lastActive).unix()).toBeGreaterThan( - moment(conversations.rows[1].lastActive).unix(), - ) - expect(moment(conversations.rows[1].lastActive).unix()).toBeGreaterThan( - moment(conversations.rows[2].lastActive).unix(), - ) - - // Test pagination - const conversationsP1 = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - limit: 2, - offset: 0, - }, - mockIRepositoryOptions, - ) - expect(conversationsP1.rows.length).toEqual(2) - expect(conversationsP1.count).toEqual(3) - - const conversationsP2 = await ConversationRepository.findAndCountAll( - { - filter: {}, - orderBy: 'lastActive_DESC', - limit: 2, - offset: 2, - }, - mockIRepositoryOptions, - ) - expect(conversationsP2.rows.length).toEqual(1) - expect(conversationsP2.count).toEqual(3) - expect(conversationsP2.rows[0].id).not.toBe(conversationsP1.rows[0].id) - expect(conversationsP2.rows[0].id).not.toBe(conversationsP1.rows[1].id) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - const conversationUpdated = await ConversationRepository.update( - conversationCreated.id, - { - published: true, - slug: 'some-other-slug', - }, - mockIRepositoryOptions, - ) - - expect(conversationUpdated.updatedAt.getTime()).toBeGreaterThan( - conversationUpdated.createdAt.getTime(), - ) - - const conversationExpected = { - id: conversationCreated.id, - title: conversationCreated.title, - slug: conversationUpdated.slug, - published: conversationUpdated.published, - activities: [], - activityCount: 0, - memberCount: 0, - conversationStarter: null, - channel: null, - lastActive: null, - platform: null, - createdAt: conversationCreated.createdAt, - updatedAt: conversationUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(conversationUpdated).toStrictEqual(conversationExpected) - }) - it('Should throw 404 error when trying to update non existent conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ConversationRepository.update(randomUUID(), { slug: 'some-slug' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const conversationCreated = await ConversationRepository.create( - { title: 'a cool title', slug: 'a-cool-title' }, - mockIRepositoryOptions, - ) - - await ConversationRepository.destroy(conversationCreated.id, mockIRepositoryOptions) - - // Try selecting it after destroy, should throw 404 - await expect(() => - ConversationRepository.findById(conversationCreated.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts b/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts deleted file mode 100644 index d6d2e45b6d..0000000000 --- a/backend/src/database/repositories/__tests__/eagleEyeActionRepository.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { EagleEyeAction, EagleEyeActionType, EagleEyeContent } from '@crowd/types' -import EagleEyeContentRepository from '../eagleEyeContentRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import EagleEyeActionRepository from '../eagleEyeActionRepository' - -const db = null - -describe('eagleEyeActionRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('createActionForContent method', () => { - it('Should create a an action for a content succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } as EagleEyeContent - - const contentCreated = await EagleEyeContentRepository.create(content, mockIRepositoryOptions) - - const action: EagleEyeAction = { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-07-27T19:13:30Z', - } - - const actionCreated = await EagleEyeActionRepository.createActionForContent( - action, - contentCreated.id, - mockIRepositoryOptions, - ) - - actionCreated.createdAt = (actionCreated.createdAt as Date).toISOString().split('T')[0] - actionCreated.updatedAt = (actionCreated.updatedAt as Date).toISOString().split('T')[0] - - const expectedAction = { - id: actionCreated.id, - ...action, - timestamp: new Date(actionCreated.timestamp), - contentId: contentCreated.id, - actionById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - } - expect(expectedAction).toStrictEqual(actionCreated) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts b/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts deleted file mode 100644 index 65b1879733..0000000000 --- a/backend/src/database/repositories/__tests__/eagleEyeContentRepository.test.ts +++ /dev/null @@ -1,552 +0,0 @@ -import { EagleEyeActionType, EagleEyeContent } from '@crowd/types' -import EagleEyeContentRepository from '../eagleEyeContentRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import EagleEyeActionRepository from '../eagleEyeActionRepository' - -const db = null - -describe('eagleEyeContentRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a content succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } as EagleEyeContent - - const created = await EagleEyeContentRepository.create(content, mockIRepositoryOptions) - - created.createdAt = (created.createdAt as Date).toISOString().split('T')[0] - created.updatedAt = (created.updatedAt as Date).toISOString().split('T')[0] - - const expectedCreated = { - id: created.id, - ...content, - postedAt: new Date(content.postedAt), - actions: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - } - expect(created).toStrictEqual(expectedCreated) - }) - - /* - - it('Should create a content with unix timestamp', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const withUnix = { - sourceId: 'sourceId', - vectorId: '123', - status: null, - platform: 'hacker_news', - title: 'title', - postAttributes: { - score: 10, - }, - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - url: 'url', - timestamp: 1660712134, - - username: 'username', - keywords: ['keyword1', 'keyword2'], - similarityScore: 0.9, - } - - const created = await EagleEyeContentRepository.upsert(withUnix, mockIRepositoryOptions) - - created.createdAt = created.createdAt.toISOString().split('T')[0] - created.updatedAt = created.updatedAt.toISOString().split('T')[0] - - const expectedCreated = { - id: created.id, - ...toCreate, - timestamp: new Date(1660712134 * 1000), - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(created).toStrictEqual(expectedCreated) - }) - - - - - }) - - describe('find by id method', () => { - it('Should find an existing record', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const found = await EagleEyeContentRepository.findById(id, mockIRepositoryOptions) - expect(found.id).toBe(id) - }) - - it('Should throw 404 error when no tag found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - EagleEyeContentRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('find and count all method', () => { - it('Should find all records without filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { filter: {} }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(5) - }) - - it('Filter by date', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - timestampRange: [moment().subtract(1, 'day').toISOString()], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by nDays', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - nDays: 1, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by status NULL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'NULL', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by status NOT_NULL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'NOT_NULL', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(2) - }) - - it('Filter by status engaged', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'engaged', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - }) - - it('Filter by status rejected', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - status: 'rejected', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - }) - - it('Filter by platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - platforms: 'hacker_news', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(2) - }) - - it('Filter by several platforms', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - await new EagleEyeContentService(mockIRepositoryOptions).upsert({ - sourceId: 't1', - vectorId: 't1', - url: 'url devto 3', - username: 'devtousername3', - status: null, - platform: 'twitter', - timestamp: moment().subtract(1, 'week').toDate(), - keywords: ['keyword3', 'keyword2'], - title: 'title devto 3', - }) - - const found = await EagleEyeContentRepository.findAndCountAll( - { - filter: { - platforms: 'hacker_news,twitter', - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(3) - }) - - it('Filter by timestamp and status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - expect( - ( - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - timestampRange: [moment().subtract(1, 'day').toISOString()], - status: 'NULL', - }, - }, - mockIRepositoryOptions, - ) - ).count, - ).toBe(2) - }) - - it('Filter by keywords', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await addAll(mockIRepositoryOptions) - - const k1 = { - sourceId: 'sourceIdk1', - vectorId: 'sourceIdk1', - status: null, - platform: 'hacker_news', - title: 'title', - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - postAttributes: { - score: 10, - }, - url: 'url', - timestamp: new Date(), - username: 'username', - keywords: ['keyword1'], - similarityScore: 0.9, - } - - await new EagleEyeContentService(mockIRepositoryOptions).upsert(k1) - - const k2 = { - sourceId: 'sourceIdk2', - vectorId: 'sourceIdk2', - status: null, - platform: 'hacker_news', - title: 'title', - userAttributes: { [PlatformType.GITHUB]: 'hey', [PlatformType.TWITTER]: 'ho' }, - text: 'text', - postAttributes: { - score: 10, - }, - url: 'url', - timestamp: new Date(), - username: 'username', - keywords: ['keyword2'], - similarityScore: 0.9, - } - - try { - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - keywords: 'keyword1,keyword2', - }, - }, - mockIRepositoryOptions, - ) - } catch (e) { - console.log(e) - } - - await new EagleEyeContentService(mockIRepositoryOptions).upsert(k2) - - expect( - ( - await EagleEyeContentRepository.findAndCountAll( - { - filter: { - keywords: 'keyword1,keyword2', - }, - }, - mockIRepositoryOptions, - ) - ).count, - ).toBe(5) - }) - }) - - describe('update method', () => { - it('Should update a record', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const updated = await EagleEyeContentRepository.update( - id, - { status: 'rejected', username: 'updated' }, - mockIRepositoryOptions, - ) - expect(updated.id).toBe(id) - expect(updated.status).toBe('rejected') - expect(updated.username).toBe('updated') - }) - - it('Should throw 404 error when no content found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - EagleEyeContentRepository.update(randomUUID(), {}, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw an error for an invalid status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - - await expect(() => - EagleEyeContentRepository.update(id, { status: 'smth' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error400('en', 'errors.invalidEagleEyeStatus.message')) - }) - - it('Keywords should not be updated', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const created = await EagleEyeContentRepository.upsert(toCreate, mockIRepositoryOptions) - - const id = created.id - const updated = await EagleEyeContentRepository.update( - id, - { keywords: ['1', '2'] }, - mockIRepositoryOptions, - ) - expect(updated.id).toBe(id) - expect(updated.keywords).toStrictEqual(created.keywords) - }) - }) - */ - }) - - describe('findAndCountAll method', () => { - it('Should find eagle eye contant, various cases', async () => { - // create random tenant with one user - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // create additional users for same tenant to test out actionBy filtering - const randomUser = await SequelizeTestUtils.getRandomUser() - - console.log('random user: ') - console.log(randomUser) - - const user2 = await mockIRepositoryOptions.database.user.create(randomUser) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: ['admin'], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - // create few content - // one without any actions - await EagleEyeContentRepository.create( - { - platform: 'reddit', - url: 'https://some-reddit-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // one with a bookmark action - let c2 = await EagleEyeContentRepository.create( - { - platform: 'hackernews', - url: 'https://some-hackernews-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2022-06-27T19:14:44Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // add bookmark action - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-07-27T19:13:30Z', - }, - c2.id, - mockIRepositoryOptions, - ) - - c2 = await EagleEyeContentRepository.findById(c2.id, mockIRepositoryOptions) - - // another content with a thumbs-up(user1) and a bookmark(user2) action - let c3 = await EagleEyeContentRepository.create( - { - platform: 'devto', - url: 'https://some-devto-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2022-06-27T19:14:44Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - mockIRepositoryOptions, - ) - - // add the thumbs up action - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.THUMBS_UP, - timestamp: '2022-09-30T23:11:10Z', - }, - c3.id, - mockIRepositoryOptions, - ) - - // also add bookmark from user2 - await EagleEyeActionRepository.createActionForContent( - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-09-30T23:11:10Z', - }, - c3.id, - { ...mockIRepositoryOptions, currentUser: user2 }, - ) - - c3 = await EagleEyeContentRepository.findById(c3.id, mockIRepositoryOptions) - - // filter by action type - let res = await EagleEyeContentRepository.findAndCountAll( - { - advancedFilter: { - action: { - type: EagleEyeActionType.BOOKMARK, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(res.count).toBe(2) - expect(res.rows.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))).toStrictEqual([c2, c3]) - - // filter by actionBy - res = await EagleEyeContentRepository.findAndCountAll( - { - advancedFilter: { - action: { - actionById: user2.id, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(res.count).toBe(1) - expect(res.rows).toStrictEqual([c3]) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/integrationRepository.test.ts b/backend/src/database/repositories/__tests__/integrationRepository.test.ts deleted file mode 100644 index c9e03e86d6..0000000000 --- a/backend/src/database/repositories/__tests__/integrationRepository.test.ts +++ /dev/null @@ -1,108 +0,0 @@ -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import IntegrationRepository from '../integrationRepository' -import { PlatformType } from '@crowd/types' - -const db = null - -describe('Integration repository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('Find all active integrations', () => { - it('Should find a single active integration', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const found: any = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found[0].tenantId).toBeDefined() - expect(found.length).toBe(1) - }) - - it('Should find all active integrations for a platform', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find active integrations', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - - const int2 = { - status: 'todo', - platform: PlatformType.TWITTER, - } - - const int3 = { - status: 'in-progress', - platform: PlatformType.TWITTER, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int2, mockIRepositoryOptions3) - - const mockIRepositoryOptions4 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int3, mockIRepositoryOptions4) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find integrations for the desired platform', async () => { - const int1 = { - status: 'done', - platform: PlatformType.TWITTER, - } - - const int2 = { - status: 'active', - platform: PlatformType.DISCORD, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await IntegrationRepository.create(int2, mockIRepositoryOptions3) - - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(2) - }) - - it('Should return an empty list if no integrations are found', async () => { - const found = await IntegrationRepository.findAllActive(PlatformType.TWITTER, 1, 100) - expect(found.length).toBe(0) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts b/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts deleted file mode 100644 index 9a32e4bf58..0000000000 --- a/backend/src/database/repositories/__tests__/memberAttributeSettingsRepository.test.ts +++ /dev/null @@ -1,394 +0,0 @@ -import { Error400, Error404 } from '@crowd/common' -import { MemberAttributeType } from '@crowd/types' -import MemberAttributeSettingsRepository from '../memberAttributeSettingsRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -describe('MemberAttributeSettings tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create settings for a member attribute succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - attributeCreated.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeCreated.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - options: [], - show: true, - canDelete: true, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should create settings for a member attribute succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - canDelete: false, - show: false, - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - attributeCreated.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeCreated.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - show: false, - options: [], - canDelete: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should throw unique constraint error for creation of already existing member attributes with same name in the same tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - await MemberAttributeSettingsRepository.create(attribute, mockIRepositoryOptions) - - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'some label', name: 'attribute1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - }) - - it('Should throw not null error if no name, label or type is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no type - await expect(() => - MemberAttributeSettingsRepository.create( - { type: undefined, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // no label - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, name: 'attribute1', label: undefined }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - - // no name - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1' }, - mockIRepositoryOptions, - ), - ).rejects.toThrow() - }) - - it('Should throw 400 error if name exists in member fixed fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no type - await expect(() => - MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'Some Email', name: 'emails' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.reservedField', 'emails'), - ) - }) - }) - - describe('findById method', () => { - it('Should successfully find created member attribute by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = { - type: MemberAttributeType.BOOLEAN, - label: 'attribute 1', - name: 'attribute1', - } - - const attributeCreated = await MemberAttributeSettingsRepository.create( - attribute, - mockIRepositoryOptions, - ) - - const attributeExpected = { - id: attributeCreated.id, - type: attribute.type, - label: attribute.label, - name: attribute.name, - show: true, - canDelete: true, - options: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const attributeById = await MemberAttributeSettingsRepository.findById( - attributeCreated.id, - mockIRepositoryOptions, - ) - - attributeById.createdAt = (attributeCreated.createdAt as any).toISOString().split('T')[0] - attributeById.updatedAt = (attributeCreated.updatedAt as any).toISOString().split('T')[0] - - expect(attributeById).toStrictEqual(attributeExpected) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all member attributes, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute1 = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'a label', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - const attribute2 = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.STRING, label: 'a label', name: 'attribute2', show: false }, - mockIRepositoryOptions, - ) - - const attribute3 = await MemberAttributeSettingsRepository.create( - { - type: MemberAttributeType.STRING, - label: 'some other label', - name: 'attribute3', - show: false, - canDelete: false, - }, - mockIRepositoryOptions, - ) - - // filter by type - let attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { type: MemberAttributeType.BOOLEAN } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute1]) - - // filter by id - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { id: attribute2.id } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute2]) - - // filter by label - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { label: 'a label' } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by name - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { name: 'attribute3' } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute3]) - - // filter by show - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { show: false } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute3, attribute2]) - - // filter by canDelete - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { filter: { canDelete: true } }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by createdAt between createdAt a1 and a3 - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [attribute1.createdAt, attribute3.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(attributes.count).toEqual(3) - expect(attributes.rows).toStrictEqual([attribute3, attribute2, attribute1]) - - // filter by createdAt <= att2.createdAt - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, attribute2.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(attributes.count).toEqual(2) - expect(attributes.rows).toStrictEqual([attribute2, attribute1]) - - // filter by createdAt <= att1.createdAt - attributes = await MemberAttributeSettingsRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, attribute1.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(attributes.count).toEqual(1) - expect(attributes.rows).toStrictEqual([attribute1]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - const attributeUpdated = await MemberAttributeSettingsRepository.update( - attribute.id, - { - type: MemberAttributeType.STRING, - label: 'some other label', - name: 'some name', - show: false, - canDelete: false, - }, - mockIRepositoryOptions, - ) - - const attributeExpected = { - id: attribute.id, - type: attributeUpdated.type, - label: attributeUpdated.label, - name: attributeUpdated.name, - show: attributeUpdated.show, - canDelete: attributeUpdated.canDelete, - options: [], - createdAt: attribute.createdAt, - updatedAt: attributeUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(attributeUpdated).toStrictEqual(attributeExpected) - }) - - it('Should throw 404 error when trying to update non existent attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberAttributeSettingsRepository.update( - randomUUID(), - { type: 'some-type' } as any, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created attribute', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const attribute = await MemberAttributeSettingsRepository.create( - { type: MemberAttributeType.BOOLEAN, label: 'attribute 1', name: 'attribute1' }, - mockIRepositoryOptions, - ) - - await MemberAttributeSettingsRepository.destroy(attribute.id, mockIRepositoryOptions) - - await expect(() => - MemberAttributeSettingsRepository.findById(attribute.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberAttributeSettingsRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts b/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts deleted file mode 100644 index 8b2438a340..0000000000 --- a/backend/src/database/repositories/__tests__/memberEnrichmentCacheRepository.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { randomUUID } from 'crypto' - -import MemberRepository from '../memberRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { PlatformType } from '@crowd/types' -import MemberEnrichmentCacheRepository from '../memberEnrichmentCacheRepository' -import { generateUUIDv1 } from '@crowd/common' - -const db = null - -describe('MemberEnrichmentCacheRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('upsert method', () => { - it('Should create non existing item successfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - const cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(enrichmentData) - }) - - it('Should update the data of existing cache item with incoming data', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - let cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - const newerEnrichmentData = { - enrichmentField1: 'anotherString', - enrichmentField2: 99, - arrayEnrichmentField: ['a', 'b', 'c'], - } - - // should overwrite with new cache data - cache = await MemberEnrichmentCacheRepository.upsert( - member.id, - newerEnrichmentData, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(newerEnrichmentData) - - // when we send an empty object, it shouldn't overwrite - cache = await MemberEnrichmentCacheRepository.upsert(member.id, {}, mockIRepositoryOptions) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toStrictEqual(newerEnrichmentData) - }) - }) - - describe('findByMemberId method', () => { - it('Should find enrichment cache by memberId', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'michael_scott', - }, - }, - displayName: 'Member 1', - email: 'michael@dd.com', - score: 10, - attributes: {}, - joinedAt: '2020-05-27T15:13:30Z', - } - - const member = await MemberRepository.create(member2add, mockIRepositoryOptions) - - const enrichmentData = { - enrichmentField1: 'string', - enrichmentField2: 24, - arrayEnrichmentField: [1, 2, 3], - } - - await MemberEnrichmentCacheRepository.upsert( - member.id, - enrichmentData, - mockIRepositoryOptions, - ) - - const cache = await MemberEnrichmentCacheRepository.findByMemberId( - member.id, - mockIRepositoryOptions, - ) - - expect(cache.memberId).toEqual(member.id) - expect(cache.data).toEqual(enrichmentData) - }) - - it('Should return null for non-existing cache entry', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const cache = await MemberEnrichmentCacheRepository.findByMemberId( - randomUUID(), - mockIRepositoryOptions, - ) - expect(cache).toBeNull() - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/memberRepository.test.ts b/backend/src/database/repositories/__tests__/memberRepository.test.ts deleted file mode 100644 index 13945324b8..0000000000 --- a/backend/src/database/repositories/__tests__/memberRepository.test.ts +++ /dev/null @@ -1,3959 +0,0 @@ -import { v4 as uuid } from 'uuid' -import moment from 'moment' - -import { Error404 } from '@crowd/common' -import { PlatformType, SegmentStatus } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import MemberRepository from '../memberRepository' -import NoteRepository from '../noteRepository' -import OrganizationRepository from '../organizationRepository' -import TagRepository from '../tagRepository' -import TaskRepository from '../taskRepository' -import lodash from 'lodash' -import SegmentRepository from '../segmentRepository' -import { populateSegments } from '../../utils/segmentTestUtils' -import MemberService from '../../../services/memberService' -import OrganizationService from '../../../services/organizationService' - -const db = null - -function mapUsername(data: any): any { - const username = {} - Object.keys(data).forEach((platform) => { - const usernameData = data[platform] - - if (Array.isArray(usernameData)) { - username[platform] = [] - if (usernameData.length > 0) { - for (const entry of usernameData) { - if (typeof entry === 'string') { - username[platform].push(entry) - } else if (typeof entry === 'object') { - username[platform].push((entry as any).username) - } else { - throw new Error('Invalid username type') - } - } - } - } else if (typeof usernameData === 'object') { - username[platform] = [usernameData.username] - } else if (typeof usernameData === 'string') { - username[platform] = [usernameData] - } else { - throw new Error('Invalid username type') - } - }) - return username -} - -describe('MemberRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given member succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - attributes: member2add.attributes, - displayName: member2add.displayName, - emails: member2add.emails, - score: member2add.score, - identities: ['github'], - lastEnriched: null, - enrichedBy: [], - contributions: null, - organizations: [], - notes: [], - tasks: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - joinedAt: new Date('2020-05-27T15:13:30Z'), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - lastActive: null, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should create succesfully but return without relations when doPopulateRelations=false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions, false) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - attributes: member2add.attributes, - emails: member2add.emails, - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: member2add.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - organizations: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should succesfully create member with only mandatory username and joinedAt fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - organizations: [], - attributes: {}, - identities: ['github'], - emails: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: -1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - joinedAt: new Date('2020-05-27T15:13:30Z'), - notes: [], - tasks: [], - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(expectedMemberCreated) - }) - - it('Should throw error when no username given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no username field, should reject the promise with - // sequelize unique constraint - const member2add = { - joinedAt: '2020-05-27T15:13:30Z', - emails: ['test@crowd.dev'], - } - - await expect(() => - MemberRepository.create(member2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw error when no joinedAt given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // no username field, should reject the promise with - // sequelize unique constraint - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - emails: ['test@crowd.dev'], - } - - await expect(() => - MemberRepository.create(member2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should succesfully create member with notes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const notes1 = await NoteRepository.create( - { - body: 'note1', - }, - mockIRepositoryOptions, - ) - - const notes2 = await NoteRepository.create( - { - body: 'note2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.SLACK]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - notes: [notes1.id, notes2.id], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.notes).toHaveLength(2) - expect(memberCreated.notes[0].id).toEqual(notes1.id) - expect(memberCreated.notes[1].id).toEqual(notes2.id) - }) - - it('Should succesfully create member with tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - tasks: [tasks1.id, task2.id], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.tasks).toHaveLength(2) - expect(memberCreated.tasks.find((t) => t.id === tasks1.id)).not.toBeUndefined() - expect(memberCreated.tasks.find((t) => t.id === task2.id)).not.toBeUndefined() - }) - - it('Should succesfully create member with organization affiliations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - affiliations: [ - { - segmentId: segment1.id, - organizationId: org1.id, - }, - { - segmentId: segment2.id, - organizationId: null, - }, - ], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.affiliations).toHaveLength(2) - expect( - memberCreated.affiliations.filter((a) => a.segmentId === segment1.id)[0].organizationId, - ).toEqual(org1.id) - expect( - memberCreated.affiliations.filter((a) => a.segmentId === segment2.id)[0].organizationId, - ).toBeNull() - }) - }) - - describe('findById method', () => { - it('Should successfully find created member by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const expectedMemberFound = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - identities: ['github'], - attributes: {}, - emails: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - score: -1, - importHash: null, - organizations: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - notes: [], - tasks: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - const memberById = await MemberRepository.findById(memberCreated.id, mockIRepositoryOptions) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberById.createdAt = memberById.createdAt.toISOString().split('T')[0] - memberById.updatedAt = memberById.updatedAt.toISOString().split('T')[0] - - expect(memberById).toStrictEqual(expectedMemberFound) - }) - - it('Should return a plain object when called with doPopulateRelations false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member2add = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const cloned = lodash.cloneDeep(member2add) - const memberCreated = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const expectedMemberFound = { - id: memberCreated.id, - username: mapUsername(member2add.username), - displayName: member2add.displayName, - lastEnriched: null, - enrichedBy: [], - contributions: null, - attributes: {}, - emails: [], - score: -1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - organizations: [], - joinedAt: new Date('2020-05-27T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - const memberById = await MemberRepository.findById( - memberCreated.id, - mockIRepositoryOptions, - true, - false, - ) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - memberById.createdAt = memberById.createdAt.toISOString().split('T')[0] - memberById.updatedAt = memberById.updatedAt.toISOString().split('T')[0] - - expect(memberById).toStrictEqual(expectedMemberFound) - }) - - it('Should throw 404 error when no member found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created member entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - const member2 = { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'some-other-name', - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - const member2Returned = await MemberRepository.create(member2, mockIRepositoryOptions) - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id, member2Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([member1Returned.id, member2Returned.id]) - }) - - it('Should only return the ids of previously created members and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-29T15:14:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([member1Returned.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-04-29T15:14:30Z', - } - - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await MemberRepository.filterIdsInTenant( - [member1Returned.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('memberExists method', () => { - it('Should return the created member for a simple query', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - - const found = await MemberRepository.memberExists( - 'test1', - PlatformType.TWITTER, - mockIRepositoryOptions, - ) - - expect(found).toStrictEqual(member1Returned) - }) - - it('Should a plain object when called with doPopulateRelations false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - const member1Returned = await MemberRepository.create(member1, mockIRepositoryOptions) - delete member1Returned.toMerge - delete member1Returned.noMerge - delete member1Returned.tags - delete member1Returned.activities - delete member1Returned.notes - delete member1Returned.tasks - delete member1Returned.lastActive - delete member1Returned.activityCount - delete member1Returned.averageSentiment - delete member1Returned.lastActivity - delete member1Returned.activeOn - delete member1Returned.identities - delete member1Returned.activityTypes - delete member1Returned.activeDaysCount - delete member1Returned.numberOfOpenSourceContributions - delete member1Returned.affiliations - delete member1Returned.manuallyCreated - member1Returned.segments = member1Returned.segments.map((s) => s.id) - - const found = await MemberRepository.memberExists( - 'test1', - PlatformType.TWITTER, - mockIRepositoryOptions, - false, - ) - - expect(found).toStrictEqual(member1Returned) - }) - - it('Should return null when non-existent at platform level', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - await MemberRepository.create(member1, mockIRepositoryOptions) - - await expect(() => - MemberRepository.memberExists('test1', PlatformType.GITHUB, mockIRepositoryOptions), - ) - }) - - it('Should return null when non-existent at username level', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - emails: ['joan@crowd.dev'], - } - await MemberRepository.create(member1, mockIRepositoryOptions) - - const memberExists = await MemberRepository.memberExists( - 'test2', - PlatformType.TWITTER, - mockIRepositoryOptions, - ) - - expect(memberExists).toBeNull() - }) - }) - - describe('findAndCountAll method', () => { - it('is successfully finding and counting all members, sortedBy activitiesCount DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: {}, orderBy: 'activityCount_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[2].activityCount).toEqual('1') - }) - - it('is successfully finding and counting all members, sortedBy numberOfOpenSourceContributions DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: {}, orderBy: 'numberOfOpenSourceContributions_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(3) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[2].numberOfOpenSourceContributions).toEqual(0) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte than 3 and less or equal to 6', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { numberOfOpenSourceContributionsRange: [3, 6] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 2', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { - filter: { numberOfOpenSourceContributionsRange: [2] }, - orderBy: 'numberOfOpenSourceContributions_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - }) - - it('is successfully finding and counting all members, and tags [nodejs, vuejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { tags: [nodeTag.id, vueTag.id] } }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.TWITTER][0] === 'test2') - expect(members.rows.length).toEqual(1) - expect(member2.tags[0].name).toEqual('nodejs') - expect(member2.tags[1].name).toEqual('vuejs') - }) - - it('is successfully finding and counting all members, and tags [nodejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { tags: [nodeTag.id] } }, - mockIRepositoryOptions, - ) - const member1 = members.rows.find((m) => m.username[PlatformType.GITHUB][0] === 'test1') - const member2 = members.rows.find((m) => m.username[PlatformType.GITHUB][0] === 'test2') - - expect(members.rows.length).toEqual(2) - expect(member1.tags[0].name).toEqual('nodejs') - expect(member1.tags[0].name).toEqual('nodejs') - expect(member2.tags[1].name).toEqual('vuejs') - }) - - it('is successfully finding and counting all members, and organisations [crowd.dev, pied piper]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const crowd = await mockIRepositoryOptions.database.organization.create({ - displayName: 'crowd.dev', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - await OrganizationRepository.addIdentity( - crowd.id, - { - name: 'crowd.dev', - url: 'https://crowd.dev', - platform: 'crowd', - }, - mockIRepositoryOptions, - ) - - const pp = await mockIRepositoryOptions.database.organization.create({ - displayName: 'pied piper', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await OrganizationRepository.addIdentity( - pp.id, - { - name: 'pied piper', - url: 'https://piedpiper.com', - platform: 'crowd', - }, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - organizations: [crowd.id, pp.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { organizations: [crowd.id, pp.id] } }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test2') - expect(members.rows.length).toEqual(1) - expect(member2.organizations[0].displayName).toEqual('crowd.dev') - expect(member2.organizations[1].displayName).toEqual('pied piper') - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 1 and less or equal to 6', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { scoreRange: [1, 6] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test1').score).toEqual( - 1, - ) - expect(members.rows.find((m) => m.username[PlatformType.SLACK][0] === 'test2').score).toEqual( - 6, - ) - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 7', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.DISCORD]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.DISCORD]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAll( - { filter: { scoreRange: [7] } }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - for (const member of members.rows) { - expect(member.score).toBeGreaterThanOrEqual(7) - } - }) - - it('is successfully find and counting members with various filters, computed attributes, and full options (filter, limit, offset and orderBy)', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - reach: { - total: 15, - }, - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - reach: { - total: 55, - }, - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - tags: [vueTag.id], - reach: { - total: 124, - }, - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-10'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'positive', - sentiment: 0.1, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-11'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-12'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - sentiment: { - positive: 0.94, - negative: 0.0, - neutral: 0.06, - mixed: 0.0, - label: 'positive', - sentiment: 0.94, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-13'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-14'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.41, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-15'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.18, - }, - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - let members = await MemberRepository.findAndCountAll( - { - filter: {}, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[0].lastActive.toISOString()).toEqual('2022-09-15T00:00:00.000Z') - - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[1].lastActive.toISOString()).toEqual('2022-09-12T00:00:00.000Z') - - expect(members.rows[2].activityCount).toEqual('1') - expect(members.rows[2].tags[0].name).toEqual('nodejs') - expect(members.rows[2].lastActive.toISOString()).toEqual('2022-09-10T00:00:00.000Z') - - expect(members.rows[1].tags.map((i) => i.name).sort()).toEqual(['nodejs', 'vuejs']) - expect(members.rows[0].tags[0].name).toEqual('vuejs') - - // filter and order by reach - members = await MemberRepository.findAndCountAll( - { - filter: { - reachRange: [55], - }, - limit: 15, - offset: 0, - orderBy: 'reach_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].id).toEqual(member3.id) - expect(members.rows[1].id).toEqual(member2.id) - - // filter and sort by activity count - members = await MemberRepository.findAndCountAll( - { - filter: { - activityCountRange: [2], - }, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by lastActive - members = await MemberRepository.findAndCountAll( - { - filter: { - lastActiveRange: ['2022-09-11'], - }, - limit: 15, - offset: 0, - orderBy: 'lastActive_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by averageSentiment (member1.avgSentiment = 0.1, member2.avgSentiment = 0.2, member3.avgSentiment = 0.34) - members = await MemberRepository.findAndCountAll( - { - filter: { - averageSentimentRange: [0.2], - }, - limit: 15, - offset: 0, - orderBy: 'averageSentiment_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member2.id, member3.id]) - }) - }) - - describe('findAndCountAllv2 method', () => { - it('is successfully finding and counting all members, sortedBy activitiesCount DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test1', - memberId: member1.id, - sourceId: '#sourceId1', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test2', - memberId: member2.id, - sourceId: '#sourceId2', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test2', - memberId: member2.id, - sourceId: '#sourceId3', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId4', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId5', - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date(), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - username: 'test3', - memberId: member3.id, - sourceId: '#sourceId6', - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { filter: {}, orderBy: 'activityCount_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[2].activityCount).toEqual('1') - }) - - it('is successfully finding and counting all members, sortedBy numberOfOpenSourceContributions DESC', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { filter: {}, orderBy: 'numberOfOpenSourceContributions_DESC' }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(3) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(3) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[2].numberOfOpenSourceContributions).toEqual(0) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 3', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - numberOfOpenSourceContributions: { - gte: 3, - }, - }, - ], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, numberOfOpenSourceContributions range gte 2 and sort by asc', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test1' }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test2' }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { [PlatformType.TWITTER]: 'test3' }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - numberOfOpenSourceContributions: { - gte: 2, - }, - }, - ], - }, - ], - }, - orderBy: 'numberOfOpenSourceContributions_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].numberOfOpenSourceContributions).toEqual(2) - expect(members.rows[1].numberOfOpenSourceContributions).toEqual(4) - }) - - it('is successfully finding and counting all members, and tags [nodejs, vuejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.TWITTER]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - tags: { - contains: [nodeTag.id, vueTag.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.TWITTER].includes('test2')) - expect(members.rows.length).toEqual(1) - expect(member2.tags.map((t) => t.name)).toEqual(expect.arrayContaining(['nodejs', 'vuejs'])) - }) - - it('is successfully finding and counting all members, and tags [nodejs]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - tags: { - contains: [nodeTag.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member1 = members.rows.find((m) => m.username[PlatformType.GITHUB].includes('test1')) - const member2 = members.rows.find((m) => m.username[PlatformType.GITHUB].includes('test2')) - - expect(members.rows.length).toEqual(2) - expect(member1.tags[0].name).toEqual('nodejs') - expect(member2.tags.map((t) => t.name)).toEqual(expect.arrayContaining(['nodejs', 'vuejs'])) - }) - - it('is successfully finding and counting all members, and organisations [crowd.dev, pied piper]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const crowd = await OrganizationRepository.create( - { - identities: [ - { - name: 'crowd.dev', - url: 'https://crowd.dev', - platform: 'crowd', - }, - ], - displayName: 'crowd.dev', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }, - mockIRepositoryOptions, - ) - const pp = await OrganizationRepository.create( - { - identities: [ - { - name: 'pied piper', - url: 'https://piedpiper.com', - platform: 'crowd', - }, - ], - displayName: 'pied piper', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - organizations: [crowd.id, pp.id], - }, - mockIRepositoryOptions, - ) - await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - organizations: { - contains: [crowd.id, pp.id], - }, - }, - ], - }, - }, - mockIRepositoryOptions, - ) - const member2 = members.rows.find((m) => m.username[PlatformType.SLACK].includes('test2')) - expect(members.rows.length).toEqual(1) - expect(member2.organizations.map((o) => o.displayName)).toEqual( - expect.arrayContaining(['crowd.dev', 'pied piper']), - ) - }) - - it('is successfully finding and counting all members, and scoreRange is gte than 7', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const user1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - } - const user2 = { - username: { - [PlatformType.DISCORD]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - } - const user3 = { - username: { - [PlatformType.DISCORD]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - } - await MemberRepository.create(user1, mockIRepositoryOptions) - await MemberRepository.create(user2, mockIRepositoryOptions) - await MemberRepository.create(user3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - score: { - gte: 7, - }, - }, - ], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(1) - for (const member of members.rows) { - expect(member.score).toBeGreaterThanOrEqual(7) - } - }) - - it('is successfully find and counting members with various filters, computed attributes, and full options (filter, limit, offset and orderBy)', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const nodeTag = await mockIRepositoryOptions.database.tag.create({ - name: 'nodejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - const vueTag = await mockIRepositoryOptions.database.tag.create({ - name: 'vuejs', - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - }) - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [nodeTag.id], - reach: { - total: 15, - }, - }, - mockIRepositoryOptions, - ) - const member2 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - score: '6', - joinedAt: new Date(), - tags: [nodeTag.id, vueTag.id], - reach: { - total: 55, - }, - }, - mockIRepositoryOptions, - ) - const member3 = await MemberRepository.create( - { - username: { - [PlatformType.SLACK]: { - username: 'test3', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 3', - score: '7', - joinedAt: new Date(), - tags: [vueTag.id], - reach: { - total: 124, - }, - }, - mockIRepositoryOptions, - ) - - await mockIRepositoryOptions.database.activity.bulkCreate([ - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-10'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member1.id, - username: member1.username[PlatformType.SLACK], - sourceId: '#sourceId1', - sentiment: { - positive: 0.55, - negative: 0.0, - neutral: 0.45, - mixed: 0.0, - label: 'positive', - sentiment: 0.1, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-11'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId2', - sentiment: { - positive: 0.01, - negative: 0.55, - neutral: 0.55, - mixed: 0.0, - label: 'negative', - sentiment: -0.54, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-12'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member2.id, - username: member2.username[PlatformType.SLACK], - sourceId: '#sourceId3', - sentiment: { - positive: 0.94, - negative: 0.0, - neutral: 0.06, - mixed: 0.0, - label: 'positive', - sentiment: 0.94, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-13'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId4', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-14'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId5', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.41, - }, - }, - { - type: 'message', - platform: PlatformType.SLACK, - timestamp: new Date('2022-09-15'), - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - memberId: member3.id, - username: member3.username[PlatformType.SLACK], - sourceId: '#sourceId6', - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.18, - }, - }, - ]) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - let members = await MemberRepository.findAndCountAllv2( - { - filter: {}, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - expect(members.rows.length).toEqual(3) - expect(members.rows[0].activityCount).toEqual('3') - expect(members.rows[0].lastActive.toISOString()).toEqual('2022-09-15T00:00:00.000Z') - - expect(members.rows[1].activityCount).toEqual('2') - expect(members.rows[1].lastActive.toISOString()).toEqual('2022-09-12T00:00:00.000Z') - - expect(members.rows[2].activityCount).toEqual('1') - expect(members.rows[2].tags[0].name).toEqual('nodejs') - expect(members.rows[2].lastActive.toISOString()).toEqual('2022-09-10T00:00:00.000Z') - - expect(members.rows[1].tags.map((i) => i.name).sort()).toEqual(['nodejs', 'vuejs']) - expect(members.rows[0].tags[0].name).toEqual('vuejs') - - // filter and order by reach - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - reach: { - gte: 55, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'reach_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows[0].id).toEqual(member3.id) - expect(members.rows[1].id).toEqual(member2.id) - - // filter and sort by activity count - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - activityCount: { - gte: 2, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'activityCount_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by lastActive - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - lastActive: { - gte: '2022-09-11', - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'lastActive_DESC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member3.id, member2.id]) - - // filter and sort by averageSentiment (member1.avgSentiment = 0.1, member2.avgSentiment = 0.2, member3.avgSentiment = 0.34) - members = await MemberRepository.findAndCountAllv2( - { - filter: { - and: [ - { - and: [ - { - averageSentiment: { - gte: 0.2, - }, - }, - ], - }, - ], - }, - limit: 15, - offset: 0, - orderBy: 'averageSentiment_ASC', - }, - mockIRepositoryOptions, - ) - - expect(members.rows.length).toEqual(2) - expect(members.rows.map((i) => i.id)).toEqual([member2.id, member3.id]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - let cloned = lodash.cloneDeep(member1) - const returnedMember = await MemberRepository.create(cloned, mockIRepositoryOptions) - - const updateFields = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2021-06-27T15:14:30Z', - location: 'Istanbul', - } - - cloned = lodash.cloneDeep(updateFields) - const updatedMember = await MemberRepository.update( - returnedMember.id, - cloned, - mockIRepositoryOptions, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedMember.updatedAt.getTime()).toBeGreaterThan(updatedMember.createdAt.getTime()) - - updatedMember.createdAt = updatedMember.createdAt.toISOString().split('T')[0] - updatedMember.updatedAt = updatedMember.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: returnedMember.id, - username: mapUsername({ - ...updateFields.username, - ...member1.username, - }), - identities: ['discord', 'github'], - displayName: returnedMember.displayName, - attributes: updateFields.attributes, - emails: updateFields.emails, - score: updateFields.score, - lastEnriched: null, - enrichedBy: [], - contributions: null, - organizations: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - notes: [], - tasks: [], - activeOn: [], - activityTypes: [], - joinedAt: new Date(updateFields.joinedAt), - tags: [], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(updatedMember).toStrictEqual(expectedMemberCreated) - }) - - it('Should update successfuly but return without relations when doPopulateRelations=false', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - const returnedMember = await MemberRepository.create(member1, mockIRepositoryOptions) - - const updateFields = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [PlatformType.GITHUB]: { - name: 'Quoc-Anh Nguyen', - isHireable: true, - url: 'https://github.com/imcvampire', - websiteUrl: 'https://imcvampire.js.org/', - bio: 'Lazy geek', - location: 'Helsinki, Finland', - actions: [ - { - score: 2, - timestamp: '2021-05-27T15:13:30Z', - }, - ], - }, - [PlatformType.TWITTER]: { - profile_url: 'https://twitter.com/imcvampire', - url: 'https://twitter.com/imcvampire', - }, - }, - joinedAt: '2021-06-27T15:14:30Z', - location: 'Istanbul', - } - - const updatedMember = await MemberRepository.update( - returnedMember.id, - updateFields, - mockIRepositoryOptions, - false, - ) - - // check updatedAt field looks ok or not. Should be greater than createdAt - expect(updatedMember.updatedAt.getTime()).toBeGreaterThan(updatedMember.createdAt.getTime()) - - updatedMember.createdAt = updatedMember.createdAt.toISOString().split('T')[0] - updatedMember.updatedAt = updatedMember.updatedAt.toISOString().split('T')[0] - - const expectedMemberCreated = { - id: returnedMember.id, - username: mapUsername({ - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - [PlatformType.GITHUB]: { - username: 'anil_github', - integrationId: generateUUIDv1(), - }, - }), - displayName: returnedMember.displayName, - attributes: updateFields.attributes, - lastEnriched: null, - enrichedBy: [], - organizations: [], - contributions: null, - emails: updateFields.emails, - score: updateFields.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - joinedAt: new Date(updateFields.joinedAt), - affiliations: [], - manuallyCreated: false, - } - - expect(updatedMember).toStrictEqual(expectedMemberCreated) - }) - - it('Should successfully update member with given tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = await TagRepository.create({ name: 'tag1' }, mockIRepositoryOptions) - const tag2 = await TagRepository.create({ name: 'tag2' }, mockIRepositoryOptions) - const tag3 = await TagRepository.create({ name: 'tag3' }, mockIRepositoryOptions) - - // Create member with tag3 - let member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - tags: [tag3.id], - }, - mockIRepositoryOptions, - ) - - // When feeding tags attribute to update, update method will overwrite the member's tags with new given tags - // member1 is expected to have [tag1,tag2] after update - member1 = await MemberRepository.update( - member1.id, - { tags: [tag1.id, tag2.id] }, - mockIRepositoryOptions, - ) - - member1.createdAt = member1.createdAt.toISOString().split('T')[0] - member1.updatedAt = member1.updatedAt.toISOString().split('T')[0] - - member1.tags = member1.tags.map((i) => i.get({ plain: true })) - - // strip members field from tags created to expect. - // we won't be returning second level relationships. - const { members: _tag1Members, ...tag1Plain } = tag1 - const { members: _tag2Members, ...tag2Plain } = tag2 - - const expectedMemberCreated = { - id: member1.id, - username: member1.username, - displayName: member1.displayName, - identities: ['discord'], - attributes: {}, - emails: member1.emails, - score: member1.score, - organizations: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - reach: { total: -1 }, - notes: [], - tasks: [], - activeOn: [], - activityTypes: [], - joinedAt: new Date(member1.joinedAt), - tags: [tag1Plain, tag2Plain], - noMerge: [], - toMerge: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - expect(member1).toStrictEqual(expectedMemberCreated) - }) - - it('Should successfully update member with given organizations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - identities: [{ name: 'crowd.dev', url: 'https://crowd.dev', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - const org2 = await OrganizationRepository.create( - { - displayName: 'pied piper', - identities: [{ name: 'pied piper', url: 'https://piedpiper.com', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - const org3 = await OrganizationRepository.create( - { - displayName: 'hooli', - identities: [{ name: 'hooli', url: 'https://hooli.com', platform: 'crowd' }], - }, - mockIRepositoryOptions, - ) - - // Create member with tag3 - let member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: new Date(), - organizations: [org3.id], - }, - mockIRepositoryOptions, - ) - - // When feeding organizations attribute to update, update method will overwrite the member's organizations with new given orgs - // member1 is expected to have [org1,org2] after update - member1 = await MemberRepository.update( - member1.id, - { organizations: [org1.id, org2.id], organizationsReplace: true }, - mockIRepositoryOptions, - ) - - member1.createdAt = member1.createdAt.toISOString().split('T')[0] - member1.updatedAt = member1.updatedAt.toISOString().split('T')[0] - - member1.organizations = member1.organizations.map((i) => - SequelizeTestUtils.objectWithoutKey(i.get({ plain: true }), ['memberOrganizations']), - ) - - // // sort member organizations by createdAt - // member1.organizations.sort((a, b) => { - // return a.createdAt < b.createdAt ? -1 : 1 - // }) - - // strip members field from tags created to expect. - // we won't be returning second level relationships. - const { memberCount: _tag1Members, ...org1Plain } = org1 - const { memberCount: _tag2Members, ...org2Plain } = org2 - - const expectedMemberCreated = { - id: member1.id, - username: member1.username, - displayName: member1.displayName, - identities: ['discord'], - attributes: {}, - emails: member1.emails, - score: member1.score, - tags: [], - lastEnriched: null, - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - activeOn: [], - activityTypes: [], - reach: { total: -1 }, - joinedAt: new Date(member1.joinedAt), - organizations: [ - SequelizeTestUtils.objectWithoutKey(org1Plain, [ - 'lastActive', - 'identities', - 'activeOn', - 'joinedAt', - 'activityCount', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(org2Plain, [ - 'lastActive', - 'identities', - 'activeOn', - 'joinedAt', - 'activityCount', - 'segments', - 'weakIdentities', - ]), - ], - noMerge: [], - toMerge: [], - notes: [], - tasks: [], - activityCount: 0, - activeDaysCount: 0, - averageSentiment: 0, - numberOfOpenSourceContributions: 0, - lastActive: null, - lastActivity: null, - affiliations: [], - manuallyCreated: false, - } - - member1.organizations = member1.organizations.sort((a, b) => { - if (a.displayName < b.displayName) { - return -1 - } - if (a.displayName > b.displayName) { - return 1 - } - return 0 - }) - - expect(member1).toStrictEqual(expectedMemberCreated) - }) - - it('Should succesfully update member with notes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const notes1 = await NoteRepository.create( - { - body: 'note1', - }, - mockIRepositoryOptions, - ) - - const notes2 = await NoteRepository.create( - { - body: 'note2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { notes: [notes1.id, notes2.id] }, - mockIRepositoryOptions, - ) - expect(memberCreated.notes).toHaveLength(0) - expect(memberUpdated.notes).toHaveLength(2) - expect(memberUpdated.notes[0].id).toEqual(notes1.id) - expect(memberUpdated.notes[1].id).toEqual(notes2.id) - }) - - it('Should succesfully update member with tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tasks1 = await TaskRepository.create( - { - name: 'task1', - }, - mockIRepositoryOptions, - ) - - const task2 = await TaskRepository.create( - { - name: 'task2', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.tasks).toHaveLength(0) - - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { tasks: [tasks1.id, task2.id] }, - mockIRepositoryOptions, - ) - expect(memberUpdated.tasks).toHaveLength(2) - expect(memberUpdated.tasks.find((t) => t.id === tasks1.id)).not.toBeUndefined() - expect(memberUpdated.tasks.find((t) => t.id === task2.id)).not.toBeUndefined() - }) - - it('Should throw 404 error when trying to update non existent member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.update(randomUUID(), { location: 'test' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw a sequelize foreign key error when trying to update a member with a non existing tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - const member1 = await MemberRepository.create( - { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: new Date(), - }, - mockIRepositoryOptions, - ) - - await expect(() => - MemberRepository.update(member1.id, { tags: [randomUUID()] }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should succesfully update member organization affiliations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const org1 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member2add = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - affiliations: [ - { - segmentId: segment1.id, - organizationId: org1.id, - }, - { - segmentId: segment2.id, - organizationId: null, - }, - ], - } - - const memberCreated = await MemberRepository.create(member2add, mockIRepositoryOptions) - expect(memberCreated.affiliations).toHaveLength(2) - - // removes segment1 affiliation, and set segment2 affilition to org1 - const memberUpdated = await MemberRepository.update( - memberCreated.id, - { - affiliations: [ - { - segmentId: segment2.id, - organizationId: org1.id, - }, - ], - }, - mockIRepositoryOptions, - ) - - expect(memberUpdated.affiliations.filter((a) => a.segmentId === segment1.id)).toHaveLength(0) - expect( - memberUpdated.affiliations.filter((a) => a.segmentId === segment2.id)[0].organizationId, - ).toEqual(org1.id) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'test1', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - score: '1', - joinedAt: '2021-05-27T15:14:30Z', - } - const returnedMember = await MemberRepository.create(member1, mockIRepositoryOptions) - - await MemberRepository.destroy(returnedMember.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - MemberRepository.findById(returnedMember.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MemberRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('removeToMerge method', () => { - it('Should remove a member from other members toMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - await MemberRepository.addToMerge( - [{ members: [memberCreated1.id, memberCreated2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [memberCreated2.id, memberCreated1.id], similarity: null }], - mockIRepositoryOptions, - ) - - let m1 = await MemberRepository.findById(memberCreated1.id, mockIRepositoryOptions) - const m2 = await MemberRepository.findById(memberCreated2.id, mockIRepositoryOptions) - m1 = await MemberRepository.removeToMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - // Member2 should be removed from Member1.toMerge - expect(m1.toMerge.length).toBe(0) - - // Member1 is still in member2.toMerge list - expect(m2.toMerge[0]).toBe(m1.id) - }) - }) - - describe('addNoMerge method', () => { - it('Should add a member to other members noMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - let memberUpdated1 = await MemberRepository.addNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - const memberUpdated2 = await MemberRepository.addNoMerge( - memberCreated2.id, - memberCreated1.id, - mockIRepositoryOptions, - ) - - memberUpdated1 = await MemberRepository.removeToMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - expect(memberUpdated1.noMerge[0]).toBe(memberUpdated2.id) - expect(memberUpdated2.noMerge[0]).toBe(memberUpdated1.id) - }) - }) - - describe('removeNoMerge method', () => { - let options - let memberService - - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - memberService = new MemberService(options) - - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - it('Should remove a member from other members noMerge list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member1 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - } - - const member2 = { - username: { - [PlatformType.DISCORD]: { - username: 'anil2', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberCreated1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const memberCreated2 = await MemberRepository.create(member2, mockIRepositoryOptions) - - let memberUpdated1 = await MemberRepository.addNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - const memberUpdated2 = await MemberRepository.addNoMerge( - memberCreated2.id, - memberCreated1.id, - mockIRepositoryOptions, - ) - - memberUpdated1 = await MemberRepository.removeNoMerge( - memberCreated1.id, - memberCreated2.id, - mockIRepositoryOptions, - ) - - // Member2 should be removed from Member1.noMerge - expect(memberUpdated1.noMerge.length).toBe(0) - - // Member1 is still in member2.noMerge list - expect(memberUpdated2.noMerge[0]).toBe(memberUpdated1.id) - }) - }) - - describe('work experiences', () => { - let options - let memberService - let organizationService - - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - memberService = new MemberService(options) - organizationService = new OrganizationService(options) - - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - - async function createMember(data = {}) { - return await memberService.upsert({ - ...defaultMember, - username: { - [PlatformType.GITHUB]: uuid(), - }, - ...data, - }) - } - - async function createOrg(name, data = {}) { - return await organizationService.createOrUpdate({ - identities: [ - { - name, - platform: 'crowd', - }, - ], - ...data, - }) - } - - async function addWorkExperience(memberId, orgId, data = {}) { - return await MemberRepository.createOrUpdateWorkExperience( - { - memberId, - organizationId: orgId, - source: 'test', - ...data, - }, - options, - ) - } - - async function findMember(id) { - return await memberService.findById(id) - } - - function formatDate(value) { - if (!value) { - return null - } - return moment(value).format('YYYY-MM-DD') - } - - it('Should not create multiple work experiences for same org without dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should not create multiple work experiences for same org with same start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should not create multiple work experiences for same org with same dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - }) - - it('Should create multiple work experiences for same org with different dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-08', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-01-05', - }) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-06', - dateEnd: '2020-01-07', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(4) - }) - - it('Should clean up work experiences without dates once we get start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBeNull() - }) - it('Should clean up work experiences without dates once we get both dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id) - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-07-01', - }) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBe('2020-07-01') - }) - it('Should not add new work experiences without dates if we have start dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBeNull() - }) - it('Should not add new work experiences without dates if we have both dates', async () => { - let member = await createMember() - - const org = await createOrg('org') - - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - dateEnd: '2020-07-01', - }) - await addWorkExperience(member.id, org.id) - - member = await findMember(member.id) - - expect(member.organizations.length).toBe(1) - const dates = member.organizations[0].memberOrganizations.dataValues - expect(formatDate(dates.dateStart)).toBe('2020-01-01') - expect(formatDate(dates.dateEnd)).toBe('2020-07-01') - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/microserviceRepository.test.ts b/backend/src/database/repositories/__tests__/microserviceRepository.test.ts deleted file mode 100644 index 66ec1ed835..0000000000 --- a/backend/src/database/repositories/__tests__/microserviceRepository.test.ts +++ /dev/null @@ -1,463 +0,0 @@ -import { Error404 } from '@crowd/common' -import MicroserviceRepository from '../microserviceRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -describe('MicroserviceRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a microservice succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - - it('Should create a microservice succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { - init: true, - running: true, - type: 'members_score', - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: microservice2Add.init, - running: microservice2Add.running, - type: microservice2Add.type, - variant: microservice2Add.variant, - settings: microservice2Add.settings, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - - it('Should throw unique constraint error for creation of already existing type microservice in the same tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1 = { - init: true, - running: true, - type: 'members_score', - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - await MicroserviceRepository.create(microservice1, mockIRepositoryOptions) - - await expect(() => - MicroserviceRepository.create({ type: 'members_score' }, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should throw not null error if no type is given', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { - init: true, - running: true, - variant: 'premium', - settings: { testSettingsField: 'test' }, - } - - await expect(() => - MicroserviceRepository.create(microservice2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created microservice by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await MicroserviceRepository.create( - microservice2Add, - mockIRepositoryOptions, - ) - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const microserviceById = await MicroserviceRepository.findById( - microserviceCreated.id, - mockIRepositoryOptions, - ) - - microserviceById.createdAt = microserviceById.createdAt.toISOString().split('T')[0] - microserviceById.updatedAt = microserviceById.updatedAt.toISOString().split('T')[0] - - expect(microserviceById).toStrictEqual(microserviceExpected) - }) - - it('Should throw 404 error when no microservice found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created microservice entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1Created = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - const microservice2Created = await MicroserviceRepository.create( - { type: 'second' }, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microservice1Created.id, microservice2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([microservice1Created.id, microservice2Created.id]) - }) - - it('Should only return the ids of previously created microservices and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microserviceCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([microserviceCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score' }, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await MicroserviceRepository.filterIdsInTenant( - [microserviceCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all microservices, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice1Created = await MicroserviceRepository.create( - { type: 'members_score', variant: 'premium' }, - mockIRepositoryOptions, - ) - - const microservice2Created = await MicroserviceRepository.create( - { type: 'second', variant: 'premium' }, - mockIRepositoryOptions, - ) - - // Filter by type - let microservices = await MicroserviceRepository.findAndCountAll( - { filter: { type: 'members_score' } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(1) - expect(microservices.rows).toStrictEqual([microservice1Created]) - - // Filter by id - microservices = await MicroserviceRepository.findAndCountAll( - { filter: { id: microservice1Created.id } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(1) - expect(microservices.rows).toStrictEqual([microservice1Created]) - - // Filter by variant - microservices = await MicroserviceRepository.findAndCountAll( - { filter: { variant: 'premium' } }, - mockIRepositoryOptions, - ) - - expect(microservices.count).toEqual(2) - expect(microservices.rows).toStrictEqual([microservice2Created, microservice1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'twitter_followers' }, - mockIRepositoryOptions, - ) - - const microserviceUpdated = await MicroserviceRepository.update( - microserviceCreated.id, - { - init: true, - running: true, - variant: 'premium', - settings: { - testSettingAttribute: { - someAtt: 'test', - someOtherAtt: true, - }, - }, - }, - mockIRepositoryOptions, - ) - - expect(microserviceUpdated.updatedAt.getTime()).toBeGreaterThan( - microserviceUpdated.createdAt.getTime(), - ) - - const microserviceExpected = { - id: microserviceCreated.id, - init: microserviceUpdated.init, - running: microserviceUpdated.running, - type: microserviceCreated.type, - variant: microserviceUpdated.variant, - settings: microserviceUpdated.settings, - importHash: null, - createdAt: microserviceCreated.createdAt, - updatedAt: microserviceUpdated.updatedAt, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceUpdated).toStrictEqual(microserviceExpected) - }) - - it('Should throw 404 error when trying to update non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.update(randomUUID(), { type: 'some-type' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - describe('destroy method', () => { - it('Should succesfully destroy previously created microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microserviceCreated = await MicroserviceRepository.create( - { type: 'members_score', variant: 'premium' }, - mockIRepositoryOptions, - ) - - await MicroserviceRepository.destroy(microserviceCreated.id, mockIRepositoryOptions) - - // Try selecting it after destroy, should throw 404 - await expect(() => - MicroserviceRepository.findById(microserviceCreated.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - MicroserviceRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('Find all available microservices', () => { - it('Should find a single available microservices for a type', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const found: any = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found[0].tenantId).toBeDefined() - expect(found.length).toBe(1) - }) - - it('Should find all available microservices for a type, multiple available', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions2) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find non-running microservices', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const ms2 = { - type: 'twitter-followers', - running: true, - init: false, - variant: 'default', - settings: {}, - } - - const ms3 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms2, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms3, mockIRepositoryOptions3) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should only find microservices for the desired type', async () => { - const ms1 = { - type: 'twitter-followers', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const ms2 = { - type: 'members_score', - running: false, - init: false, - variant: 'default', - settings: {}, - } - - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions) - - const mockIRepositoryOptions2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms1, mockIRepositoryOptions2) - - const mockIRepositoryOptions3 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await MicroserviceRepository.create(ms2, mockIRepositoryOptions3) - - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(2) - }) - - it('Should return an empty list if no integrations are found', async () => { - const found = await MicroserviceRepository.findAllByType('twitter-followers', 1, 100) - expect(found.length).toBe(0) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/noteRepository.test.ts b/backend/src/database/repositories/__tests__/noteRepository.test.ts deleted file mode 100644 index 7768377ce6..0000000000 --- a/backend/src/database/repositories/__tests__/noteRepository.test.ts +++ /dev/null @@ -1,331 +0,0 @@ -import { PlatformType } from '@crowd/types' -import { Error404 } from '@crowd/common' -import NoteRepository from '../noteRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import MemberRepository from '../memberRepository' - -const db = null - -describe('NoteRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given note succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note2add, mockIRepositoryOptions) - - noteCreated.createdAt = noteCreated.createdAt.toISOString().split('T')[0] - noteCreated.updatedAt = noteCreated.updatedAt.toISOString().split('T')[0] - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const expectedNoteCreated = { - id: noteCreated.id, - body: note2add.body, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(noteCreated).toStrictEqual(expectedNoteCreated) - }) - - it('Should throw sequelize not null error -- body field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = {} - - await expect(() => NoteRepository.create(note2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created note by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note2add = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note2add, mockIRepositoryOptions) - - noteCreated.createdAt = noteCreated.createdAt.toISOString().split('T')[0] - noteCreated.updatedAt = noteCreated.updatedAt.toISOString().split('T')[0] - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const expectedNoteFound = { - id: noteCreated.id, - body: note2add.body, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const noteById = await NoteRepository.findById(noteCreated.id, mockIRepositoryOptions) - - noteById.createdAt = noteById.createdAt.toISOString().split('T')[0] - noteById.updatedAt = noteById.updatedAt.toISOString().split('T')[0] - - expect(noteById).toStrictEqual(expectedNoteFound) - }) - - it('Should throw 404 error when no note found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created note entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note1 = { body: 'test1' } - const note2 = { body: 'test2' } - - const note1Created = await NoteRepository.create(note1, mockIRepositoryOptions) - const note2Created = await NoteRepository.create(note2, mockIRepositoryOptions) - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [note1Created.id, note2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([note1Created.id, note2Created.id]) - }) - - it('Should only return the ids of previously created notes and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test1' } - - const noteCreated = await NoteRepository.create(note, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [noteCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([noteCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test' } - - const noteCreated = await NoteRepository.create(note, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await NoteRepository.filterIdsInTenant( - [noteCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all notes, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member = await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: 'test', - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - mockIRepositoryOptions, - ) - - const note1 = { body: 'test-note', members: [member.id] } - const note2 = { body: 'test-note-2', members: [member.id] } - const note3 = { body: 'another-note', members: [member.id] } - - const note1Created = await NoteRepository.create(note1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const note2Created = await NoteRepository.create(note2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const note3Created = await NoteRepository.create(note3, mockIRepositoryOptions) - - // Test filter by body - // Current findAndCountAll uses wildcarded like statement so it matches both notes - let notes = await NoteRepository.findAndCountAll( - { filter: { body: 'test-note' } }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(2) - expect(notes.rows).toStrictEqual([note2Created, note1Created]) - - // Test filter by id - notes = await NoteRepository.findAndCountAll( - { filter: { id: note1Created.id } }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(1) - expect(notes.rows).toStrictEqual([note1Created]) - - // Test filter by createdAt - find all between note1.createdAt and note3.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [note1Created.createdAt, note3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(notes.count).toEqual(3) - expect(notes.rows).toStrictEqual([note3Created, note2Created, note1Created]) - - // Test filter by createdAt - find all where createdAt < note2.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, note2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(notes.count).toEqual(2) - expect(notes.rows).toStrictEqual([note2Created, note1Created]) - - // Test filter by createdAt - find all where createdAt < note1.createdAt - notes = await NoteRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, note1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(notes.count).toEqual(1) - expect(notes.rows).toStrictEqual([note1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note1 = { body: 'test-note' } - - const noteCreated = await NoteRepository.create(note1, mockIRepositoryOptions) - - const noteUpdated = await NoteRepository.update( - noteCreated.id, - { body: 'updated-note-body' }, - mockIRepositoryOptions, - ) - - expect(noteUpdated.updatedAt.getTime()).toBeGreaterThan(noteUpdated.createdAt.getTime()) - - const plainUser = mockIRepositoryOptions.currentUser.get({ plain: true }) - const expectedCreatedBy = { - id: plainUser.id, - fullName: plainUser.fullName, - avatarUrl: null, - } - - const noteExpected = { - id: noteCreated.id, - body: noteUpdated.body, - importHash: null, - createdAt: noteCreated.createdAt, - updatedAt: noteUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdBy: expectedCreatedBy, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(noteUpdated).toStrictEqual(noteExpected) - }) - - it('Should throw 404 error when trying to update non existent note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.update(randomUUID(), { body: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const note = { body: 'test-note' } - - const returnedNote = await NoteRepository.create(note, mockIRepositoryOptions) - - await NoteRepository.destroy(returnedNote.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - NoteRepository.findById(returnedNote.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent note', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - NoteRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts b/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts deleted file mode 100644 index 09d7e88d5b..0000000000 --- a/backend/src/database/repositories/__tests__/organizationCacheRepository.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { Error404 } from '@crowd/common' -import organizationCacheRepository from '../organizationCacheRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -const toCreate = { - name: 'crowd.dev', - url: 'https://crowd.dev', - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crow.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - website: 'https://crowd.dev', - location: 'Berlin', - github: { - handle: 'CrowdDotDev', - }, - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - manuallyCreated: false, -} - -describe('OrganizationCacheCacheRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given organizationCache succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheCreated = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - expect(organizationCacheCreated).toStrictEqual(expectedorganizationCacheCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCache2add = {} - - await expect(() => - organizationCacheRepository.create(organizationCache2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created organizationCache by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findById( - organizationCacheCreated.id, - mockIRepositoryOptions, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - - it('Should throw 404 error when no organizationCache found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('findByUrl method', () => { - it('Should successfully find created organizationCache by URL', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findByUrl( - organizationCacheCreated.url, - mockIRepositoryOptions, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - - it('Should be independend of tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mock2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCacheCreated.createdAt = organizationCacheCreated.createdAt - .toISOString() - .split('T')[0] - organizationCacheCreated.updatedAt = organizationCacheCreated.updatedAt - .toISOString() - .split('T')[0] - - const expectedorganizationCacheFound = { - id: organizationCacheCreated.id, - ...toCreate, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - } - const organizationCacheById = await organizationCacheRepository.findByUrl( - organizationCacheCreated.url, - mock2, - ) - - organizationCacheById.createdAt = organizationCacheById.createdAt.toISOString().split('T')[0] - organizationCacheById.updatedAt = organizationCacheById.updatedAt.toISOString().split('T')[0] - - expect(organizationCacheById).toStrictEqual(expectedorganizationCacheFound) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCacheCreated = await organizationCacheRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - const organizationCacheUpdated = await organizationCacheRepository.update( - organizationCacheCreated.id, - { name: 'updated-organizationCache-name' }, - mockIRepositoryOptions, - ) - - expect(organizationCacheUpdated.updatedAt.getTime()).toBeGreaterThan( - organizationCacheUpdated.createdAt.getTime(), - ) - - const organizationCacheExpected = { - id: organizationCacheCreated.id, - ...toCreate, - name: organizationCacheUpdated.name, - importHash: null, - createdAt: organizationCacheCreated.createdAt, - updatedAt: organizationCacheUpdated.updatedAt, - deletedAt: null, - } - - expect(organizationCacheUpdated).toStrictEqual(organizationCacheExpected) - }) - - it('Should throw 404 error when trying to update non existent organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.update( - randomUUID(), - { name: 'non-existent' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCache = { name: 'test-organizationCache' } - - const returnedorganizationCache = await organizationCacheRepository.create( - organizationCache, - mockIRepositoryOptions, - ) - - await organizationCacheRepository.destroy( - returnedorganizationCache.id, - mockIRepositoryOptions, - true, - ) - - // Try selecting it after destroy, should throw 404 - await expect(() => - organizationCacheRepository.findById(returnedorganizationCache.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent organizationCache', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - organizationCacheRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/organizationRepository.test.ts b/backend/src/database/repositories/__tests__/organizationRepository.test.ts deleted file mode 100644 index fa36bc6878..0000000000 --- a/backend/src/database/repositories/__tests__/organizationRepository.test.ts +++ /dev/null @@ -1,1465 +0,0 @@ -import moment from 'moment' -import { generateUUIDv1, Error404 } from '@crowd/common' -import { PlatformType } from '@crowd/types' -import OrganizationRepository from '../organizationRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import MemberRepository from '../memberRepository' -import ActivityRepository from '../activityRepository' - -const db = null - -const toCreate = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - displayName: 'crowd.dev', - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crow.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, -} - -async function createMembers(options) { - return await [ - ( - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'gilfoyle', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: '2020-05-27T15:13:30Z', - }, - options, - ) - ).id, - ( - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'dinesh', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 2', - joinedAt: '2020-06-27T15:13:30Z', - }, - options, - ) - ).id, - ] -} - -async function createActivitiesForMembers(memberIds: string[], organizationId: string, options) { - for (const memberId of memberIds) { - await ActivityRepository.create( - { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - }, - title: 'Title', - body: 'Here', - url: 'https://github.com', - channel: 'channel', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 98, - }, - isContribution: true, - username: 'test', - member: memberId, - organizationId, - score: 1, - sourceId: '#sourceId:' + memberId, - }, - options, - ) - } -} - -async function createOrganization(organization: any, options, members = []) { - const memberIds = [] - for (const member of members) { - const memberCreated = await MemberRepository.create( - SequelizeTestUtils.objectWithoutKey(member, 'activities'), - options, - ) - - if (member.activities) { - for (const activity of member.activities) { - await ActivityRepository.create({ ...activity, member: memberCreated.id }, options) - } - } - - memberIds.push(memberCreated.id) - } - organization.members = memberIds - const organizationCreated = await OrganizationRepository.create(organization, options) - return { ...organizationCreated, members: memberIds } -} - -describe('OrganizationRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given organization succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - - delete organizationCreated.identities[0].createdAt - delete organizationCreated.identities[0].updatedAt - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationCreated = { - id: organizationCreated.id, - ...toCreate, - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - importHash: null, - lastActive: null, - joinedAt: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - expect(organizationCreated).toStrictEqual(expectedOrganizationCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization2add = {} - - await expect(() => - OrganizationRepository.create(organization2add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - - it('Should create an organization with members succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const memberIds = await createMembers(mockIRepositoryOptions) - - const toCreateWithMember = { - ...toCreate, - members: memberIds, - } - let organizationCreated = await OrganizationRepository.create( - toCreateWithMember, - mockIRepositoryOptions, - ) - await createActivitiesForMembers(memberIds, organizationCreated.id, mockIRepositoryOptions) - await mockIRepositoryOptions.database.sequelize.query( - 'REFRESH MATERIALIZED VIEW mv_activities_cube', - ) - - organizationCreated = await OrganizationRepository.findById( - organizationCreated.id, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - organizationCreated.lastActive = organizationCreated.lastActive.toISOString().split('T')[0] - organizationCreated.joinedAt = organizationCreated.joinedAt.toISOString().split('T')[0] - - delete organizationCreated.identities[0].createdAt - delete organizationCreated.identities[0].updatedAt - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationCreated = { - id: organizationCreated.id, - ...toCreate, - memberCount: 2, - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - activityCount: 2, - github: null, - location: null, - website: null, - lastActive: '2020-05-27', - joinedAt: '2020-05-27', - activeOn: ['github'], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - expect(organizationCreated).toStrictEqual(expectedOrganizationCreated) - - const member1 = await MemberRepository.findById(memberIds[0], mockIRepositoryOptions) - const member2 = await MemberRepository.findById(memberIds[1], mockIRepositoryOptions) - expect(member1.organizations.length).toEqual(1) - expect(member2.organizations.length).toEqual(1) - }) - }) - - describe('findById method', () => { - it('Should successfully find created organization by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - organizationCreated.createdAt = organizationCreated.createdAt.toISOString().split('T')[0] - organizationCreated.updatedAt = organizationCreated.updatedAt.toISOString().split('T')[0] - - const primaryIdentity = toCreate.identities[0] - - const expectedOrganizationFound = { - id: organizationCreated.id, - ...toCreate, - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - lastActive: null, - joinedAt: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - const organizationById = await OrganizationRepository.findById( - organizationCreated.id, - mockIRepositoryOptions, - ) - - organizationById.createdAt = organizationById.createdAt.toISOString().split('T')[0] - organizationById.updatedAt = organizationById.updatedAt.toISOString().split('T')[0] - - delete organizationById.identities[0].createdAt - delete organizationById.identities[0].updatedAt - - expect(organizationById).toStrictEqual(expectedOrganizationFound) - }) - - it('Should throw 404 error when no organization found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created organization entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization1 = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - const organization2 = { - identities: [{ name: 'test2', platform: 'crowd' }], - displayName: 'test2', - } - - const organization1Created = await OrganizationRepository.create( - organization1, - mockIRepositoryOptions, - ) - const organization2Created = await OrganizationRepository.create( - organization2, - mockIRepositoryOptions, - ) - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organization1Created.id, organization2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([organization1Created.id, organization2Created.id]) - }) - - it('Should only return the ids of previously created organizations and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - - const organizationCreated = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organizationCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([organizationCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { - identities: [{ name: 'test1', platform: 'crowd' }], - displayName: 'test1', - } - - const organizationCreated = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await OrganizationRepository.filterIdsInTenant( - [organizationCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - // we can skip this test - findAndCount is not used anymore - we use opensearch method findAndCountAllOpensearch instead - it.skip('Should find and count all organizations, with simple filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization1 = { name: 'test-organization' } - const organization2 = { name: 'test-organization-2' } - const organization3 = { name: 'another-organization' } - - const organization1Created = await OrganizationRepository.create( - organization1, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const organization2Created = await OrganizationRepository.create( - organization2, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const organization3Created = await OrganizationRepository.create( - organization3, - mockIRepositoryOptions, - ) - - await MemberRepository.create( - { - username: { - [PlatformType.GITHUB]: { - username: 'test-member', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Member 1', - joinedAt: moment().toDate(), - organizations: [ - organization1Created.id, - organization2Created.id, - organization3Created.id, - ], - }, - mockIRepositoryOptions, - ) - - const foundOrganization1 = await OrganizationRepository.findById( - organization1Created.id, - mockIRepositoryOptions, - ) - - const foundOrganization2 = await OrganizationRepository.findById( - organization2Created.id, - mockIRepositoryOptions, - ) - - const foundOrganization3 = await OrganizationRepository.findById( - organization3Created.id, - mockIRepositoryOptions, - ) - - // Test filter by name - // Current findAndCountAll uses wildcarded like statement so it matches both organizations - let organizations - try { - organizations = await OrganizationRepository.findAndCountAll( - { filter: { name: 'test-organization' } }, - mockIRepositoryOptions, - ) - } catch (err) { - console.error(err) - throw err - } - - expect(organizations.count).toEqual(2) - expect(organizations.rows).toEqual([foundOrganization2, foundOrganization1]) - - // Test filter by id - organizations = await OrganizationRepository.findAndCountAll( - { filter: { id: organization1Created.id } }, - mockIRepositoryOptions, - ) - - expect(organizations.count).toEqual(1) - expect(organizations.rows).toStrictEqual([foundOrganization1]) - - // Test filter by createdAt - find all between organization1.createdAt and organization3.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [organization1Created.createdAt, organization3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(organizations.count).toEqual(3) - expect(organizations.rows).toStrictEqual([ - foundOrganization3, - foundOrganization2, - foundOrganization1, - ]) - - // Test filter by createdAt - find all where createdAt < organization2.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, organization2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(organizations.count).toEqual(2) - expect(organizations.rows).toStrictEqual([foundOrganization2, foundOrganization1]) - - // Test filter by createdAt - find all where createdAt < organization1.createdAt - organizations = await OrganizationRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, organization1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(organizations.count).toEqual(1) - expect(organizations.rows).toStrictEqual([foundOrganization1]) - }) - }) - - // we can skip these tests as well - we use opensearch method findAndCountAllOpensearch instead - describe.skip('filter method', () => { - const crowddev = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - url: 'https://crowd.dev', - }, - ], - description: 'Community-led Growth for Developer-first Companies.\nJoin our private beta', - emails: ['hello@crowd.dev', 'jonathan@crowd.dev'], - phoneNumbers: ['+42 424242424'], - logo: 'https://logo.clearbit.com/crowd.dev', - tags: ['community', 'growth', 'developer-first'], - twitter: { - handle: 'CrowdDotDev', - id: '1362101830923259908', - bio: 'Community-led Growth for Developer-first Companies.\nJoin our private beta. 👇', - followers: 107, - following: 0, - location: '🌍 remote', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/crowddevhq', - }, - crunchbase: { - handle: 'company/crowddevhq', - }, - employees: 42, - revenueRange: { - min: 10, - max: 50, - }, - } - - const piedpiper = { - identities: [ - { - name: 'Pied Piper', - platform: 'crowd', - url: 'https://piedpiper.io', - }, - ], - description: 'Pied Piper is a fictional technology company in the HBO television series', - emails: ['richard@piedpiper.io', 'jarded@pipedpiper.io'], - phoneNumbers: ['+42 54545454'], - logo: 'https://logo.clearbit.com/piedpiper', - tags: ['new-internet', 'compression'], - twitter: { - handle: 'piedPiper', - id: '1362101830923259908', - bio: 'Pied Piper is a making the new, decentralized internet', - followers: 1024, - following: 0, - location: 'silicon valley', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/piedpiper', - }, - crunchbase: { - handle: 'company/piedpiper', - }, - employees: 100, - revenueRange: { - min: 0, - max: 1, - }, - } - - const hooli = { - identities: [ - { - name: 'Hooli', - platform: 'crowd', - url: 'https://hooli.com', - }, - ], - description: 'Hooli is a fictional technology company in the HBO television series', - emails: ['gavin@hooli.com'], - phoneNumbers: ['+42 12121212'], - logo: 'https://logo.clearbit.com/hooli', - tags: ['not-google', 'elephant'], - twitter: { - handle: 'hooli', - id: '1362101830923259908', - bio: 'Hooli is making the world a better place', - followers: 1000000, - following: 0, - location: 'silicon valley', - site: 'https://t.co/GRLDhqFWk4', - avatar: 'https://pbs.twimg.com/profile_images/1419741008716251141/6exZe94-_normal.jpg', - }, - linkedin: { - handle: 'company/hooli', - }, - crunchbase: { - handle: 'company/hooli', - }, - employees: 10000, - revenueRange: { - min: 200, - max: 500, - }, - } - - it('Should filter by name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - name: 'Pied Piper', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toEqual('Pied Piper') - }) - - it('Should filter by url', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - url: 'crowd.dev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by description', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - description: 'community', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by emails', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - emails: 'richard@piedpiper.io,jonathan@crowd.dev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(2) - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - emails: ['richard@piedpiper.io', 'jonathan@crowd.dev'], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by tags', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - tags: 'new-internet,not-google,new', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(2) - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - tags: ['new-internet', 'not-google', 'new'], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by twitter handle', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - twitter: 'crowdDotDev', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by linkedin handle', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - linkedin: 'crowddevhq', - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - it('Should filter by employee range', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - employeesRange: [90, 120], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('Pied Piper') - }) - - it('Should filter by revenue range', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - revenueMin: 0, - revenueMax: 1, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('Pied Piper') - - const found2 = await OrganizationRepository.findAndCountAll( - { - filter: { - revenueMin: 9, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - - expect(found2.count).toEqual(2) - }) - - it('Should filter by members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - activities: [ - { - username: 'joan', - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - sourceId: '#sourceId1', - }, - ], - }, - ]) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const memberId = await ( - await MemberRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows[0].id - - const found = await OrganizationRepository.findAndCountAll( - { - filter: { - members: [memberId], - }, - }, - mockIRepositoryOptions, - ) - - expect(found.count).toEqual(1) - expect(found.rows[0].name).toBe('crowd.dev') - }) - - // we can skip this test - findAndCount is not used anymore - we use opensearch method findAndCountAllOpensearch instead - it.skip('Should filter by memberCount', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const org1 = await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'anil', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'uros', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'uros', - joinedAt: moment().toDate(), - }, - ]) - const org2 = await createOrganization(piedpiper, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'mario', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'mario', - joinedAt: moment().toDate(), - }, - { - username: { - github: { - username: 'igor', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'igor', - joinedAt: moment().toDate(), - }, - ]) - await createOrganization(hooli, mockIRepositoryOptions) - - const found = await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - memberCount: { - gte: 2, - }, - }, - }, - mockIRepositoryOptions, - ) - - delete org1.members - delete org2.members - - expect(found.count).toBe(2) - expect(found.rows.sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1))).toStrictEqual([ - org1, - org2, - ]) - }) - - it('Should work with advanced filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await createOrganization(crowddev, mockIRepositoryOptions, [ - { - username: { - github: { - username: 'joan', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Joan', - joinedAt: moment().toDate(), - }, - ]) - await createOrganization(piedpiper, mockIRepositoryOptions) - await createOrganization(hooli, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const memberId = await ( - await MemberRepository.findAndCountAll({}, mockIRepositoryOptions) - ).rows[0].id - - // Revenue nested field - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - revenue: { - gte: 9, - }, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - - // Twitter bio - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - 'twitter.bio': { - textContains: 'world a better place', - }, - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(1) - - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - and: [ - { - revenue: { - gte: 9, - }, - }, - { - revenue: { - lte: 100, - }, - }, - ], - }, - { - 'twitter.bio': { - textContains: 'world a better place', - }, - }, - ], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - - expect( - ( - await OrganizationRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - and: [ - { - tags: { - overlap: ['not-google'], - }, - }, - { - 'twitter.location': { - textContains: 'silicon valley', - }, - }, - ], - }, - { - members: [memberId], - }, - ], - }, - includeOrganizationsWithoutMembers: true, - }, - mockIRepositoryOptions, - ) - ).count, - ).toEqual(2) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organizationCreated = await OrganizationRepository.create( - toCreate, - mockIRepositoryOptions, - ) - - const organizationUpdated = await OrganizationRepository.update( - organizationCreated.id, - { displayName: 'updated-organization-name' }, - mockIRepositoryOptions, - ) - - expect(organizationUpdated.updatedAt.getTime()).toBeGreaterThan( - organizationUpdated.createdAt.getTime(), - ) - - const primaryIdentity = organizationCreated.identities[0] - - delete organizationUpdated.identities[0].createdAt - delete organizationUpdated.identities[0].updatedAt - - const organizationExpected = { - id: organizationCreated.id, - ...toCreate, - github: null, - location: null, - website: null, - memberCount: 0, - activityCount: 0, - activeOn: [], - identities: [ - { - integrationId: null, - name: primaryIdentity.name, - organizationId: organizationCreated.id, - platform: primaryIdentity.platform, - url: primaryIdentity.url, - sourceId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - }, - ], - lastActive: null, - joinedAt: null, - displayName: organizationUpdated.displayName, - importHash: null, - createdAt: organizationCreated.createdAt, - updatedAt: organizationUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments.map((s) => s.id), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - isTeamOrganization: false, - attributes: {}, - weakIdentities: [], - } - - expect(organizationUpdated).toStrictEqual(organizationExpected) - }) - - it('Should throw 404 error when trying to update non existent organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.update( - randomUUID(), - { name: 'non-existent' }, - mockIRepositoryOptions, - ), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('setOrganizationIsTeam method', () => { - const member1 = { - username: { - devto: { - username: 'iambarker', - integrationId: generateUUIDv1(), - }, - github: { - username: 'barker', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Jack Barker', - attributes: { - bio: { - github: 'Head of development at Hooli', - twitter: 'Head of development at Hooli | ex CEO at Pied Piper', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'Head of development', default: 'Head of development' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: - 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jack-barker-best.jpg', - default: - 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/jack-barker-best.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'barker', - sourceId: '#sourceId1', - }, - ], - } - - const member2 = { - username: { - devto: { - username: 'thebelson', - integrationId: generateUUIDv1(), - }, - github: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - discord: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - twitter: { - username: 'gavin', - integrationId: generateUUIDv1(), - }, - linkedin: { - username: 'gavinbelson', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Gavin Belson', - attributes: { - bio: { - custom: 'CEO at Hooli', - github: 'CEO at Hooli', - default: 'CEO at Hooli', - twitter: 'CEO at Hooli', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'CEO', default: 'CEO' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gavin.jpg', - default: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/gavin.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-28T15:13:30Z', - username: 'gavinbelson', - platform: PlatformType.GITHUB, - sourceId: '#sourceId2', - }, - ], - } - - const member3 = { - username: { - devto: { - username: 'bigheader', - integrationId: generateUUIDv1(), - }, - github: { - username: 'bighead', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Big Head', - attributes: { - bio: { - custom: 'Executive at the Hooli XYZ project', - github: 'Co-head Dreamer of the Hooli XYZ project', - default: 'Executive at the Hooli XYZ project', - twitter: 'Co-head Dreamer of the Hooli XYZ project', - }, - sample: { crowd: true, default: true }, - jobTitle: { custom: 'Co-head Dreamer', default: 'Co-head Dreamer' }, - location: { github: 'Silicon Valley', default: 'Silicon Valley' }, - avatarUrl: { - custom: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/big-head-small.jpg', - default: 'https://s3.eu-central-1.amazonaws.com/crowd.dev-sample-data/big-head-small.jpg', - }, - }, - joinedAt: moment().toDate(), - activities: [ - { - type: 'star', - timestamp: '2020-05-29T15:13:30Z', - platform: PlatformType.GITHUB, - username: 'bighead', - sourceId: '#sourceId3', - }, - ], - } - it('Should succesfully set/unset organization members as team members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const org = await createOrganization(toCreate, mockIRepositoryOptions, [ - member1, - member2, - member3, - ]) - - // mark organization members as team members - await OrganizationRepository.setOrganizationIsTeam(org.id, true, mockIRepositoryOptions) - - let m1 = await MemberRepository.findById(org.members[0], mockIRepositoryOptions) - let m2 = await MemberRepository.findById(org.members[1], mockIRepositoryOptions) - let m3 = await MemberRepository.findById(org.members[2], mockIRepositoryOptions) - - expect(m1.attributes.isTeamMember.default).toEqual(true) - expect(m2.attributes.isTeamMember.default).toEqual(true) - expect(m3.attributes.isTeamMember.default).toEqual(true) - - // expect other attributes intact - delete m1.attributes.isTeamMember - expect(m1.attributes).toStrictEqual(member1.attributes) - - delete m2.attributes.isTeamMember - expect(m2.attributes).toStrictEqual(member2.attributes) - - delete m3.attributes.isTeamMember - expect(m3.attributes).toStrictEqual(member3.attributes) - - // now unmark - await OrganizationRepository.setOrganizationIsTeam(org.id, false, mockIRepositoryOptions) - - m1 = await MemberRepository.findById(org.members[0], mockIRepositoryOptions) - m2 = await MemberRepository.findById(org.members[1], mockIRepositoryOptions) - m3 = await MemberRepository.findById(org.members[2], mockIRepositoryOptions) - - expect(m1.attributes.isTeamMember.default).toEqual(false) - expect(m2.attributes.isTeamMember.default).toEqual(false) - expect(m3.attributes.isTeamMember.default).toEqual(false) - - // expect other attributes intact - delete m1.attributes.isTeamMember - expect(m1.attributes).toStrictEqual(member1.attributes) - - delete m2.attributes.isTeamMember - expect(m2.attributes).toStrictEqual(member2.attributes) - - delete m3.attributes.isTeamMember - expect(m3.attributes).toStrictEqual(member3.attributes) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const organization = { displayName: 'test-organization' } - - const returnedOrganization = await OrganizationRepository.create( - organization, - mockIRepositoryOptions, - ) - - await OrganizationRepository.destroy(returnedOrganization.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - OrganizationRepository.findById(returnedOrganization.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent organization', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - OrganizationRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts b/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts deleted file mode 100644 index 8eaf8b98b8..0000000000 --- a/backend/src/database/repositories/__tests__/recurringEmailsHistoryRepository.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { randomUUID } from 'crypto' - -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import { - RecurringEmailsHistoryData, - RecurringEmailType, -} from '../../../types/recurringEmailsHistoryTypes' -import RecurringEmailsHistoryRepository from '../recurringEmailsHistoryRepository' - -const db = null - -describe('RecurringEmailsHistory tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create recurring email history with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - type: RecurringEmailType.WEEKLY_ANALYTICS, - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - const history = await rehRepository.create(historyData) - - expect(new Date(historyData.emailSentAt)).toStrictEqual(history.emailSentAt) - expect(historyData.emailSentTo).toStrictEqual(history.emailSentTo) - expect(historyData.tenantId).toStrictEqual(history.tenantId) - expect(historyData.weekOfYear).toStrictEqual(history.weekOfYear) - }) - - it('Should throw an error when mandatory fields are missing', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - type: undefined, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: undefined, - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: undefined, - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - - await expect(() => - rehRepository.create({ - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: undefined, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - }), - ).rejects.toThrowError() - }) - }) - - describe('findById method', () => { - it('Should find historical receipt by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.WEEKLY_ANALYTICS, - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const receiptCreated = await rehRepository.create(historyData) - const receiptFoundById = await rehRepository.findById(receiptCreated.id) - - expect(receiptFoundById).toStrictEqual(receiptCreated) - }) - - it('Should return null for non-existing receipt entry', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const cache = await rehRepository.findById(randomUUID()) - expect(cache).toBeNull() - }) - }) - - describe('findByWeekOfYear method', () => { - it('Should find historical receipt by week of year', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const historyData: RecurringEmailsHistoryData = { - emailSentAt: '2023-01-02T00:00:00Z', - emailSentTo: ['anil@crowd.dev', 'uros@crowd.dev'], - tenantId: mockIRepositoryOptions.currentTenant.id, - weekOfYear: '1', - type: RecurringEmailType.EAGLE_EYE_DIGEST, - } - - const rehRepository = new RecurringEmailsHistoryRepository(mockIRepositoryOptions) - - const receiptCreated = await rehRepository.create(historyData) - - // should find recently created receipt - let receiptFound = await rehRepository.findByWeekOfYear( - mockIRepositoryOptions.currentTenant.id, - '1', - RecurringEmailType.EAGLE_EYE_DIGEST, - ) - - expect(receiptCreated).toStrictEqual(receiptFound) - - // shouldn't find any receipts - receiptFound = await rehRepository.findByWeekOfYear( - mockIRepositoryOptions.currentTenant.id, - '2', - RecurringEmailType.EAGLE_EYE_DIGEST, - ) - - expect(receiptFound).toBeNull() - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/reportRepository.test.ts b/backend/src/database/repositories/__tests__/reportRepository.test.ts deleted file mode 100644 index 2ca0fcb09b..0000000000 --- a/backend/src/database/repositories/__tests__/reportRepository.test.ts +++ /dev/null @@ -1,450 +0,0 @@ -import { Error404 } from '@crowd/common' -import ReportRepository from '../reportRepository' -import WidgetRepository from '../widgetRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -describe('ReportRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a report succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { name: 'test-report' } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: false, - isTemplate: false, - name: report2Add.name, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should create a report succesfully with given values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { - name: 'test-report', - public: true, - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: report2Add.public, - name: report2Add.name, - importHash: null, - isTemplate: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should create a report succesfully with given values and widgets', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let widget1 = await WidgetRepository.create( - { type: 'test-type', title: 'Some line graph' }, - mockIRepositoryOptions, - ) - let widget2 = await WidgetRepository.create( - { type: 'test-type-2', title: 'Some bar graph' }, - mockIRepositoryOptions, - ) - let widget3 = await WidgetRepository.create( - { type: 'test-type-3', title: 'Some area graph' }, - mockIRepositoryOptions, - ) - - const report2Add = { - name: 'test-report', - public: true, - widgets: [widget1.id, widget2.id, widget3.id], - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.widgets = reportCreated.widgets.map((i) => i.get({ plain: true })) - - widget1 = await WidgetRepository.findById(widget1.id, mockIRepositoryOptions) - widget2 = await WidgetRepository.findById(widget2.id, mockIRepositoryOptions) - widget3 = await WidgetRepository.findById(widget3.id, mockIRepositoryOptions) - - // strip report object from widgets (only first layer associations are returned) - const { report: _widget1Report, ...widget1Raw } = widget1 - const { report: _widget2Report, ...widget2Raw } = widget2 - const { report: _widget3Report, ...widget3Raw } = widget3 - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const reportExpected = { - id: reportCreated.id, - public: report2Add.public, - name: report2Add.name, - importHash: null, - isTemplate: false, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [widget1Raw, widget2Raw, widget3Raw], - viewedBy: [], - } - - expect(reportCreated).toStrictEqual(reportExpected) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = {} - - await expect(() => - ReportRepository.create(report2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created report by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { name: 'test-report' } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - reportCreated.createdAt = reportCreated.createdAt.toISOString().split('T')[0] - reportCreated.updatedAt = reportCreated.updatedAt.toISOString().split('T')[0] - - const expectedReport = { - id: reportCreated.id, - public: false, - name: report2Add.name, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - isTemplate: false, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [], - viewedBy: [], - } - const reportById = await ReportRepository.findById(reportCreated.id, mockIRepositoryOptions) - - reportById.createdAt = reportById.createdAt.toISOString().split('T')[0] - reportById.updatedAt = reportById.updatedAt.toISOString().split('T')[0] - - expect(reportById).toStrictEqual(expectedReport) - }) - - it('Should throw 404 error when no report found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created report entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = { - name: 'report-test-1', - public: true, - } - const report2 = { name: 'report-test-2' } - - const report1Created = await ReportRepository.create(report1, mockIRepositoryOptions) - const report2Created = await ReportRepository.create(report2, mockIRepositoryOptions) - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [report1Created.id, report2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([report1Created.id, report2Created.id]) - }) - - it('Should only return the ids of previously created reports and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = { name: 'report-test' } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [reportCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([reportCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = { name: 'report-test' } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await ReportRepository.filterIdsInTenant( - [reportCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all reports, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = await ReportRepository.create( - { name: 'test-report-1', public: true, isTemplate: false }, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const report2 = await ReportRepository.create( - { name: 'test-report-2', public: false, isTemplate: true }, - mockIRepositoryOptions, - ) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const report3 = await ReportRepository.create( - { name: 'another-report', public: false, isTemplate: true }, - mockIRepositoryOptions, - ) - - // Filter by name - let reports = await ReportRepository.findAndCountAll( - { filter: { name: 'test-report' } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report2, report1]) - - // Filter by id - reports = await ReportRepository.findAndCountAll( - { filter: { id: report3.id } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report3]) - - // filter by public - reports = await ReportRepository.findAndCountAll( - { filter: { public: false } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report3, report2]) - - // Filter by createdAt - find all between report1.createdAt and report3.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [report1.createdAt, report3.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(3) - expect(reports.rows).toStrictEqual([report3, report2, report1]) - - // Filter by createdAt - find all where createdAt < report2.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, report2.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(2) - expect(reports.rows).toStrictEqual([report2, report1]) - - // Filter by createdAt - find all where createdAt < report1.createdAt - reports = await ReportRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, report1.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report1]) - - // filter by isTemplate - reports = await ReportRepository.findAndCountAll( - { filter: { isTemplate: false } }, - mockIRepositoryOptions, - ) - - expect(reports.count).toEqual(1) - expect(reports.rows).toStrictEqual([report1]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report = await ReportRepository.create({ name: 'test-report' }, mockIRepositoryOptions) - - let widget = await WidgetRepository.create({ type: 'widget-test' }, mockIRepositoryOptions) - - const reportUpdated = await ReportRepository.update( - report.id, - { - name: 'updated-report-name', - public: true, - widgets: [widget.id], - }, - mockIRepositoryOptions, - ) - - // Check updatedat is updated correctly - expect(reportUpdated.updatedAt.getTime()).toBeGreaterThan(reportUpdated.createdAt.getTime()) - - reportUpdated.widgets = reportUpdated.widgets.map((i) => i.get({ plain: true })) - - widget = await WidgetRepository.findById(widget.id, mockIRepositoryOptions) - - const { report: _widget1Report, ...widgetRaw } = widget - - const reportExpected = { - id: report.id, - public: reportUpdated.public, - name: reportUpdated.name, - importHash: null, - createdAt: report.createdAt, - updatedAt: reportUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - widgets: [widgetRaw], - isTemplate: false, - viewedBy: [], - } - - expect(reportUpdated).toStrictEqual(reportExpected) - }) - - it('Should throw 404 error when trying to update non existent report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.update(randomUUID(), { type: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created report and its widgets', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = await WidgetRepository.create({ type: 'widget-test' }, mockIRepositoryOptions) - - const report = await ReportRepository.create( - { - name: 'test-report', - public: true, - widgets: [widget.id], - }, - mockIRepositoryOptions, - ) - - await ReportRepository.destroy(report.id, mockIRepositoryOptions, true) - - // Try selecting both report and its widget after destroy, should throw 404 - await expect(() => - ReportRepository.findById(report.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - await expect(() => - WidgetRepository.findById(widget.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - ReportRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/tagRepository.test.ts b/backend/src/database/repositories/__tests__/tagRepository.test.ts deleted file mode 100644 index a43c20d744..0000000000 --- a/backend/src/database/repositories/__tests__/tagRepository.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import { Error404 } from '@crowd/common' -import TagRepository from '../tagRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -describe('TagRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given tag succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag2add, mockIRepositoryOptions) - - tagCreated.createdAt = tagCreated.createdAt.toISOString().split('T')[0] - tagCreated.updatedAt = tagCreated.updatedAt.toISOString().split('T')[0] - - const expectedTagCreated = { - id: tagCreated.id, - name: tag2add.name, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(tagCreated).toStrictEqual(expectedTagCreated) - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = {} - - await expect(() => TagRepository.create(tag2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created tag by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag2add = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag2add, mockIRepositoryOptions) - - tagCreated.createdAt = tagCreated.createdAt.toISOString().split('T')[0] - tagCreated.updatedAt = tagCreated.updatedAt.toISOString().split('T')[0] - - const expectedTagFound = { - id: tagCreated.id, - name: tag2add.name, - members: [], - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const tagById = await TagRepository.findById(tagCreated.id, mockIRepositoryOptions) - - tagById.createdAt = tagById.createdAt.toISOString().split('T')[0] - tagById.updatedAt = tagById.updatedAt.toISOString().split('T')[0] - - expect(tagById).toStrictEqual(expectedTagFound) - }) - - it('Should throw 404 error when no tag found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created tag entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test1' } - const tag2 = { name: 'test2' } - - const tag1Created = await TagRepository.create(tag1, mockIRepositoryOptions) - const tag2Created = await TagRepository.create(tag2, mockIRepositoryOptions) - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tag1Created.id, tag2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([tag1Created.id, tag2Created.id]) - }) - - it('Should only return the ids of previously created tags and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test1' } - - const tagCreated = await TagRepository.create(tag, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tagCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([tagCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test' } - - const tagCreated = await TagRepository.create(tag, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await TagRepository.filterIdsInTenant( - [tagCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all tags, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test-tag' } - const tag2 = { name: 'test-tag-2' } - const tag3 = { name: 'another-tag' } - - const tag1Created = await TagRepository.create(tag1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const tag2Created = await TagRepository.create(tag2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const tag3Created = await TagRepository.create(tag3, mockIRepositoryOptions) - - // Test filter by name - // Current findAndCountAll uses wildcarded like statement so it matches both tags - let tags = await TagRepository.findAndCountAll( - { filter: { name: 'test-tag' } }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(2) - expect(tags.rows).toStrictEqual([tag2Created, tag1Created]) - - // Test filter by id - tags = await TagRepository.findAndCountAll( - { filter: { id: tag1Created.id } }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(1) - expect(tags.rows).toStrictEqual([tag1Created]) - - // Test filter by createdAt - find all between tag1.createdAt and tag3.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [tag1Created.createdAt, tag3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(tags.count).toEqual(3) - expect(tags.rows).toStrictEqual([tag3Created, tag2Created, tag1Created]) - - // Test filter by createdAt - find all where createdAt < tag2.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, tag2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(tags.count).toEqual(2) - expect(tags.rows).toStrictEqual([tag2Created, tag1Created]) - - // Test filter by createdAt - find all where createdAt < tag1.createdAt - tags = await TagRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, tag1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(tags.count).toEqual(1) - expect(tags.rows).toStrictEqual([tag1Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag1 = { name: 'test-tag' } - - const tagCreated = await TagRepository.create(tag1, mockIRepositoryOptions) - - const tagUpdated = await TagRepository.update( - tagCreated.id, - { name: 'updated-tag-name' }, - mockIRepositoryOptions, - ) - - expect(tagUpdated.updatedAt.getTime()).toBeGreaterThan(tagUpdated.createdAt.getTime()) - - const tagExpected = { - id: tagCreated.id, - name: tagUpdated.name, - importHash: null, - createdAt: tagCreated.createdAt, - updatedAt: tagUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(tagUpdated).toStrictEqual(tagExpected) - }) - - it('Should throw 404 error when trying to update non existent tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.update(randomUUID(), { name: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const tag = { name: 'test-tag' } - - const returnedTag = await TagRepository.create(tag, mockIRepositoryOptions) - - await TagRepository.destroy(returnedTag.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - TagRepository.findById(returnedTag.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent tag', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TagRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/taskRepository.test.ts b/backend/src/database/repositories/__tests__/taskRepository.test.ts deleted file mode 100644 index 746664bae0..0000000000 --- a/backend/src/database/repositories/__tests__/taskRepository.test.ts +++ /dev/null @@ -1,1269 +0,0 @@ -import moment from 'moment' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1, Error404 } from '@crowd/common' -import TaskRepository from '../taskRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import MemberRepository from '../memberRepository' -import ActivityRepository from '../activityRepository' -import lodash from 'lodash' - -const db = null - -const toCreate = { - name: 'name', - body: 'body', - type: 'regular', - status: 'done', - dueDate: new Date(), -} - -const sampleMembers = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'harry_potter', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Harry Potter', - joinedAt: new Date(), - }, - { - username: { - [PlatformType.GITHUB]: { - username: 'hermione', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Hermione Granger', - joinedAt: new Date(), - }, - { - username: { - [PlatformType.GITHUB]: { - username: 'ron_weasley', - integrationId: generateUUIDv1(), - }, - }, - displayName: 'Ron Weasley', - joinedAt: new Date(), - }, -] - -const sampleActivities = [ - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId1', - }, - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId2', - }, - { - type: 'type', - timestamp: new Date(), - platform: 'daily_prophet', - sourceId: 'sourceId3', - }, -] - -async function getToCreate(task, options, from = { fromMembers: [], fromActivities: [] }) { - const { fromMembers, fromActivities } = from - task.members = [] - task.activities = [] - task.assignees = [] - - for (const sampleMember of fromMembers) { - const cloned = lodash.cloneDeep(sampleMember) - task.members.push((await MemberRepository.create(cloned, options)).id) - } - - for (const sampleActivity of fromActivities) { - const existing = await MemberRepository.memberExists( - sampleMembers[0].username[PlatformType.GITHUB].username, - PlatformType.GITHUB, - options, - ) - - if (existing) { - sampleActivity.member = existing.id - sampleActivity.username = sampleMembers[0].username[PlatformType.GITHUB].username - } else { - const cloned = lodash.cloneDeep(sampleMembers[0]) - const member = await MemberRepository.create(cloned, options) - sampleActivity.member = member.id - sampleActivity.username = sampleMembers[0].username[PlatformType.GITHUB].username - } - - task.activities.push((await ActivityRepository.create(sampleActivity, options)).id) - } - task.assignees.push(options.currentUser.id) - return task -} - -describe('TaskRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create the given task succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - }) - - it('Should create a task with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - - // Make sure the task exists in the member - for (const memberId of createdTask.members) { - const found = await MemberRepository.findById(memberId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.activities.length).toBe(sampleActivities.length) - - // Make sure the task exists in the member - for (const activityId of createdTask.activities) { - const found = await ActivityRepository.findById(activityId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with members and activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.activities.length).toBe(sampleActivities.length) - expect(createdTask.members.length).toBe(sampleMembers.length) - }) - - it('Should create a task with a different assignee as the user creating it', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mockAssignee = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockAssignee, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.assignees[0]).toBe(mockAssignee.currentUser.id) - expect(createdTask.assignees[0]).not.toBe(mockIRepositoryOptions.currentUser.id) - }) - - it('Should throw an error when status is something not allowed', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = { - name: 'Task 2', - status: 'something', - } - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = {} - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('createSuggestedTasks method', () => { - it('Should create the static suggested tasks succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await TaskRepository.createSuggestedTasks(mockIRepositoryOptions) - - const tasks = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - expect(tasks.count).toBe(6) - - expect(tasks.rows.map((i) => i.name).sort()).toStrictEqual([ - 'Check for negative reactions', - 'Engage with relevant content', - 'Reach out to influential contacts', - 'Reach out to poorly engaged contacts', - 'Set up your team', - 'Set up your workspace integrations', - ]) - }) - - it('Should create a task with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.members.length).toBe(sampleMembers.length) - - // Make sure the task exists in the member - for (const memberId of createdTask.members) { - const found = await MemberRepository.findById(memberId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.activities.length).toBe(sampleActivities.length) - - // Make sure the task exists in the member - for (const activityId of createdTask.activities) { - const found = await ActivityRepository.findById(activityId, mockIRepositoryOptions) - expect(found.tasks[0].id).toBe(createdTask.id) - } - }) - - it('Should create a task with members and activities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: sampleMembers, - fromActivities: sampleActivities, - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const clone1 = { ...createdTask } - const clone2 = { ...expectedTaskCreated } - delete clone1.members - delete clone2.members - expect(clone1).toStrictEqual(clone2) - expect(createdTask.members.sort()).toEqual(expectedTaskCreated.members.sort()) - expect(createdTask.activities.length).toBe(sampleActivities.length) - expect(createdTask.members.length).toBe(sampleMembers.length) - }) - - it('Should create a task with a different assignee as the user creating it', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mockAssignee = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockAssignee, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskCreated = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - expect(createdTask).toStrictEqual(expectedTaskCreated) - expect(createdTask.assignees[0]).toBe(mockAssignee.currentUser.id) - expect(createdTask.assignees[0]).not.toBe(mockIRepositoryOptions.currentUser.id) - }) - - it('Should throw an error when status is something not allowed', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = { - name: 'Task 2', - status: 'something', - } - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - - it('Should throw sequelize not null error -- name field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task2add = {} - - await expect(() => TaskRepository.create(task2add, mockIRepositoryOptions)).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created task by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - createdTask.createdAt = createdTask.createdAt.toISOString().split('T')[0] - createdTask.updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - createdTask.members = createdTask.members.map((member) => member.id) - createdTask.activities = createdTask.activities.map((activity) => activity.id) - createdTask.assignees = createdTask.assignees.map((assignee) => assignee.id) - - const expectedTaskFound = { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const taskById = await TaskRepository.findById(createdTask.id, mockIRepositoryOptions) - - taskById.createdAt = taskById.createdAt.toISOString().split('T')[0] - taskById.updatedAt = taskById.updatedAt.toISOString().split('T')[0] - taskById.assignees = taskById.assignees.map((assignee) => assignee.id) - - expect(taskById).toStrictEqual(expectedTaskFound) - }) - - it('Should throw 404 error when no task found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created task entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task1 = { name: 'test1' } - const task2 = { name: 'test2' } - - const task1Created = await TaskRepository.create(task1, mockIRepositoryOptions) - const task2Created = await TaskRepository.create(task2, mockIRepositoryOptions) - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [task1Created.id, task2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([task1Created.id, task2Created.id]) - }) - - it('Should only return the ids of previously created tasks and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test1' } - - const taskCreated = await TaskRepository.create(task, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [taskCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([taskCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test' } - - const taskCreated = await TaskRepository.create(task, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await TaskRepository.filterIdsInTenant( - [taskCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task 2', - type: 'regular', - status: 'done', - }, - mockIRepositoryOptions, - ) - const createdTask = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - const createdTask2 = await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - found.rows[1].createdAt = createdTask.createdAt.toISOString().split('T')[0] - found.rows[1].updatedAt = createdTask.updatedAt.toISOString().split('T')[0] - - found.rows[1].members = createdTask.members.map((member) => member.id) - found.rows[1].activities = createdTask.activities.map((activity) => activity.id) - found.rows[1].assignees = createdTask.assignees.map((assignee) => assignee.id) - - found.rows[0].createdAt = createdTask2.createdAt.toISOString().split('T')[0] - found.rows[0].updatedAt = createdTask2.updatedAt.toISOString().split('T')[0] - - found.rows[0].members = createdTask2.members.map((member) => member.id) - found.rows[0].activities = createdTask2.activities.map((activity) => activity.id) - found.rows[0].assignees = createdTask2.assignees.map((assignee) => assignee.id) - - expect(found).toStrictEqual({ - rows: [ - { - id: createdTask2.id, - ...toCreate2, - body: null, - dueDate: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - }, - { - id: createdTask.id, - ...toCreate1, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - }, - ], - count: 2, - limit: 10, - offset: 0, - }) - }) - - describe('filter', () => { - it('by name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'done', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { name: 'Task' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe('Task') - }) - - it('by type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Suggested task', - type: 'suggested', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { type: 'suggested' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe('Suggested task') - }) - - it('by status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - }, - mockIRepositoryOptions, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { filter: { status: 'done' } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].status).toBe('done') - }) - - it('by assignees', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - }, - options2, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const toFilter = options2.currentUser.id.toString() - - const found = await TaskRepository.findAndCountAll( - { filter: { assignees: [toFilter] } }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].assignees[0].id).toBe(options2.currentUser.id) - }) - - it('by dueDate', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - options2, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const found = await TaskRepository.findAndCountAll( - { - filter: { - dueDateRange: [moment().startOf('day').toDate(), moment().endOf('day').toDate()], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - expect(found.rows[0].name).toBe(toCreate1.name) - }) - - it('by members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [sampleMembers[1], sampleMembers[2]], - fromActivities: [], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const member = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows[0] - - const toFilter = [member.id.toString()] - - const found = await TaskRepository.findAndCountAll( - { - filter: { - members: toFilter, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const members = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const m0Id = members.filter((m) => m.displayName === sampleMembers[0].displayName)[0].id - - const m1Id = members.filter((m) => m.displayName === sampleMembers[1].displayName)[0].id - const found2 = await TaskRepository.findAndCountAll( - { - filter: { - members: [m0Id, m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - }) - - it('by activity', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[0]], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [], - fromActivities: [sampleActivities[1], sampleActivities[2]], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - - const act = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows[0] - - const toFilter = [act.id.toString()] - - const found = await TaskRepository.findAndCountAll( - { - filter: { - activities: toFilter, - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const activities = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const a0Id = activities.filter((a) => a.sourceId === sampleActivities[0].sourceId)[0].id - - const a1Id = activities.filter((a) => a.sourceId === sampleActivities[1].sourceId)[0].id - - const found2 = await TaskRepository.findAndCountAll( - { - filter: { - activities: [a0Id, a1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - }) - }) - - it('by activities and members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [sampleActivities[0]], - }) - const toCreate2 = await getToCreate( - { - name: 'Task', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [sampleMembers[1]], - fromActivities: [sampleActivities[1]], - }, - ) - const toCreate3 = await getToCreate( - { - name: 'Task 3', - status: 'in-progress', - dueDate: moment().add(1, 'day').toDate(), - }, - mockIRepositoryOptions, - { - fromMembers: [], - fromActivities: [sampleActivities[2]], - }, - ) - await TaskRepository.create(toCreate1, mockIRepositoryOptions) - await TaskRepository.create(toCreate2, mockIRepositoryOptions) - await TaskRepository.create(toCreate3, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const members = ( - await MemberRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const m1Id = members.filter((m) => m.displayName === sampleMembers[1].displayName)[0].id - - const activities = ( - await ActivityRepository.findAndCountAll( - { - filter: {}, - }, - mockIRepositoryOptions, - ) - ).rows - - const a1Id = activities.filter((a) => a.sourceId === sampleActivities[1].sourceId)[0].id - const a2Id = activities.filter((a) => a.sourceId === sampleActivities[2].sourceId)[0].id - - const found = await TaskRepository.findAndCountAll( - { - filter: { - activities: [a1Id], - members: [m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found.count).toBe(1) - - const found2 = await TaskRepository.findAndCountAll( - { - advancedFilter: { - or: [ - { - activities: [a2Id], - }, - { - members: [m1Id], - }, - ], - }, - }, - mockIRepositoryOptions, - ) - expect(found2.count).toBe(2) - - const found3 = await TaskRepository.findAndCountAll( - { - advancedFilter: { - activities: [a1Id], - members: [m1Id], - }, - }, - mockIRepositoryOptions, - ) - expect(found3.count).toBe(1) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { name: 'updated-task-name' }, - mockIRepositoryOptions, - ) - - expect(taskUpdated.updatedAt.getTime()).toBeGreaterThan(taskCreated.createdAt.getTime()) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - const taskExpected = { - id: taskCreated.id, - ...taskUpdated, - name: taskUpdated.name, - createdAt: taskUpdated.createdAt, - updatedAt: taskUpdated.updatedAt, - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - members: [], - } - - expect(taskUpdated).toStrictEqual(taskExpected) - }) - - it('Should succesfully update members related to the task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[0]], - fromActivities: [], - }) - - const newMembers = ( - await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [sampleMembers[1]], - fromActivities: [], - }) - ).members - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { members: newMembers }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.members.length).toBe(1) - expect(taskUpdated.members[0].id).toStrictEqual(newMembers[0]) - }) - - it('Should succesfully update activities related to the task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[0]], - }) - - const newActivities = ( - await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [sampleActivities[1]], - }) - ).activities - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { activities: newActivities }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.activities.length).toBe(1) - expect(taskUpdated.activities[0].id).toStrictEqual(newActivities[0]) - }) - - it('Should succesfully update assignees', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const toCreate1 = await getToCreate(toCreate, mockIRepositoryOptions, { - fromMembers: [], - fromActivities: [], - }) - - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const taskCreated = await TaskRepository.create(toCreate1, mockIRepositoryOptions) - - const toUpdate = options2.currentUser.id.toString() - - const taskUpdated = await TaskRepository.update( - taskCreated.id, - { assignees: [toUpdate] }, - mockIRepositoryOptions, - ) - - taskUpdated.createdAt = taskUpdated.createdAt.toISOString().split('T')[0] - taskUpdated.updatedAt = taskUpdated.updatedAt.toISOString().split('T')[0] - - expect(taskUpdated.assignees.map((i) => i.id)).toStrictEqual([toUpdate]) - }) - - it('Should throw 404 error when trying to update non existent task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.update(randomUUID(), { name: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const task = { name: 'test-task' } - - const returnedTask = await TaskRepository.create(task, mockIRepositoryOptions) - - await TaskRepository.destroy(returnedTask.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - TaskRepository.findById(returnedTask.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent task', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - TaskRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('updateBulk method', () => { - it('Should succesfully bulk update given tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let task1 = await TaskRepository.create( - { name: 'test-task', status: 'in-progress' }, - mockIRepositoryOptions, - ) - let task2 = await TaskRepository.create( - { name: 'test-task-2', status: 'in-progress' }, - mockIRepositoryOptions, - ) - let task3 = await TaskRepository.create( - { name: 'test-task-3', status: 'archived' }, - mockIRepositoryOptions, - ) - - let result = await TaskRepository.updateBulk( - [task1.id, task2.id], - { status: 'done' }, - mockIRepositoryOptions, - ) - - expect(result.rowsUpdated).toBe(2) - - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - expect(task1.status).toStrictEqual('done') - expect(task2.status).toStrictEqual('done') - expect(task3.status).toStrictEqual('archived') - - result = await TaskRepository.updateBulk( - [task1.id, task2.id, task3.id], - { status: 'in-progress' }, - mockIRepositoryOptions, - ) - - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - expect(task1.status).toStrictEqual('in-progress') - expect(task2.status).toStrictEqual('in-progress') - expect(task3.status).toStrictEqual('in-progress') - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/tenantRepository.test.ts b/backend/src/database/repositories/__tests__/tenantRepository.test.ts deleted file mode 100644 index 7e29fa2497..0000000000 --- a/backend/src/database/repositories/__tests__/tenantRepository.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -import TenantRepository from '../tenantRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Plans from '../../../security/plans' - -const db = null - -describe('TenantRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('getPayingTenantIds method', () => { - it('should return tenants not using the essential plan', async () => { - const ToCreatePLanForEssentialPlanTenantOnTrial = { - name: 'essential tenant name', - url: 'an-essential-tenant-name', - plan: Plans.values.essential, - } - const ToCreatPlanForGrowthTenantOnTrial = { - name: 'growth tenant name', - url: 'a-growth-tenant-name', - plan: Plans.values.growth, - } - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await options.database.tenant.create(ToCreatePLanForEssentialPlanTenantOnTrial) - const growthTenant = await options.database.tenant.create(ToCreatPlanForGrowthTenantOnTrial) - const tenantIds = await TenantRepository.getPayingTenantIds(options) - - expect(tenantIds).toHaveLength(1) - expect(growthTenant.id).toStrictEqual(tenantIds[0].id) - }) - }) - - describe('generateTenantUrl method', () => { - it('Should generate a url from name - 0 existing tenants with same url', async () => { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantName = 'some tenant Name with !@#_% non-alphanumeric characters' - - const generatedUrl = await TenantRepository.generateTenantUrl(tenantName, options) - const expectedGeneratedUrl = 'some-tenant-name-with-non-alphanumeric-characters' - - expect(generatedUrl).toStrictEqual(expectedGeneratedUrl) - }) - - it('Should generate a url from name - with existing tenant that has the same url', async () => { - const options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantName = 'a tenant name' - - // create a tenant with url 'a-tenant-name' - await options.database.tenant.create({ - name: tenantName, - url: 'a-tenant-name', - plan: Plans.values.essential, - }) - - // now generate function should return 'a-tenant-name-1' because it already exists - const generatedUrl = await TenantRepository.generateTenantUrl(tenantName, options) - - const expectedGeneratedUrl = 'a-tenant-name-1' - - expect(generatedUrl).toStrictEqual(expectedGeneratedUrl) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/userRepository.test.ts b/backend/src/database/repositories/__tests__/userRepository.test.ts deleted file mode 100644 index b46045721f..0000000000 --- a/backend/src/database/repositories/__tests__/userRepository.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Error404 } from '@crowd/common' -import UserRepository from '../userRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' -import Roles from '../../../security/roles' - -const db = null - -describe('UserRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('findAllUsersOfTenant method', () => { - it('Should find all related users of a tenant successfully', async () => { - // Getting options already creates one random user - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - let allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - ]) - - // add more users to the test tenant - const randomUser2 = await SequelizeTestUtils.getRandomUser() - const user2 = await mockIRepositoryOptions.database.user.create(randomUser2) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - ]) - - const randomUser3 = await SequelizeTestUtils.getRandomUser() - const user3 = await mockIRepositoryOptions.database.user.create(randomUser3) - - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user3.id, - }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - user3.get({ plain: true }), - ]) - - // add other users and tenants that are non related to previous couples - await SequelizeTestUtils.getTestIRepositoryOptions(db) - - // users of the previous tenant should be the same - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - user3.get({ plain: true }), - ]) - - const tenantUsers = await mockIRepositoryOptions.database.tenantUser.findAll({ - tenantId: mockIRepositoryOptions.currentTenant.id, - }) - - // remove last user added to the tenant - await tenantUsers[2].destroy({ force: true }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([ - mockIRepositoryOptions.currentUser.get({ plain: true }), - user2.get({ plain: true }), - ]) - - // remove first user added to the tenant - await tenantUsers[0].destroy({ force: true }) - - allUsersOfTenant = ( - await UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id) - ).map((u) => SequelizeTestUtils.objectWithoutKey(u, 'tenants')) - - expect(allUsersOfTenant).toStrictEqual([user2.get({ plain: true })]) - - // remove the last remaining user from the tenant - await tenantUsers[1].destroy({ force: true }) - - // function now should be throwing Error404 - await expect(() => - UserRepository.findAllUsersOfTenant(mockIRepositoryOptions.currentTenant.id), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/__tests__/widgetRepository.test.ts b/backend/src/database/repositories/__tests__/widgetRepository.test.ts deleted file mode 100644 index 2ea97ad615..0000000000 --- a/backend/src/database/repositories/__tests__/widgetRepository.test.ts +++ /dev/null @@ -1,573 +0,0 @@ -import { Error404 } from '@crowd/common' -import WidgetRepository from '../widgetRepository' -import ReportRepository from '../reportRepository' -import SequelizeTestUtils from '../../utils/sequelizeTestUtils' - -const db = null - -describe('WidgetRepository tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('create method', () => { - it('Should create a widget succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = { type: 'test-widget' } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - report: null, - } - - expect(widgetCreated).toStrictEqual(widgetExpected) - }) - - it('Should create a widget succesfully with given values -- without report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = { - type: 'test-widget', - title: 'Activities by Date', - settings: { - chartType: 'line', - query: { - measures: ['Activities.activityCount'], - timeDimensions: [ - { - dimension: 'Activities.date', - granularity: 'week', - dateRange: 'Last 30 days', - }, - ], - limit: 10000, - }, - layout: { - x: 6, - y: 0, - w: 6, - h: 18, - i: '620d303b0895bb8bee0a7e24', - moved: false, - }, - }, - } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - // Trim the report object, we'll only expect the reportId - const { report: _reportObj, ...widgetWithoutReport } = widgetCreated - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: widget2Add.title, - settings: widget2Add.settings, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should create a widget succesfully with given values -- with report', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report2Add = { - name: 'test-report', - public: true, - } - - const reportCreated = await ReportRepository.create(report2Add, mockIRepositoryOptions) - - const widget2Add = { - type: 'test-widget', - title: 'Activities by Date', - report: reportCreated.id, - settings: { - chartType: 'line', - query: { - measures: ['Activities.activityCount'], - timeDimensions: [ - { - dimension: 'Activities.date', - granularity: 'week', - dateRange: 'Last 30 days', - }, - ], - limit: 10000, - }, - layout: { - x: 6, - y: 0, - w: 6, - h: 18, - i: '620d303b0895bb8bee0a7e24', - moved: false, - }, - }, - } - - const widgetCreated = await WidgetRepository.create(widget2Add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - // Trim the report object, we'll only expect the reportId - const { report: _reportObj, ...widgetWithoutReport } = widgetCreated - - const widgetExpected = { - id: widgetCreated.id, - type: widget2Add.type, - title: widget2Add.title, - settings: widget2Add.settings, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: reportCreated.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw sequelize not null error -- type field is required', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2Add = {} - - await expect(() => - WidgetRepository.create(widget2Add, mockIRepositoryOptions), - ).rejects.toThrow() - }) - }) - - describe('findById method', () => { - it('Should successfully find created widget by id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget2add = { type: 'test-widget' } - - const widgetCreated = await WidgetRepository.create(widget2add, mockIRepositoryOptions) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widget2add.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const widgetById = await WidgetRepository.findById(widgetCreated.id, mockIRepositoryOptions) - - widgetById.createdAt = widgetById.createdAt.toISOString().split('T')[0] - widgetById.updatedAt = widgetById.updatedAt.toISOString().split('T')[0] - - const { report: _reportObj, ...widgetWithoutReport } = widgetById - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw 404 error when no widget found with given id', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.findById(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('findByType method', () => { - it('Should successfully find one widget by type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widgetCreated = await WidgetRepository.create( - { type: 'test-widget' }, - mockIRepositoryOptions, - ) - - widgetCreated.createdAt = widgetCreated.createdAt.toISOString().split('T')[0] - widgetCreated.updatedAt = widgetCreated.updatedAt.toISOString().split('T')[0] - - const widgetExpected = { - id: widgetCreated.id, - type: widgetCreated.type, - title: null, - settings: null, - cache: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - reportId: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - const widgetByType = await WidgetRepository.findByType( - widgetCreated.type, - mockIRepositoryOptions, - ) - - widgetByType.createdAt = widgetByType.createdAt.toISOString().split('T')[0] - widgetByType.updatedAt = widgetByType.updatedAt.toISOString().split('T')[0] - - const { report: _reportObj, ...widgetWithoutReport } = widgetByType - - expect(widgetWithoutReport).toStrictEqual(widgetExpected) - }) - - it('Should throw 404 error when no widget found with given type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await WidgetRepository.create({ type: 'some-type' }, mockIRepositoryOptions) - - await expect(() => - WidgetRepository.findByType('some-other-type', mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('filterIdsInTenant method', () => { - it('Should return the given ids of previously created widget entities', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget1 = { type: 'widget-test1' } - const widget2 = { type: 'widget-test2' } - - const widget1Created = await WidgetRepository.create(widget1, mockIRepositoryOptions) - const widget2Created = await WidgetRepository.create(widget2, mockIRepositoryOptions) - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widget1Created.id, widget2Created.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([widget1Created.id, widget2Created.id]) - }) - - it('Should only return the ids of previously created widgets and filter random uuids out', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widgetCreated.id, randomUUID(), randomUUID()], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([widgetCreated.id]) - }) - - it('Should return an empty array for an irrelevant tenant', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget, mockIRepositoryOptions) - - // create a new tenant and bind options to it - mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const filterIdsReturned = await WidgetRepository.filterIdsInTenant( - [widgetCreated.id], - mockIRepositoryOptions, - ) - - expect(filterIdsReturned).toStrictEqual([]) - }) - }) - - describe('findAndCountAll method', () => { - it('Should find and count all widgets, with various filters', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const report1 = { name: 'test-report', public: true } - const report2 = { name: 'test-report', public: true } - - const report1Created = await ReportRepository.create(report1, mockIRepositoryOptions) - const report2Created = await ReportRepository.create(report2, mockIRepositoryOptions) - - const widget1 = { - title: 'Number of activities - graph', - type: 'number-activities-graph', - report: report1Created.id, - settings: { - l1_settings: { - l2_settings: { - values: ['test2'], - }, - values: ['test1'], - }, - }, - } - - const widget2 = { - title: 'Time to first interaction - graph', - type: 'time-to-first-interaction-graph', - report: report1Created.id, - settings: { - l1_settings: { - l2_settings: { - values: ['test2'], - }, - values: ['test1'], - }, - }, - } - - const widget3 = { - title: 'Some cubejs widget', - type: 'cubejs', - report: report2Created.id, - } - const widget4 = { type: 'number-activities' } - - const widget1Created = await WidgetRepository.create(widget1, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget2Created = await WidgetRepository.create(widget2, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget3Created = await WidgetRepository.create(widget3, mockIRepositoryOptions) - await new Promise((resolve) => { - setTimeout(resolve, 50) - }) - - const widget4Created = await WidgetRepository.create(widget4, mockIRepositoryOptions) - - // Filter by type - // Current findAndCountAll uses wildcarded like statement so it matches both widget1 and widget4 - let widgets = await WidgetRepository.findAndCountAll( - { filter: { type: 'number-activities' } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget4Created, widget1Created]) - - // Filter by id - widgets = await WidgetRepository.findAndCountAll( - { filter: { id: widget1Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget1Created]) - - // Filter by createdAt - find all between widget1.createdAt and widget3.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [widget1Created.createdAt, widget3Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(3) - expect(widgets.rows).toStrictEqual([widget3Created, widget2Created, widget1Created]) - - // Filter by createdAt - find all where createdAt < widget2.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, widget2Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by createdAt - find all where createdAt < widget1.createdAt - widgets = await WidgetRepository.findAndCountAll( - { - filter: { - createdAtRange: [null, widget1Created.createdAt], - }, - }, - mockIRepositoryOptions, - ) - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget1Created]) - - // Filter by title - widgets = await WidgetRepository.findAndCountAll( - { filter: { title: 'graph' } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by report1 - widgets = await WidgetRepository.findAndCountAll( - { filter: { report: report1Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(2) - expect(widgets.rows).toStrictEqual([widget2Created, widget1Created]) - - // Filter by report2 - widgets = await WidgetRepository.findAndCountAll( - { filter: { report: report2Created.id } }, - mockIRepositoryOptions, - ) - - expect(widgets.count).toEqual(1) - expect(widgets.rows).toStrictEqual([widget3Created]) - }) - }) - - describe('update method', () => { - it('Should succesfully update previously created widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget1 = { type: 'widget-test' } - - const widgetCreated = await WidgetRepository.create(widget1, mockIRepositoryOptions) - - // Test adding report to widget on update as well - const report = { name: 'test-report', public: true } - - const reportCreated = await ReportRepository.create(report, mockIRepositoryOptions) - - const widgetUpdated = await WidgetRepository.update( - widgetCreated.id, - { - type: 'updated-widget-type', - title: 'new-title', - report: reportCreated.id, - }, - mockIRepositoryOptions, - ) - - expect(widgetUpdated.updatedAt.getTime()).toBeGreaterThan(widgetUpdated.createdAt.getTime()) - - const widgetExcpected = { - id: widgetCreated.id, - type: widgetUpdated.type, - title: widgetUpdated.title, - settings: null, - cache: null, - importHash: null, - createdAt: widgetCreated.createdAt, - updatedAt: widgetUpdated.updatedAt, - deletedAt: null, - reportId: reportCreated.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - const { report: _reportObj, ...widgetWithoutReport } = widgetUpdated - - expect(widgetWithoutReport).toStrictEqual(widgetExcpected) - }) - - it('Should throw 404 error when trying to update non existent widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.update(randomUUID(), { type: 'non-existent' }, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('destroy method', () => { - it('Should succesfully destroy previously created widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const widget = { - type: 'integrations', - title: 'Metric graph', - } - - const returnedWidget = await WidgetRepository.create(widget, mockIRepositoryOptions) - - await WidgetRepository.destroy(returnedWidget.id, mockIRepositoryOptions, true) - - // Try selecting it after destroy, should throw 404 - await expect(() => - WidgetRepository.findById(returnedWidget.id, mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - - it('Should throw 404 when trying to destroy a non existent widget', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const { randomUUID } = require('crypto') - - await expect(() => - WidgetRepository.destroy(randomUUID(), mockIRepositoryOptions), - ).rejects.toThrowError(new Error404()) - }) - }) -}) diff --git a/backend/src/database/repositories/filters/__tests__/queryParser.test.ts b/backend/src/database/repositories/filters/__tests__/queryParser.test.ts deleted file mode 100644 index d73a7ea3d5..0000000000 --- a/backend/src/database/repositories/filters/__tests__/queryParser.test.ts +++ /dev/null @@ -1,555 +0,0 @@ -import Sequelize from 'sequelize' -import { generateUUIDv4 as uuid } from '@crowd/common' -import SequelizeTestUtils from '../../../utils/sequelizeTestUtils' -import QueryParser from '../queryParser' - -const { Op } = Sequelize -const db = null - -describe('QueryParser tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('Simple tests', () => { - it('With empty values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With some filtering values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - or: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - [Op.or]: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With some sorting values: list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: ['timestamp_DESC', 'channel_ASC'], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With some sorting values: string', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: 'timestamp_DESC,channel_ASC', - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With offset', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - offset: 10, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 10, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With limit', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - limit: 100, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 100, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With too large limit', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: {}, - orderBy: [], - limit: 210, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 200, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - - it('With filtering, sorting, limit and offset', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - or: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - orderBy: ['timestamp_DESC', 'channel_ASC'], - limit: 100, - offset: 10, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - [Op.or]: [{ channel: 'dev' }, { channel: 'bugs' }], - }, - limit: 100, - offset: 10, - order: [ - ['timestamp', 'DESC'], - ['channel', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - }) - - describe('Complex filtering tests', () => { - it('With nested fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - nestedFields: { - sentiment: 'sentiment.sentiment', - mood: 'sentiment.mood', - }, - }, - mockIRepositoryOptions, - ) - const parsed = parser.parse({ - filter: { - sentiment: { - gte: 0.5, - }, - or: [{ mood: 'happy' }, { mood: 'sad' }], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - 'sentiment.sentiment': { - [Op.gte]: 0.5, - }, - [Op.or]: [{ 'sentiment.mood': 'happy' }, { 'sentiment.mood': 'sad' }], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With complex operators', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser({}, mockIRepositoryOptions) - const parsed = parser.parse({ - filter: { - body: { - textContains: 'test', - }, - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - body: { - [Op.iLike]: '%test%', - }, - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With aggregators', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - aggregators: { - count: mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - platform: Sequelize.literal(`"activities"."platform"`), - }, - }, - mockIRepositoryOptions, - ) - const parsed = parser.parse({ - filter: { - or: [ - { - platform: { - in: ['discord', 'github'], - }, - }, - { - count: { - gte: 10, - }, - }, - ], - }, - orderBy: [], - }) - const expected = { - having: { - [Op.or]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"activities"."platform"`), - Op.in, - Sequelize.literal(`('discord','github')`), - ), - ], - }, - { - [Op.and]: [ - Sequelize.where( - mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - Op.gte, - 10, - ), - ], - }, - ], - }, - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With many to many relations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - manyToMany: { - members: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'memberTasks', - from: 'taskId', - to: 'memberId', - }, - }, - activities: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'activityTasks', - from: 'taskId', - to: 'activityId', - }, - }, - }, - }, - mockIRepositoryOptions, - ) - - const aid1 = uuid() - const aid2 = uuid() - const mid1 = uuid() - - const parsed = parser.parse({ - filter: { - or: [ - { - members: [mid1], - activities: [aid1, aid2], - }, - { - status: { - eq: 'some-task-name', - }, - }, - ], - }, - orderBy: [], - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - [Op.or]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "memberTasks" ON "memberTasks"."taskId" = "tasks"."id" WHERE "memberTasks"."memberId" = '${mid1}')`, - ), - ), - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "activityTasks" ON "activityTasks"."taskId" = "tasks"."id" WHERE "activityTasks"."activityId" = '${aid1}' OR "activityTasks"."activityId" = '${aid2}')`, - ), - ), - ], - }, - { - status: { - [Op.eq]: 'some-task-name', - }, - }, - ], - }, - limit: 10, - offset: 0, - order: [], - } - expect(parsed).toStrictEqual(expected) - }) - it('With nested fields, complex operators, aggregators and many to many', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const parser = new QueryParser( - { - nestedFields: { - sentiment: 'sentiment.sentiment', - mood: 'sentiment.mood', - }, - aggregators: { - count: mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - platform: Sequelize.literal(`"activities"."platform"`), - }, - manyToMany: { - members: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'memberTasks', - from: 'taskId', - to: 'memberId', - }, - }, - activities: { - table: 'tasks', - model: 'task', - relationTable: { - name: 'activityTasks', - from: 'taskId', - to: 'activityId', - }, - }, - }, - }, - mockIRepositoryOptions, - ) - - const aid1 = uuid() - const aid2 = uuid() - - const parsed = parser.parse({ - filter: { - sentiment: { - gte: 0.5, - }, - or: [ - { - description: { - textContains: 'test', - }, - }, - { - and: [ - { - platform: { - in: ['discord', 'github'], - }, - }, - { - count: { - lt: 10, - }, - }, - ], - }, - { - activities: [aid1, aid2], - }, - ], - }, - orderBy: ['dueDate_DESC', 'createdAt_ASC'], - offset: 102, - limit: 101, - }) - const expected = { - where: { - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments.map((s) => s.id), - }, - having: { - 'sentiment.sentiment': { - [Op.gte]: 0.5, - }, - [Op.or]: [ - { - description: { - [Op.iLike]: '%test%', - }, - }, - { - [Op.and]: [ - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"activities"."platform"`), - Op.in, - Sequelize.literal(`('discord','github')`), - ), - ], - }, - { - [Op.and]: [ - Sequelize.where( - mockIRepositoryOptions.database.Sequelize.fn( - 'COUNT', - mockIRepositoryOptions.database.Sequelize.col('activities.id'), - ), - Op.lt, - 10, - ), - ], - }, - ], - }, - { - [Op.and]: [ - Sequelize.where( - Sequelize.literal(`"task"."id"`), - Op.in, - Sequelize.literal( - `(SELECT "tasks"."id" FROM "tasks" INNER JOIN "activityTasks" ON "activityTasks"."taskId" = "tasks"."id" WHERE "activityTasks"."activityId" = '${aid1}' OR "activityTasks"."activityId" = '${aid2}')`, - ), - ), - ], - }, - ], - }, - limit: 101, - offset: 102, - order: [ - ['dueDate', 'DESC'], - ['createdAt', 'ASC'], - ], - } - expect(parsed).toStrictEqual(expected) - }) - }) -}) diff --git a/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts b/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts deleted file mode 100644 index c8612f504a..0000000000 --- a/backend/src/database/repositories/filters/__tests__/rawQueryParser.test.ts +++ /dev/null @@ -1,300 +0,0 @@ -import { MemberAttributeType } from '@crowd/types' -import MemberRepository from '../../memberRepository' -import RawQueryParser from '../rawQueryParser' - -describe('RawQueryParser', () => { - it('Should parse simple filter with an empty second operand', () => { - const basicFilter = { - and: [ - { - isOrganization: { - not: true, - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - basicFilter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - "((coalesce((m.attributes -> 'isOrganization' -> 'default')::boolean, false) <> :isOrganization_1) and (1=1))", - ) - expect(params.isOrganization_1).toEqual(true) - }) - - it('Should parse simple default filter', () => { - const basicFilter = { - and: [ - { - isOrganization: { - not: true, - }, - }, - { - isBot: { - eq: false, - }, - }, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - basicFilter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - "((coalesce((m.attributes -> 'isOrganization' -> 'default')::boolean, false) <> :isOrganization_1) and (coalesce((m.attributes -> 'isBot' -> 'default')::boolean, false) = :isBot_1))", - ) - expect(params.isOrganization_1).toEqual(true) - expect(params.isBot_1).toEqual(false) - }) - - it('Should parse filter with a between condition', () => { - const filter = { - and: [ - { - activityCount: { - between: [10, 100], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual( - `((aggs."activityCount" between :activityCount_1 and :activityCount_2) and (1=1))`, - ) - expect(params.activityCount_1).toEqual(10) - expect(params.activityCount_2).toEqual(100) - }) - - it('Should parse filter with a contains condition', () => { - const filter = { - and: [ - { - identities: { - contains: ['github', 'slack'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((aggs.identities @> array[:identities_1, :identities_2]) and (1=1))`) - expect(params.identities_1).toEqual('github') - expect(params.identities_2).toEqual('slack') - }) - - it('Should parse filter with an overlap condition', () => { - const filter = { - and: [ - { - identities: { - overlap: ['github', 'slack'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((aggs.identities && array[:identities_1, :identities_2]) and (1=1))`) - expect(params.identities_1).toEqual('github') - expect(params.identities_2).toEqual('slack') - }) - - it('Should parse filter with an in condition', () => { - const filter = { - and: [ - { - emails: { - in: ['crash@crowd.dev', 'burn@crowd.dev'], - }, - }, - {}, - ], - } - - const params: any = {} - - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((m.emails in (:emails_1, :emails_2)) and (1=1))`) - expect(params.emails_1).toEqual('crash@crowd.dev') - expect(params.emails_2).toEqual('burn@crowd.dev') - }) - - it('Should parse filter with attribute column multiselect filter', () => { - const filter = { - and: [ - { - 'attributes.skills.default': { - contains: ['javascript', 'typescript'], - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'attributes', - column: 'm.attributes', - attributeInfos: [ - { - name: 'skills', - type: MemberAttributeType.MULTI_SELECT, - }, - ], - }, - ], - params, - ) - - expect(result).toEqual( - `(((m.attributes -> 'skills' -> 'default') ?& array[:attributes_skills_default_1, :attributes_skills_default_2]) and (1=1))`, - ) - expect(params.attributes_skills_default_1).toEqual('javascript') - expect(params.attributes_skills_default_2).toEqual('typescript') - }) - - it('Should parse filter with attribute column number filter', () => { - const filter = { - and: [ - { - 'attributes.age.default': { - between: [20, 30], - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'attributes', - column: 'm.attributes', - attributeInfos: [ - { - name: 'age', - type: MemberAttributeType.NUMBER, - }, - ], - }, - ], - params, - ) - - expect(result).toEqual( - `(((m.attributes -> 'age' -> 'default')::integer between :attributes_age_default_1 and :attributes_age_default_2) and (1=1))`, - ) - expect(params.attributes_age_default_1).toEqual(20) - expect(params.attributes_age_default_2).toEqual(30) - }) - - it('Should parse filter with json column array filter', () => { - const filter = { - and: [ - { - tags: ['c194036e-cf7c-4353-ae16-e8572a208f51'], - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [ - { - property: 'tags', - column: 'mt.all_ids', - attributeInfos: [], - }, - ], - params, - ) - - expect(result).toEqual(`(((mt.all_ids) ?& array[:tags_1]) and (1=1))`) - expect(params.tags_1).toEqual('c194036e-cf7c-4353-ae16-e8572a208f51') - }) - - it('Should parse filter with not operator', () => { - const filter = { - and: [ - { - not: { - displayName: { - textContains: 'test', - }, - }, - }, - {}, - ], - } - - const params: any = {} - const result = RawQueryParser.parseFilters( - filter, - MemberRepository.MEMBER_QUERY_FILTER_COLUMN_MAP, - [], - params, - ) - - expect(result).toEqual(`((not (m."displayName" ilike :displayName_1)) and (1=1))`) - expect(params.displayName_1).toEqual('%test%') - }) -}) diff --git a/backend/src/database/repositories/integrationRepository.ts b/backend/src/database/repositories/integrationRepository.ts index de882ba681..a1f5dfc44c 100644 --- a/backend/src/database/repositories/integrationRepository.ts +++ b/backend/src/database/repositories/integrationRepository.ts @@ -258,6 +258,13 @@ class IntegrationRepository { status: 'done', platform, }, + include: [ + { + model: options.database.tenant, + as: 'tenant', + required: true, + }, + ], limit: perPage, offset: (page - 1) * perPage, order: [['id', 'ASC']], diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 6a31d8b7d2..c60a09450d 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -48,6 +48,7 @@ import { import OrganizationRepository from './organizationRepository' import MemberSyncRemoteRepository from './memberSyncRemoteRepository' import MemberAffiliationRepository from './memberAffiliationRepository' +import MemberAttributeSettingsRepository from './memberAttributeSettingsRepository' const { Op } = Sequelize @@ -421,7 +422,7 @@ class MemberRepository { const query = ` INSERT INTO "memberToMerge" ("memberId", "toMergeId", "similarity", "createdAt", "updatedAt") - VALUES ${placeholders.join(', ')}; + VALUES ${placeholders.join(', ')} on conflict do nothing; ` try { await seq.query(query, { @@ -1166,6 +1167,39 @@ class MemberRepository { }) } + static async findByIdOpensearch(id, options: IRepositoryOptions, segmentId?: string) { + const segments = segmentId ? [segmentId] : SequelizeRepository.getSegmentIds(options) + + const memberAttributeSettings = ( + await MemberAttributeSettingsRepository.findAndCountAll({}, options) + ).rows + + const response = await this.findAndCountAllOpensearch( + { + filter: { + and: [ + { + id: { + eq: id, + }, + }, + ], + }, + limit: 1, + offset: 0, + attributesSettings: memberAttributeSettings, + segments, + }, + options, + ) + + if (response.count === 0) { + throw new Error404() + } + + return response.rows[0] + } + static async findAndCountActiveOpensearch( filter: IActiveMemberFilter, limit: number, diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index ad478a345f..065f771c00 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -40,6 +40,7 @@ interface IOrganizationPartialAggregatesOpensearch { string_name: string }[] uuid_arr_noMergeIds: string[] + keyword_displayName: string } } @@ -49,6 +50,7 @@ interface ISimilarOrganization { uuid_organizationId: string nested_identities: IOrganizationIdentityOpensearch[] nested_weakIdentities: IOrganizationIdentityOpensearch[] + keyword_displayName: string } } @@ -517,7 +519,64 @@ class OrganizationRepository { }) } - static async update(id, data, options: IRepositoryOptions, overrideIdentities = false) { + static ORGANIZATION_UPDATE_COLUMNS = [ + 'displayName', + 'description', + 'emails', + 'phoneNumbers', + 'logo', + 'tags', + 'website', + 'location', + 'github', + 'twitter', + 'linkedin', + 'crunchbase', + 'employees', + 'revenueRange', + 'importHash', + 'isTeamOrganization', + 'employeeCountByCountry', + 'type', + 'ticker', + 'headline', + 'profiles', + 'naics', + 'industry', + 'founded', + 'size', + 'employees', + 'twitter', + 'lastEnrichedAt', + 'affiliatedProfiles', + 'allSubsidiaries', + 'alternativeDomains', + 'alternativeNames', + 'averageEmployeeTenure', + 'averageTenureByLevel', + 'averageTenureByRole', + 'directSubsidiaries', + 'employeeChurnRate', + 'employeeCountByMonth', + 'employeeGrowthRate', + 'employeeCountByMonthByLevel', + 'employeeCountByMonthByRole', + 'gicsSector', + 'grossAdditionsByMonth', + 'grossDeparturesByMonth', + 'ultimateParent', + 'immediateParent', + 'attributes', + 'weakIdentities', + ] + + static async update( + id, + data, + options: IRepositoryOptions, + overrideIdentities = false, + manualChange = false, + ) { const currentUser = SequelizeRepository.getCurrentUser(options) const transaction = SequelizeRepository.getTransaction(options) @@ -541,59 +600,56 @@ class OrganizationRepository { delete data.attributes.syncRemote } + if (manualChange) { + const manuallyChangedFields: string[] = record.manuallyChangedFields || [] + + for (const column of this.ORGANIZATION_UPDATE_COLUMNS) { + let changed = false + + // only check fields that are in the data object that will be updated + if (column in data) { + if ( + record[column] !== null && + column in data && + (data[column] === null || data[column] === undefined) + ) { + // column was removed in the update -> will be set to null by sequelize + changed = true + } else if ( + record[column] === null && + data[column] !== null && + data[column] !== undefined + ) { + // column was null before now it's not anymore + changed = true + } else if (record[column] !== data[column]) { + // column value has changed + changed = true + } + } + + if (changed && !manuallyChangedFields.includes(column)) { + manuallyChangedFields.push(column) + } + } + + data.manuallyChangedFields = manuallyChangedFields + } else { + // ignore columns that were manually changed + // by rewriting them with db data + const manuallyChangedFields: string[] = record.manuallyChangedFields || [] + for (const manuallyChangedColumn of manuallyChangedFields) { + data[manuallyChangedColumn] = record[manuallyChangedColumn] + } + + data.manuallyChangedFields = manuallyChangedFields + } + record = await record.update( { - ...lodash.pick(data, [ - 'displayName', - 'description', - 'emails', - 'phoneNumbers', - 'logo', - 'tags', - 'website', - 'location', - 'github', - 'twitter', - 'linkedin', - 'crunchbase', - 'employees', - 'revenueRange', - 'importHash', - 'isTeamOrganization', - 'employeeCountByCountry', - 'type', - 'ticker', - 'headline', - 'profiles', - 'naics', - 'industry', - 'founded', - 'size', - 'employees', - 'twitter', - 'lastEnrichedAt', - 'affiliatedProfiles', - 'allSubsidiaries', - 'alternativeDomains', - 'alternativeNames', - 'averageEmployeeTenure', - 'averageTenureByLevel', - 'averageTenureByRole', - 'directSubsidiaries', - 'employeeChurnRate', - 'employeeCountByMonth', - 'employeeGrowthRate', - 'employeeCountByMonthByLevel', - 'employeeCountByMonthByRole', - 'gicsSector', - 'grossAdditionsByMonth', - 'grossDeparturesByMonth', - 'ultimateParent', - 'immediateParent', - 'attributes', - 'weakIdentities', - ]), + ...lodash.pick(data, this.ORGANIZATION_UPDATE_COLUMNS), updatedById: currentUser.id, + manuallyChangedFields: data.manuallyChangedFields, }, { transaction, @@ -1135,6 +1191,7 @@ class OrganizationRepository { for (const primaryIdentity of primaryOrganization._source.nested_identities) { // similar organization has a weakIdentity as one of primary organization's strong identity, return score 95 if ( + similarOrganization._source.nested_weakIdentities && similarOrganization._source.nested_weakIdentities.length > 0 && similarOrganization._source.nested_weakIdentities.some( (weakIdentity) => @@ -1144,6 +1201,15 @@ class OrganizationRepository { ) { return 0.95 } + + // check displayName match + if ( + similarOrganization._source.keyword_displayName === + primaryOrganization._source.keyword_displayName + ) { + return 0.98 + } + for (const secondaryIdentity of similarOrganization._source.nested_identities) { const currentLevenstheinDistance = getLevenshteinDistance( primaryIdentity.string_name, @@ -1179,7 +1245,12 @@ class OrganizationRepository { collapse: { field: 'uuid_organizationId', }, - _source: ['uuid_organizationId', 'nested_identities', 'uuid_arr_noMergeIds'], + _source: [ + 'uuid_organizationId', + 'nested_identities', + 'uuid_arr_noMergeIds', + 'keyword_displayName', + ], } let organizations: IOrganizationPartialAggregatesOpensearch[] = [] @@ -1190,25 +1261,6 @@ class OrganizationRepository { queryBody.query = { bool: { filter: [ - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, { term: { uuid_tenantId: tenant.id, @@ -1228,25 +1280,6 @@ class OrganizationRepository { queryBody.query = { bool: { filter: [ - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, { term: { uuid_tenantId: tenant.id, @@ -1276,6 +1309,11 @@ class OrganizationRepository { ) { const identitiesPartialQuery = { should: [ + { + term: { + [`keyword_displayName`]: organization._source.keyword_displayName, + }, + }, { nested: { path: 'nested_weakIdentities', @@ -1315,25 +1353,6 @@ class OrganizationRepository { uuid_tenantId: tenant.id, }, }, - { - bool: { - should: [ - { - range: { - int_activityCount: { - gt: 0, - }, - }, - }, - { - term: { - bool_manuallyCreated: true, - }, - }, - ], - minimum_should_match: 1, - }, - }, ], } @@ -1342,7 +1361,7 @@ class OrganizationRepository { for (const identity of organization._source.nested_identities) { if (identity.string_name.length > 0) { // weak identity search - identitiesPartialQuery.should[0].nested.query.bool.should.push({ + identitiesPartialQuery.should[1].nested.query.bool.should.push({ bool: { must: [ { match: { [`nested_weakIdentities.keyword_name`]: identity.string_name } }, @@ -1363,7 +1382,7 @@ class OrganizationRepository { if (Number.isNaN(Number(identity.string_name))) { hasFuzzySearch = true // fuzzy search for identities - identitiesPartialQuery.should[1].nested.query.bool.should.push({ + identitiesPartialQuery.should[2].nested.query.bool.should.push({ match: { [`nested_identities.keyword_name`]: { query: cleanedIdentityName, @@ -1375,7 +1394,7 @@ class OrganizationRepository { // also check for prefix for identities that has more than 5 characters and no whitespace if (identity.string_name.length > 5 && identity.string_name.indexOf(' ') === -1) { - identitiesPartialQuery.should[1].nested.query.bool.should.push({ + identitiesPartialQuery.should[2].nested.query.bool.should.push({ prefix: { [`nested_identities.keyword_name`]: { value: cleanedIdentityName.slice(0, prefixLength(cleanedIdentityName)), @@ -1414,7 +1433,12 @@ class OrganizationRepository { collapse: { field: 'uuid_organizationId', }, - _source: ['uuid_organizationId', 'nested_identities', 'nested_weakIdentities'], + _source: [ + 'uuid_organizationId', + 'nested_identities', + 'nested_weakIdentities', + 'keyword_displayName', + ], } const organizationsToMerge: ISimilarOrganization[] = @@ -1570,6 +1594,87 @@ class OrganizationRepository { return segments } + static async findByIdentities( + identities: IOrganizationIdentity[], + options: IRepositoryOptions, + ): Promise { + const transaction = SequelizeRepository.getTransaction(options) + const sequelize = SequelizeRepository.getSequelize(options) + const currentTenant = SequelizeRepository.getCurrentTenant(options) + + const identityConditions = identities + .map( + (identity, index) => ` + (oi.platform = :platform${index} and oi.name = :name${index}) + `, + ) + .join(' or ') + + const results = await sequelize.query( + ` + with + "organizationsWithIdentity" as ( + select oi."organizationId" + from "organizationIdentities" oi + where ${identityConditions} + ), + "organizationsWithCounts" as ( + select o.id, count(oi."organizationId") as total_counts + from organizations o + join "organizationIdentities" oi on o.id = oi."organizationId" + where o.id in (select "organizationId" from "organizationsWithIdentity") + group by o.id + ) + select o.id, + o.description, + o.emails, + o.logo, + o.tags, + o.github, + o.twitter, + o.linkedin, + o.crunchbase, + o.employees, + o.location, + o.website, + o.type, + o.size, + o.headline, + o.industry, + o.founded, + o.attributes + from organizations o + inner join "organizationsWithCounts" oc on o.id = oc.id + where o."tenantId" = :tenantId + order by oc.total_counts desc + limit 1; + `, + { + replacements: { + tenantId: currentTenant.id, + ...identities.reduce( + (acc, identity, index) => ({ + ...acc, + [`platform${index}`]: identity.platform, + [`name${index}`]: identity.name, + }), + {}, + ), + }, + type: QueryTypes.SELECT, + transaction, + }, + ) + + if (results.length === 0) { + return null + } + + const result = results[0] as IOrganization + + return result + } + static async findByIdentity( identity: IOrganizationIdentity, options: IRepositoryOptions, @@ -2039,6 +2144,38 @@ class OrganizationRepository { return results } + static async findByIdOpensearch( + id: string, + options: IRepositoryOptions, + segmentId?: string, + ): Promise> { + const segments = segmentId ? [segmentId] : SequelizeRepository.getSegmentIds(options) + + const response = await this.findAndCountAllOpensearch( + { + filter: { + and: [ + { + id: { + eq: id, + }, + }, + ], + }, + limit: 1, + offset: 0, + segments, + }, + options, + ) + + if (response.count === 0) { + throw new Error404() + } + + return response.rows[0] + } + static async findAndCountAllOpensearch( { filter = {} as any, diff --git a/backend/src/database/repositories/priorityLevelContextRepository.ts b/backend/src/database/repositories/priorityLevelContextRepository.ts new file mode 100644 index 0000000000..8466726600 --- /dev/null +++ b/backend/src/database/repositories/priorityLevelContextRepository.ts @@ -0,0 +1,30 @@ +import { IQueuePriorityCalculationContext } from '@crowd/types' +import { QueryTypes } from 'sequelize' +import { IRepositoryOptions } from './IRepositoryOptions' +import SequelizeRepository from './sequelizeRepository' + +export class PriorityLevelContextRepository { + public constructor(private readonly options: IRepositoryOptions) {} + + public async loadPriorityLevelContext( + tenantId: string, + ): Promise { + const seq = SequelizeRepository.getSequelize(this.options) + + const results = await seq.query( + `select plan, "priorityLevel" as "dbPriority" from tenants where id = :tenantId`, + { + replacements: { + tenantId, + }, + type: QueryTypes.SELECT, + }, + ) + + if (results.length === 1) { + return results[0] as IQueuePriorityCalculationContext + } + + throw new Error(`Tenant not found: ${tenantId}!`) + } +} diff --git a/backend/src/database/utils/__tests__/getUserContext.test.ts b/backend/src/database/utils/__tests__/getUserContext.test.ts deleted file mode 100644 index fb94c3bf6a..0000000000 --- a/backend/src/database/utils/__tests__/getUserContext.test.ts +++ /dev/null @@ -1,25 +0,0 @@ -import getUserContext from '../getUserContext' -import SequelizeTestUtils from '../sequelizeTestUtils' - -const db = null - -describe('Get user context tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Get user context tests', () => { - it('Should get the user context for an existing tenant', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const tenantId = mockIRepositoryOptions.currentTenant.dataValues.id - const userContext = await getUserContext(tenantId) - expect(userContext.currentTenant.dataValues.id).toBe(tenantId) - expect(userContext.currentUser).toBeDefined() - }) - }) -}) diff --git a/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts b/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts deleted file mode 100644 index b24dbdf0b5..0000000000 --- a/backend/src/serverless/dbOperations/__tests__/operationsWorker.test.ts +++ /dev/null @@ -1,457 +0,0 @@ -import moment from 'moment' -import SequelizeTestUtils from '../../../database/utils/sequelizeTestUtils' -import ActivityService from '../../../services/activityService' -import MemberService from '../../../services/memberService' -import IntegrationService from '../../../services/integrationService' -import MicroserviceService from '../../../services/microserviceService' -import worker from '../operationsWorker' -import { PlatformType } from '@crowd/types' -import { generateUUIDv1 } from '@crowd/common' -import { populateSegments } from '../../../database/utils/segmentTestUtils' - -const db = null - -describe('Serverless database operations worker tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Bulk upsert method for members', () => { - it('Should add a single simple member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const member = { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - } - - await worker('upsert_members', [member], mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(1) - expect(dbMembers[0].username[PlatformType.GITHUB]).toEqual(['member1']) - }) - - it('Should add a list of members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const members = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - }, - { - username: { - [PlatformType.SLACK]: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.SLACK, - }, - ] - - await worker('upsert_members', members, mockIRepositoryOptions) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(2) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('upsert_members', [], mockIRepositoryOptions) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(0) - }) - }) - - describe('Bulk upsert method for activities with members', () => { - it('Should add a single simple activity with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const ts = moment().toDate() - const activity = { - timestamp: ts, - type: 'message', - platform: 'api', - username: 'member1', - member: { - username: { - api: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId1', - } - - await worker('upsert_activities_with_members', [activity], mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - - expect(dbActivities.length).toBe(1) - expect(moment(dbActivities[0].timestamp).unix()).toBe(moment(ts).unix()) - }) - - it('Should add a list of activities with members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const ts = moment().toDate() - const ts2 = moment().subtract(2, 'days').toDate() - - const activities = [ - { - timestamp: ts, - type: 'message', - platform: 'api', - username: 'member1', - member: { - username: { - api: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId1', - }, - { - timestamp: ts2, - type: 'message', - platform: 'api', - username: 'member2', - member: { - username: { - api: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - }, - sourceId: '#sourceId2', - }, - ] - - await worker('upsert_activities_with_members', activities, mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - expect(dbActivities.length).toBe(2) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('upsert_activities_with_members', [], mockIRepositoryOptions) - - const dbActivities = (await new ActivityService(mockIRepositoryOptions).findAndCountAll({})) - .rows - - expect(dbActivities.length).toBe(0) - }) - }) - - describe('Bulk update method for members', () => { - it('Should update a single member', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const member = { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - score: 1, - } - - const dbMember = await new MemberService(mockIRepositoryOptions).upsert(member) - const memberId = dbMember.id - - await worker( - 'update_members', - [{ id: memberId, update: { score: 10 } }], - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(1) - expect(dbMembers[0].username[PlatformType.GITHUB]).toEqual(['member1']) - expect(dbMembers[0].score).toBe(10) - }) - - it('Should update a list of members', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const members = [ - { - username: { - [PlatformType.GITHUB]: { - username: 'member1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - score: 1, - }, - { - username: { - [PlatformType.DISCORD]: { - username: 'member2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.DISCORD, - score: 2, - }, - ] - - const memberIds = [] - for (const member of members) { - const { id } = await new MemberService(mockIRepositoryOptions).upsert(member) - memberIds.push(id) - } - - await worker( - 'update_members', - [ - { id: memberIds[0], update: { score: 10 } }, - { id: memberIds[1], update: { score: 3 } }, - ], - mockIRepositoryOptions, - ) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(2) - expect(dbMembers[1].score).toBe(10) - expect(dbMembers[0].score).toBe(3) - }) - - it('Should work for an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_members', [], mockIRepositoryOptions) - - const dbMembers = (await new MemberService(mockIRepositoryOptions).findAndCountAll({})).rows - - expect(dbMembers.length).toBe(0) - }) - }) - - describe('Bulk update method for integrations', () => { - it('Should update a single integration', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const integration = { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration1', - status: 'todo', - } - - const dbIntegration = await new IntegrationService(mockIRepositoryOptions).create(integration) - - await worker( - 'update_integrations', - [ - { - id: dbIntegration.id, - update: { status: 'done' }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(1) - expect(dbIntegrations[0].status).toBe('done') - }) - - it('Should update a list of integrations', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const integrations = [ - { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration1', - status: 'todo', - }, - { - platform: PlatformType.SLACK, - integrationIdentifier: 'integration2', - status: 'todo', - }, - ] - - const integrationIds = [] - for (const integration of integrations) { - const { id } = await new IntegrationService(mockIRepositoryOptions).create(integration) - integrationIds.push(id) - } - - await worker( - 'update_integrations', - [ - { - id: integrationIds[0], - update: { status: 'done' }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(2) - expect(dbIntegrations[1].status).toBe('done') - expect(dbIntegrations[0].status).toBe('todo') - }) - - it('Should work with an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_integrations', [], mockIRepositoryOptions) - - const dbIntegrations = ( - await new IntegrationService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(0) - }) - }) - - describe('Bulk update method for microservice', () => { - it('Should update a single microservice', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice = { - type: 'other', - running: false, - init: true, - variant: 'default', - } - - const dbMs = await new MicroserviceService(mockIRepositoryOptions).create(microservice) - - await worker( - 'update_microservices', - [ - { - id: dbMs.id, - update: { running: true }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(1) - expect(dbIntegrations[0].running).toBe(true) - }) - - it('Should update a list of microservices', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservices = [ - { - type: 'other', - running: false, - init: true, - variant: 'default', - }, - { - type: 'member_score', - running: false, - init: true, - variant: 'default', - }, - ] - - const dbMs = await new MicroserviceService(mockIRepositoryOptions).create(microservices[0]) - const dbMs2 = await new MicroserviceService(mockIRepositoryOptions).create(microservices[1]) - - await worker( - 'update_microservices', - [ - { - id: dbMs.id, - update: { running: true }, - }, - { - id: dbMs2.id, - update: { running: true }, - }, - ], - mockIRepositoryOptions, - ) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - expect(dbIntegrations.length).toBe(2) - expect(dbIntegrations[0].running).toBe(true) - expect(dbIntegrations[1].running).toBe(true) - }) - - it('Should work with an empty list', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await worker('update_microservices', [], mockIRepositoryOptions) - - const dbIntegrations = ( - await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({}) - ).rows - - expect(dbIntegrations.length).toBe(0) - }) - }) - - describe('Unknown operation', () => { - it('Should throw an error', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - await expect(worker('unknownOperation', [], mockIRepositoryOptions)).rejects.toThrow( - 'Operation unknownOperation not found', - ) - }) - }) -}) diff --git a/backend/src/serverless/integrations/services/integrationProcessor.ts b/backend/src/serverless/integrations/services/integrationProcessor.ts index f74f190ab5..4fafe5b3c5 100644 --- a/backend/src/serverless/integrations/services/integrationProcessor.ts +++ b/backend/src/serverless/integrations/services/integrationProcessor.ts @@ -1,73 +1,20 @@ import { LoggerBase } from '@crowd/logging' -import { ApiPubSubEmitter, RedisClient } from '@crowd/redis' import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../../database/repositories/integrationStreamRepository' import { IServiceOptions } from '../../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { IntegrationRunProcessor } from './integrationRunProcessor' import { IntegrationTickProcessor } from './integrationTickProcessor' -import { WebhookProcessor } from './webhookProcessor' export class IntegrationProcessor extends LoggerBase { private readonly tickProcessor: IntegrationTickProcessor - private readonly webhookProcessor: WebhookProcessor - - private readonly runProcessor: IntegrationRunProcessor | undefined - - constructor(options: IServiceOptions, redisEmitterClient?: RedisClient) { + constructor(options: IServiceOptions) { super(options.log) - const integrationServices = [] - - this.log.debug( - { supportedIntegrations: integrationServices.map((i) => i.type) }, - 'Successfully detected supported integrations!', - ) - - let apiPubSubEmitter: ApiPubSubEmitter | undefined - - if (redisEmitterClient) { - apiPubSubEmitter = new ApiPubSubEmitter(redisEmitterClient, this.log) - } - const integrationRunRepository = new IntegrationRunRepository(options) - const integrationStreamRepository = new IntegrationStreamRepository(options) - this.tickProcessor = new IntegrationTickProcessor( - options, - integrationServices, - integrationRunRepository, - ) - - this.webhookProcessor = new WebhookProcessor(options, integrationServices) - - if (apiPubSubEmitter) { - this.runProcessor = new IntegrationRunProcessor( - options, - integrationServices, - integrationRunRepository, - integrationStreamRepository, - apiPubSubEmitter, - ) - } else { - this.log.warn('No apiPubSubEmitter provided, runProcessor will not be initialized!') - } + this.tickProcessor = new IntegrationTickProcessor(options, integrationRunRepository) } async processTick() { await this.tickProcessor.processTick() } - - async processWebhook(webhookId: string, force?: boolean, fireCrowdWebhooks?: boolean) { - await this.webhookProcessor.processWebhook(webhookId, force, fireCrowdWebhooks) - } - - async process(req: NodeWorkerIntegrationProcessMessage) { - if (this.runProcessor) { - await this.runProcessor.process(req) - } else { - throw new Error('runProcessor is not initialized!') - } - } } diff --git a/backend/src/serverless/integrations/services/integrationRunProcessor.ts b/backend/src/serverless/integrations/services/integrationRunProcessor.ts deleted file mode 100644 index 3575b726e4..0000000000 --- a/backend/src/serverless/integrations/services/integrationRunProcessor.ts +++ /dev/null @@ -1,573 +0,0 @@ -import moment from 'moment' -import { ApiPubSubEmitter } from '@crowd/redis' -import { Logger, getChildLogger, LoggerBase } from '@crowd/logging' -import { i18n, singleOrDefault } from '@crowd/common' -import { IntegrationRunState, PlatformType } from '@crowd/types' -import { sendSlackAlert, SlackAlertTypes } from '@crowd/alerting' -import IntegrationRepository from '../../../database/repositories/integrationRepository' -import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' -import IntegrationStreamRepository from '../../../database/repositories/integrationStreamRepository' -import MicroserviceRepository from '../../../database/repositories/microserviceRepository' -import getUserContext from '../../../database/utils/getUserContext' -import { twitterFollowers } from '../../../database/utils/keys/microserviceTypes' -import { IServiceOptions } from '../../../services/IServiceOptions' -import { - IIntegrationStream, - IProcessStreamResults, - IStepContext, -} from '../../../types/integration/stepResult' -import { IntegrationRun } from '../../../types/integrationRunTypes' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { IntegrationServiceBase } from './integrationServiceBase' -import SampleDataService from '../../../services/sampleDataService' -import { - DbIntegrationStreamCreateData, - IntegrationStream, - IntegrationStreamState, -} from '../../../types/integrationStreamTypes' -import bulkOperations from '../../dbOperations/operationsWorker' -import UserRepository from '../../../database/repositories/userRepository' -import EmailSender from '../../../services/emailSender' -import { API_CONFIG, SLACK_ALERTING_CONFIG } from '../../../conf' -import SegmentRepository from '../../../database/repositories/segmentRepository' - -export class IntegrationRunProcessor extends LoggerBase { - constructor( - options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], - private readonly integrationRunRepository: IntegrationRunRepository, - private readonly integrationStreamRepository: IntegrationStreamRepository, - private readonly apiPubSubEmitter?: ApiPubSubEmitter, - ) { - super(options.log) - } - - async process(req: NodeWorkerIntegrationProcessMessage) { - if (!req.runId) { - this.log.warn("No runId provided! Skipping because it's an old message.") - return - } - - this.log.info({ runId: req.runId }, 'Detected integration run!') - - const run = await this.integrationRunRepository.findById(req.runId) - - const userContext = await getUserContext(run.tenantId) - - let integration - - if (run.integrationId) { - integration = await IntegrationRepository.findById(run.integrationId, userContext) - } else if (run.microserviceId) { - const microservice = await MicroserviceRepository.findById(run.microserviceId, userContext) - - switch (microservice.type) { - case twitterFollowers: - integration = await IntegrationRepository.findByPlatform( - PlatformType.TWITTER, - userContext, - ) - break - default: - throw new Error(`Microservice type '${microservice.type}' is not supported!`) - } - } else { - this.log.error({ runId: req.runId }, 'Integration run has no integration or microservice!') - throw new Error(`Integration run '${req.runId}' has no integration or microservice!`) - } - - const segmentRepository = new SegmentRepository(userContext) - userContext.currentSegments = [await segmentRepository.findById(integration.segmentId)] - - const logger = getChildLogger('process', this.log, { - runId: req.runId, - type: integration.platform, - tenantId: integration.tenantId, - integrationId: run.integrationId, - onboarding: run.onboarding, - microserviceId: run.microserviceId, - }) - - logger.info('Processing integration!') - - userContext.log = logger - - // get the relevant integration service that is supposed to be configured already - const intService = singleOrDefault( - this.integrationServices, - (s) => s.type === integration.platform, - ) - if (intService === undefined) { - logger.error('No integration service configured!') - throw new Error(`No integration service configured for type '${integration.platform}'!`) - } - - const stepContext: IStepContext = { - startTimestamp: moment().utc().unix(), - limitCount: integration.limitCount || 0, - onboarding: run.onboarding, - pipelineData: {}, - runId: req.runId, - integration, - serviceContext: userContext, - repoContext: userContext, - logger, - } - - if (!req.streamId) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - run.integrationId, - run.microserviceId, - req.runId, - ) - - if (existingRun) { - logger.info('Integration is already being processed!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'check_existing_run', - message: 'Integration is already being processed!', - existingRunId: existingRun.id, - }) - return - } - - if (run.state === IntegrationRunState.PROCESSED) { - logger.warn('Integration is already processed!') - return - } - - if (run.state === IntegrationRunState.PENDING) { - logger.info('Started processing integration!') - } else if (run.state === IntegrationRunState.DELAYED) { - logger.info('Continued processing delayed integration!') - } else if (run.state === IntegrationRunState.ERROR) { - logger.info('Restarted processing errored integration!') - } else if (run.state === IntegrationRunState.PROCESSING) { - throw new Error(`Invalid state '${run.state}' for integration run!`) - } - - await this.integrationRunRepository.markProcessing(req.runId) - run.state = IntegrationRunState.PROCESSING - - if (integration.settings.updateMemberAttributes) { - logger.trace('Updating member attributes!') - - await intService.createMemberAttributes(stepContext) - - integration.settings.updateMemberAttributes = false - await IntegrationRepository.update( - integration.id, - { settings: integration.settings }, - userContext, - ) - } - - // delete sample data on onboarding - if (run.onboarding) { - try { - await new SampleDataService(userContext).deleteSampleData() - } catch (err) { - logger.error(err, { tenantId: integration.tenantId }, 'Error deleting sample data!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'delete_sample_data', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - return - } - } - } - - try { - // check global limit reset - if (intService.limitResetFrequencySeconds > 0 && integration.limitLastResetAt) { - const secondsSinceLastReset = moment() - .utc() - .diff(moment(integration.limitLastResetAt).utc(), 'seconds') - - if (secondsSinceLastReset >= intService.limitResetFrequencySeconds) { - integration.limitCount = 0 - integration.limitLastResetAt = moment().utc().toISOString() - - await IntegrationRepository.update( - integration.id, - { - limitCount: integration.limitCount, - limitLastResetAt: integration.limitLastResetAt, - }, - userContext, - ) - } - } - - // preprocess if needed - logger.trace('Preprocessing integration!') - try { - await intService.preprocess(stepContext) - } catch (err) { - if (err.rateLimitResetSeconds) { - // need to delay integration processing - logger.warn(err, 'Rate limit reached while preprocessing integration! Delaying...') - await this.handleRateLimitError(logger, run, err.rateLimitResetSeconds, stepContext) - return - } - - logger.error(err, 'Error preprocessing integration!') - await this.integrationRunRepository.markError(req.runId, { - errorPoint: 'preprocessing', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - return - } - - // detect streams to process for this integration - - let forcedStream: IntegrationStream | undefined - if (req.streamId) { - forcedStream = await this.integrationStreamRepository.findById(req.streamId) - - if (!forcedStream) { - logger.error({ streamId: req.streamId }, 'Stream not found!') - throw new Error(`Stream '${req.streamId}' not found!`) - } - } else { - const dbStreams = await this.integrationStreamRepository.findByRunId(req.runId, 1, 1) - if (dbStreams.length > 0) { - logger.trace('Streams already detected and saved to the database!') - } else { - // need to optimize this as well since it may happen that we have a lot of streams - logger.trace('Detecting streams!') - try { - const pendingStreams = await intService.getStreams(stepContext) - const createStreams: DbIntegrationStreamCreateData[] = pendingStreams.map((s) => ({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: s.value, - metadata: s.metadata, - })) - await this.integrationStreamRepository.bulkCreate(createStreams) - await this.integrationRunRepository.touch(run.id) - } catch (err) { - if (err.rateLimitResetSeconds) { - // need to delay integration processing - logger.warn(err, 'Rate limit reached while getting integration streams! Delaying...') - await this.handleRateLimitError(logger, run, err.rateLimitResetSeconds, stepContext) - return - } - - throw err - } - } - } - - // process streams - let processedCount = 0 - let notifyCount = 0 - - let nextStream: IntegrationStream | undefined - if (forcedStream) { - nextStream = forcedStream - } else { - nextStream = await this.integrationStreamRepository.getNextStreamToProcess(req.runId) - } - - while (nextStream) { - if ((req as any).exiting) { - if (!run.onboarding) { - logger.warn('Stopped processing integration (not onboarding)!') - break - } else { - logger.warn('Stopped processing integration (onboarding)!') - const delayUntil = moment() - .add(3 * 60, 'seconds') - .toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - break - } - } - - const stream: IIntegrationStream = { - id: nextStream.id, - value: nextStream.name, - metadata: nextStream.metadata, - } - - processedCount++ - notifyCount++ - - let processStreamResult: IProcessStreamResults - - logger.trace({ streamId: stream.id }, 'Processing stream!') - await this.integrationStreamRepository.markProcessing(stream.id) - await this.integrationRunRepository.touch(run.id) - try { - processStreamResult = await intService.processStream(stream, stepContext) - } catch (err) { - if (err.rateLimitResetSeconds) { - logger.warn( - { streamId: stream.id, message: err.message }, - 'Rate limit reached while processing stream! Delaying...', - ) - await this.handleRateLimitError( - logger, - run, - err.rateLimitResetSeconds, - stepContext, - stream, - ) - return - } - - const retries = await this.integrationStreamRepository.markError(stream.id, { - errorPoint: 'process_stream', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - await this.integrationRunRepository.touch(run.id) - - logger.error(err, { retries, streamId: stream.id }, 'Error while processing stream!') - } - - if (processStreamResult) { - // surround with try catch so if one stream fails we try all of them as well just in case - try { - logger.trace({ stream: JSON.stringify(stream) }, `Processing stream results!`) - - if (processStreamResult.newStreams && processStreamResult.newStreams.length > 0) { - const dbCreateStreams: DbIntegrationStreamCreateData[] = - processStreamResult.newStreams.map((s) => ({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: s.value, - metadata: s.metadata, - })) - - await this.integrationStreamRepository.bulkCreate(dbCreateStreams) - await this.integrationRunRepository.touch(run.id) - - logger.info( - `Detected ${processStreamResult.newStreams.length} new streams to process!`, - ) - } - - for (const operation of processStreamResult.operations) { - if (operation.records.length > 0) { - logger.trace( - { operationType: operation.type }, - `Processing bulk operation with ${operation.records.length} records!`, - ) - stepContext.limitCount += operation.records.length - await bulkOperations( - operation.type, - operation.records, - userContext, - req.fireCrowdWebhooks ?? true, - ) - } - } - - if (processStreamResult.nextPageStream !== undefined) { - if ( - !run.onboarding && - (await intService.isProcessingFinished( - stepContext, - stream, - processStreamResult.operations, - processStreamResult.lastRecordTimestamp, - )) - ) { - logger.warn('Integration processing finished because of service implementation!') - } else { - logger.trace( - { currentStream: JSON.stringify(stream) }, - `Detected next page stream!`, - ) - await this.integrationStreamRepository.create({ - runId: req.runId, - tenantId: run.tenantId, - integrationId: run.integrationId, - microserviceId: run.microserviceId, - name: processStreamResult.nextPageStream.value, - metadata: processStreamResult.nextPageStream.metadata, - }) - await this.integrationRunRepository.touch(run.id) - } - } - - if (processStreamResult.sleep !== undefined && processStreamResult.sleep > 0) { - logger.warn( - `Stream processing resulted in a requested delay of ${processStreamResult.sleep}! Will delay remaining streams!`, - ) - - const delayUntil = moment().add(processStreamResult.sleep, 'seconds').toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - break - } - - if (intService.globalLimit > 0 && stepContext.limitCount >= intService.globalLimit) { - // if limit reset frequency is 0 we don't need to care about limits - if (intService.limitResetFrequencySeconds > 0) { - logger.warn( - { - limitCount: stepContext.limitCount, - globalLimit: intService.globalLimit, - }, - 'We reached a global limit - stopping processing!', - ) - - integration.limitCount = stepContext.limitCount - - const secondsSinceLastReset = moment() - .utc() - .diff(moment(integration.limitLastResetAt).utc(), 'seconds') - - if (secondsSinceLastReset < intService.limitResetFrequencySeconds) { - const delayUntil = moment() - .add(intService.limitResetFrequencySeconds - secondsSinceLastReset, 'seconds') - .toDate() - await this.integrationRunRepository.delay(req.runId, delayUntil) - } - - break - } - } - - if (notifyCount === 50) { - logger.info(`Processed ${processedCount} streams!`) - notifyCount = 0 - } - - await this.integrationStreamRepository.markProcessed(stream.id) - await this.integrationRunRepository.touch(run.id) - } catch (err) { - logger.error( - err, - { stream: JSON.stringify(stream) }, - 'Error processing stream results!', - ) - await this.integrationStreamRepository.markError(stream.id, { - errorPoint: 'process_stream_results', - message: err.message, - stack: err.stack, - errorString: JSON.stringify(err), - }) - await this.integrationRunRepository.touch(run.id) - } - } - - if (forcedStream) { - break - } - - nextStream = await this.integrationStreamRepository.getNextStreamToProcess(req.runId) - } - - // postprocess integration settings - await intService.postprocess(stepContext) - - logger.info('Done processing integration!') - } catch (err) { - logger.error(err, 'Error while processing integration!') - } finally { - const newState = await this.integrationRunRepository.touchState(req.runId) - - let emailSentAt - if (newState === IntegrationRunState.PROCESSED) { - if (!integration.emailSentAt) { - const tenantUsers = await UserRepository.findAllUsersOfTenant(integration.tenantId) - emailSentAt = new Date() - for (const user of tenantUsers) { - await new EmailSender(EmailSender.TEMPLATES.INTEGRATION_DONE, { - integrationName: i18n('en', `entities.integration.name.${integration.platform}`), - link: API_CONFIG.frontendUrl, - }).sendTo(user.email) - } - } - } - - let status - switch (newState) { - case IntegrationRunState.PROCESSED: - status = 'done' - break - case IntegrationRunState.ERROR: - status = 'error' - break - default: - status = integration.status - } - - await IntegrationRepository.update( - integration.id, - { - status, - emailSentAt, - settings: stepContext.integration.settings, - refreshToken: stepContext.integration.refreshToken, - token: stepContext.integration.token, - }, - userContext, - ) - - if (newState === IntegrationRunState.PROCESSING && !req.streamId) { - const failedStreams = await this.integrationStreamRepository.findByRunId(req.runId, 1, 1, [ - IntegrationStreamState.ERROR, - ]) - if (failedStreams.length > 0) { - logger.warn('Integration ended but we are still processing - delaying for a minute!') - const delayUntil = moment().add(60, 'seconds') - await this.integrationRunRepository.delay(run.id, delayUntil.toDate()) - } else { - logger.error('Integration ended but we are still processing!') - } - } else if (newState === IntegrationRunState.ERROR) { - await sendSlackAlert({ - slackURL: SLACK_ALERTING_CONFIG.url, - alertType: SlackAlertTypes.INTEGRATION_ERROR, - integration, - userContext, - log: logger, - frameworkVersion: 'old', - }) - } - - if (run.onboarding && this.apiPubSubEmitter) { - this.apiPubSubEmitter.emitIntegrationCompleted(integration.tenantId, integration.id, status) - } - } - } - - private async handleRateLimitError( - logger: Logger, - run: IntegrationRun, - rateLimitResetSeconds: number, - context: IStepContext, - stream?: IIntegrationStream, - ): Promise { - await IntegrationRepository.update( - context.integration.id, - { - settings: context.integration.settings, - refreshToken: context.integration.refreshToken, - token: context.integration.token, - }, - context.repoContext, - ) - - logger.warn('Rate limit reached, delaying integration processing!') - const delayUntil = moment().add(rateLimitResetSeconds + 30, 'seconds') - await this.integrationRunRepository.delay(run.id, delayUntil.toDate()) - - if (stream) { - await this.integrationStreamRepository.reset(stream.id) - } - } -} diff --git a/backend/src/serverless/integrations/services/integrationServiceBase.ts b/backend/src/serverless/integrations/services/integrationServiceBase.ts deleted file mode 100644 index 6e459dbe0a..0000000000 --- a/backend/src/serverless/integrations/services/integrationServiceBase.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { SuperfaceClient } from '@superfaceai/one-sdk' -import moment from 'moment' -import crypto from 'crypto' -import { getServiceChildLogger } from '@crowd/logging' -import { IntegrationRunState, IntegrationType } from '@crowd/types' -import { IRepositoryOptions } from '../../../database/repositories/IRepositoryOptions' -import { - IIntegrationStream, - IPendingStream, - IProcessStreamResults, - IProcessWebhookResults, - IStepContext, - IStreamResultOperation, -} from '../../../types/integration/stepResult' -import { IS_TEST_ENV } from '../../../conf' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' - -const logger = getServiceChildLogger('integrationService') - -/* eslint class-methods-use-this: 0 */ - -/* eslint-disable @typescript-eslint/no-unused-vars */ - -export abstract class IntegrationServiceBase { - /** - * How many records to process before we stop - */ - public globalLimit: number - - /** - * If onboarding globalLimit will be multiplied by this factor for that run - */ - public onboardingLimitModifierFactor: number - - /** - * How many seconds between global limit reset (0 for auto reset) - */ - public limitResetFrequencySeconds: number - - /** - * Every new integration should extend this class and implement its methods. - * - * @param type What integration is this? - * @param ticksBetweenChecks How many ticks to skip between each integration checks (each tick is 1 minute). If 0 it will be triggered every tick same as if it was 1. If negative it will never be triggered. - */ - protected constructor( - public readonly type: IntegrationType, - public readonly ticksBetweenChecks: number, - ) { - this.globalLimit = 0 - this.onboardingLimitModifierFactor = 1.0 - this.limitResetFrequencySeconds = 0 - } - - async triggerIntegrationCheck(integrations: any[], options: IRepositoryOptions): Promise { - const repository = new IntegrationRunRepository(options) - - for (const integration of integrations) { - const run = await repository.create({ - integrationId: integration.id, - tenantId: integration.tenantId, - onboarding: false, - state: IntegrationRunState.PENDING, - }) - - logger.info( - { integrationId: integration.id, runId: run.id }, - 'Triggering integration processing!', - ) - await sendNodeWorkerMessage( - integration.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - } - - async preprocess(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - async createMemberAttributes(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - abstract getStreams(context: IStepContext): Promise - - abstract processStream( - stream: IIntegrationStream, - context: IStepContext, - ): Promise - - async isProcessingFinished( - context: IStepContext, - currentStream: IIntegrationStream, - lastOperations: IStreamResultOperation[], - lastRecord?: any, - lastRecordTimestamp?: number, - ): Promise { - return false - } - - async postprocess(context: IStepContext): Promise { - // do nothing - override if something is needed - } - - async processWebhook(webhook: any, context: IStepContext): Promise { - throw new Error('Not implemented') - } - - static superfaceClient(): SuperfaceClient { - if (IS_TEST_ENV) { - return undefined - } - - return new SuperfaceClient() - } - - /** - * Check whether the last record is over the retrospect that we are interested in - * @param lastRecordTimestamp The last activity timestamp we got - * @param startTimestamp The timestamp when we started - * @param maxRetrospect The maximum time we want to crawl - * @returns Whether we are over the retrospect already - */ - static isRetrospectOver( - lastRecordTimestamp: number, - startTimestamp: number, - maxRetrospect: number, - ): boolean { - return startTimestamp - moment(lastRecordTimestamp).unix() > maxRetrospect - } - - /** - * Some activities will not have a remote(API) counterparts so they will miss sourceIds. - * Since we're using sourceIds to find out if an activity already exists in our DB, - * sourceIds are required when creating an activity. - * This function generates an md5 hash that can be used as a sourceId of an activity. - * Prepends string `gen-` to the beginning so generated and remote sourceIds - * can be distinguished. - * - * @param {string} uniqueRemoteId remote member id from an integration. This id needs to be unique in a platform - * @param {string} type type of the activity - * @param {string} timestamp unix timestamp of the activity - * @param {string} platform platform of the activity - * @returns 32 bit md5 hash generated from the given data, prepended with string `gen-` - */ - static generateSourceIdHash( - uniqueRemoteId: string, - type: string, - timestamp: string, - platform: string, - ) { - if (!uniqueRemoteId || !type || !timestamp || !platform) { - throw new Error('Bad hash input') - } - - const data = `${uniqueRemoteId}-${type}-${timestamp}-${platform}` - return `gen-${crypto.createHash('md5').update(data).digest('hex')}` - } - - /** - * Get the number of seconds from a date to a unix timestamp. - * Adding a 25% padding for security. - * If the unix timestamp is before the date, return 3 minutes for security - * @param date The date to get the seconds from - * @param unixTimestamp The unix timestamp to get the seconds from - * @returns The number of seconds from the date to the unix timestamp - */ - static secondsUntilTimestamp( - unixTimestamp: number, - date: Date = moment().utc().toDate(), - ): number { - const timestampedDate: number = moment.utc(date).unix() - if (timestampedDate > unixTimestamp) { - return 60 * 3 - } - return Math.floor(unixTimestamp - timestampedDate) - } -} diff --git a/backend/src/serverless/integrations/services/integrationTickProcessor.ts b/backend/src/serverless/integrations/services/integrationTickProcessor.ts index 183e59d508..4e5ea98e60 100644 --- a/backend/src/serverless/integrations/services/integrationTickProcessor.ts +++ b/backend/src/serverless/integrations/services/integrationTickProcessor.ts @@ -1,26 +1,20 @@ import { processPaginated, singleOrDefault } from '@crowd/common' -import { INTEGRATION_SERVICES } from '@crowd/integrations' -import { LoggerBase, getChildLogger } from '@crowd/logging' import { + DataSinkWorkerEmitter, IntegrationRunWorkerEmitter, IntegrationStreamWorkerEmitter, - DataSinkWorkerEmitter, -} from '@crowd/sqs' -import { IntegrationRunState, IntegrationType } from '@crowd/types' -import SequelizeRepository from '@/database/repositories/sequelizeRepository' -import MicroserviceRepository from '@/database/repositories/microserviceRepository' +} from '@crowd/common_services' +import { INTEGRATION_SERVICES } from '@crowd/integrations' +import { LoggerBase, getChildLogger } from '@crowd/logging' +import { IntegrationType, TenantPlans } from '@crowd/types' import IntegrationRepository from '@/database/repositories/integrationRepository' -import { IRepositoryOptions } from '@/database/repositories/IRepositoryOptions' import IntegrationRunRepository from '../../../database/repositories/integrationRunRepository' import { IServiceOptions } from '../../../services/IServiceOptions' -import { NodeWorkerIntegrationProcessMessage } from '../../../types/mq/nodeWorkerIntegrationProcessMessage' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' import { + getDataSinkWorkerEmitter, getIntegrationRunWorkerEmitter, getIntegrationStreamWorkerEmitter, - getDataSinkWorkerEmitter, } from '../../utils/serviceSQS' -import { IntegrationServiceBase } from './integrationServiceBase' export class IntegrationTickProcessor extends LoggerBase { private tickTrackingMap: Map = new Map() @@ -35,15 +29,10 @@ export class IntegrationTickProcessor extends LoggerBase { constructor( options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], private readonly integrationRunRepository: IntegrationRunRepository, ) { super(options.log) - for (const intService of this.integrationServices) { - this.tickTrackingMap[intService.type] = 0 - } - for (const intService of INTEGRATION_SERVICES) { this.tickTrackingMap[intService.type] = 0 } @@ -67,18 +56,11 @@ export class IntegrationTickProcessor extends LoggerBase { private async processCheckTick() { this.log.trace('Processing integration processor tick!') - const tickers: IIntTicker[] = this.integrationServices.map((i) => ({ + const tickers: IIntTicker[] = INTEGRATION_SERVICES.map((i) => ({ type: i.type, - ticksBetweenChecks: i.ticksBetweenChecks, + ticksBetweenChecks: i.checkEvery || -1, })) - for (const service of INTEGRATION_SERVICES) { - tickers.push({ - type: service.type, - ticksBetweenChecks: service.checkEvery || -1, - }) - } - const promises: Promise[] = [] for (const intService of tickers) { @@ -121,134 +103,75 @@ export class IntegrationTickProcessor extends LoggerBase { const logger = getChildLogger('processCheck', this.log, { IntegrationType: type }) logger.trace('Processing integration check!') - if (type === IntegrationType.TWITTER_REACH) { - await processPaginated( - async (page) => MicroserviceRepository.findAllByType('twitter_followers', page, 10), - async (microservices) => { - this.log.debug({ type, count: microservices.length }, 'Found microservices to check!') - for (const micro of microservices) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - undefined, - micro.id, - ) - if (!existingRun) { - const microservice = micro as any + const newIntService = singleOrDefault(INTEGRATION_SERVICES, (i) => i.type === type) - const run = await this.integrationRunRepository.create({ - microserviceId: microservice.id, - tenantId: microservice.tenantId, - onboarding: false, - state: IntegrationRunState.PENDING, - }) - - this.log.debug({ type, runId: run.id }, 'Triggering microservice processing!') - - await sendNodeWorkerMessage( - microservice.tenantId, - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - } - }, - ) - } else { - const options = - (await SequelizeRepository.getDefaultIRepositoryOptions()) as IRepositoryOptions - - // get the relevant integration service that is supposed to be configured already - const intService = singleOrDefault(this.integrationServices, (s) => s.type === type) + if (!newIntService) { + throw new Error(`No integration service found for type ${type}!`) + } - if (intService) { - await processPaginated( - async (page) => IntegrationRepository.findAllActive(type, page, 10), - async (integrations) => { - logger.debug( - { integrationIds: integrations.map((i) => i.id) }, - 'Found old integrations to check!', - ) - const inactiveIntegrations: any[] = [] - for (const integration of integrations as any[]) { - const existingRun = await this.integrationRunRepository.findLastProcessingRun( - integration.id, - ) - if (!existingRun) { - inactiveIntegrations.push(integration) - } - } + const emitter = await getIntegrationRunWorkerEmitter() - if (inactiveIntegrations.length > 0) { + await processPaginated( + async (page) => IntegrationRepository.findAllActive(type, page, 10), + async (integrations) => { + logger.debug( + { integrationIds: integrations.map((i) => i.id) }, + 'Found new integrations to check!', + ) + for (const integration of integrations as any[]) { + const existingRun = + await this.integrationRunRepository.findLastProcessingRunInNewFramework(integration.id) + if (!existingRun) { + const CHUNKS = 3 // Define the number of chunks + const DELAY_BETWEEN_CHUNKS = 30 * 60 * 1000 // Define the delay between chunks in milliseconds + const rand = Math.random() * CHUNKS + const chunkIndex = Math.min(Math.floor(rand), CHUNKS - 1) + const delay = chunkIndex * DELAY_BETWEEN_CHUNKS + + if ( + newIntService.type === IntegrationType.DISCORD && + integration.tenant.plan === TenantPlans.Essential + ) { + // not triggering discord integrations for essential plan, only paid plans logger.info( - { integrationIds: inactiveIntegrations.map((i) => i.id) }, - 'Triggering old integration checks!', + { integrationId: integration.id }, + 'Not triggering new integration check for Discord for essential plan!', ) - await intService.triggerIntegrationCheck(inactiveIntegrations, options) + // eslint-disable-next-line no-continue + continue } - }, - ) - } else { - const newIntService = singleOrDefault(INTEGRATION_SERVICES, (i) => i.type === type) - - if (!newIntService) { - throw new Error(`No integration service found for type ${type}!`) - } - - const emitter = await getIntegrationRunWorkerEmitter() - await processPaginated( - async (page) => IntegrationRepository.findAllActive(type, page, 10), - async (integrations) => { - logger.debug( - { integrationIds: integrations.map((i) => i.id) }, - 'Found new integrations to check!', - ) - for (const integration of integrations as any[]) { - const existingRun = - await this.integrationRunRepository.findLastProcessingRunInNewFramework( + // Divide integrations into chunks for Discord + if (newIntService.type === IntegrationType.DISCORD) { + setTimeout(async () => { + logger.info( + { integrationId: integration.id }, + `Triggering new delayed integration check for Discord in ${ + delay / 60 / 1000 + } minutes!`, + ) + await emitter.triggerIntegrationRun( + integration.tenantId, + integration.platform, integration.id, + false, ) - if (!existingRun) { - const CHUNKS = 3 // Define the number of chunks - const DELAY_BETWEEN_CHUNKS = 30 * 60 * 1000 // Define the delay between chunks in milliseconds - const rand = Math.random() * CHUNKS - const chunkIndex = Math.min(Math.floor(rand), CHUNKS - 1) - const delay = chunkIndex * DELAY_BETWEEN_CHUNKS - - // Divide integrations into chunks for Discord - if (newIntService.type === IntegrationType.DISCORD) { - setTimeout(async () => { - logger.info( - { integrationId: integration.id }, - `Triggering new delayed integration check for Discord in ${ - delay / 60 / 1000 - } minutes!`, - ) - await emitter.triggerIntegrationRun( - integration.tenantId, - integration.platform, - integration.id, - false, - ) - }, delay) - } else { - logger.info( - { integrationId: integration.id }, - 'Triggering new integration check!', - ) - await emitter.triggerIntegrationRun( - integration.tenantId, - integration.platform, - integration.id, - false, - ) - } - } else { - logger.info({ integrationId: integration.id }, 'Existing run found, skipping!') - } + }, delay) + } else { + logger.info({ integrationId: integration.id }, 'Triggering new integration check!') + await emitter.triggerIntegrationRun( + integration.tenantId, + integration.platform, + integration.id, + false, + ) } - }, - ) - } - } + } else { + logger.info({ integrationId: integration.id }, 'Existing run found, skipping!') + } + } + }, + ) } private async processDelayedTick() { @@ -256,23 +179,6 @@ export class IntegrationTickProcessor extends LoggerBase { await this.intRunWorkerEmitter.checkRuns() await this.intStreamWorkerEmitter.checkStreams() await this.dataSinkWorkerEmitter.checkResults() - - // TODO check streams as well - this.log.trace('Checking for delayed integration runs!') - - await processPaginated( - async (page) => this.integrationRunRepository.findDelayedRuns(page, 10), - async (delayedRuns) => { - for (const run of delayedRuns) { - this.log.info({ runId: run.id }, 'Triggering delayed integration run processing!') - - await sendNodeWorkerMessage( - new Date().toISOString(), - new NodeWorkerIntegrationProcessMessage(run.id), - ) - } - }, - ) } } diff --git a/backend/src/serverless/integrations/services/webhookProcessor.ts b/backend/src/serverless/integrations/services/webhookProcessor.ts deleted file mode 100644 index af4ad4d1d1..0000000000 --- a/backend/src/serverless/integrations/services/webhookProcessor.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { LoggerBase, getChildLogger } from '@crowd/logging' -import moment from 'moment' -import { singleOrDefault } from '@crowd/common' -import { IRepositoryOptions } from '../../../database/repositories/IRepositoryOptions' -import IncomingWebhookRepository from '../../../database/repositories/incomingWebhookRepository' -import IntegrationRepository from '../../../database/repositories/integrationRepository' -import SequelizeRepository from '../../../database/repositories/sequelizeRepository' -import getUserContext from '../../../database/utils/getUserContext' -import { IServiceOptions } from '../../../services/IServiceOptions' -import { IStepContext } from '../../../types/integration/stepResult' -import { NodeWorkerProcessWebhookMessage } from '../../../types/mq/nodeWorkerProcessWebhookMessage' -import { WebhookState } from '../../../types/webhooks' -import bulkOperations from '../../dbOperations/operationsWorker' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' -import { IntegrationServiceBase } from './integrationServiceBase' -import SegmentRepository from '../../../database/repositories/segmentRepository' - -export class WebhookProcessor extends LoggerBase { - constructor( - options: IServiceOptions, - private readonly integrationServices: IntegrationServiceBase[], - ) { - super(options.log) - } - - static readonly MAX_RETRY_LIMIT = 5 - - async processWebhook(webhookId: string, force?: boolean, fireCrowdWebhooks?: boolean) { - const options = (await SequelizeRepository.getDefaultIRepositoryOptions()) as IRepositoryOptions - const repo = new IncomingWebhookRepository(options) - const webhook = await repo.findById(webhookId) - let logger = getChildLogger('processWebhook', this.log, { webhookId }) - - if (webhook === null || webhook === undefined) { - logger.error('Webhook not found!') - return - } - - logger.debug('Processing webhook!') - - logger = getChildLogger('processWebhook', this.log, { - type: webhook.type, - tenantId: webhook.tenantId, - integrationId: webhook.integrationId, - }) - - logger.debug('Webhook found!') - - if (!(force === true) && webhook.state !== WebhookState.PENDING) { - logger.error({ state: webhook.state }, 'Webhook is not in pending state!') - return - } - - const userContext = await getUserContext(webhook.tenantId) - userContext.log = logger - - const integration = await IntegrationRepository.findById(webhook.integrationId, userContext) - if (integration.platform === 'github' || integration.platform === 'discord') { - return - } - const segment = await new SegmentRepository(userContext).findById(integration.segmentId) - userContext.currentSegments = [segment] - - const intService = singleOrDefault( - this.integrationServices, - (s) => s.type === integration.platform, - ) - if (intService === undefined) { - logger.error('No integration service configured!') - throw new Error(`No integration service configured for type '${integration.platform}'!`) - } - - const stepContext: IStepContext = { - startTimestamp: moment().utc().unix(), - limitCount: integration.limitCount || 0, - onboarding: false, - pipelineData: {}, - webhook, - integration, - serviceContext: userContext, - repoContext: userContext, - logger, - } - - if (integration.settings.updateMemberAttributes) { - logger.trace('Updating member attributes!') - - await intService.createMemberAttributes(stepContext) - - integration.settings.updateMemberAttributes = false - await IntegrationRepository.update( - integration.id, - { settings: integration.settings }, - userContext, - ) - } - - const whContext = { ...userContext } - whContext.transaction = await SequelizeRepository.createTransaction(whContext) - - try { - const result = await intService.processWebhook(webhook, stepContext) - for (const operation of result.operations) { - if (operation.records.length > 0) { - logger.trace( - { operationType: operation.type }, - `Processing bulk operation with ${operation.records.length} records!`, - ) - await bulkOperations(operation.type, operation.records, userContext, fireCrowdWebhooks) - } - } - await repo.markCompleted(webhook.id) - logger.debug('Webhook processed!') - } catch (err) { - if (err.rateLimitResetSeconds) { - logger.warn(err, 'Rate limit reached while processing webhook! Delaying...') - await sendNodeWorkerMessage( - integration.tenantId, - new NodeWorkerProcessWebhookMessage(integration.tenantId, webhookId), - err.rateLimitResetSeconds + 5, - ) - } else { - logger.error(err, 'Error processing webhook!') - await repo.markError(webhook.id, err) - } - } finally { - await SequelizeRepository.commitTransaction(whContext.transaction) - } - } -} diff --git a/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts b/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts deleted file mode 100644 index c5b2d3c1aa..0000000000 --- a/backend/src/serverless/integrations/usecases/__tests__/devto.api.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { getOrganizationArticles } from '../devto/getOrganizationArticles' -import { getArticleComments } from '../devto/getArticleComments' -import { getUserArticles } from '../devto/getUserArticles' -import { getUserById } from '../devto/getUser' - -function expectDefinedNumber(val: any) { - expect(val).toBeDefined() - expect(typeof val).toBe('number') -} - -function expectDefinedString(val: any) { - expect(val).toBeDefined() - expect(typeof val).toBe('string') -} - -function expectDefinedStringOrNull(val: any) { - expect(val).toBeDefined() - expect(typeof val === 'string' || val === null).toBeTruthy() -} - -function expectDefinedArray(val: any) { - expect(val).toBeDefined() - expect(Array.isArray(val)).toBeTruthy() -} - -describe('Devto API tests', () => { - const organization = 'digitalocean' - const organizationArticleId = 524804 - - const username = 'kukicado' - const userId = 139953 - - it('Should return correct required properties when fetching organization articles', async () => { - const articles = await getOrganizationArticles(organization, 1, 1) - - expect(articles.length).toEqual(1) - - const article = articles[0] - expectDefinedNumber(article.id) - expectDefinedString(article.title) - expectDefinedString(article.description) - expectDefinedString(article.readable_publish_date) - expectDefinedArray(article.tag_list) - expectDefinedString(article.slug) - expectDefinedString(article.url) - expectDefinedNumber(article.comments_count) - expectDefinedString(article.published_at) - expectDefinedString(article.last_comment_at) - }) - - it('Should return the correct required properties when fetching article comments', async () => { - const comments = await getArticleComments(organizationArticleId) - expect(comments.length > 0).toBeTruthy() - - const comment = comments[0] - expectDefinedString(comment.id_code) - expectDefinedString(comment.created_at) - expectDefinedString(comment.body_html) - expectDefinedString(comment.body_html) - expectDefinedArray(comment.children) - expect(comment.user).toBeDefined() - - // check comment user properties - expectDefinedNumber(comment.user.user_id) - expectDefinedString(comment.user.name) - expectDefinedString(comment.user.username) - expectDefinedStringOrNull(comment.user.twitter_username) - expectDefinedStringOrNull(comment.user.github_username) - expectDefinedStringOrNull(comment.user.website_url) - expectDefinedString(comment.user.profile_image) - expectDefinedString(comment.user.profile_image_90) - }) - - it('Should return the correct required properties when fetching user articles', async () => { - const articles = await getUserArticles(username, 1, 1) - - expect(articles.length).toEqual(1) - - const article = articles[0] - expectDefinedNumber(article.id) - expectDefinedString(article.title) - expectDefinedString(article.description) - expectDefinedString(article.readable_publish_date) - expectDefinedArray(article.tag_list) - expectDefinedString(article.slug) - expectDefinedString(article.url) - expectDefinedNumber(article.comments_count) - expectDefinedString(article.published_at) - expectDefinedString(article.last_comment_at) - }) - - it('Should return the correct required properties when fetching a user', async () => { - const user = await getUserById(userId) - - expectDefinedNumber(user.id) - expectDefinedString(user.name) - expectDefinedString(user.username) - expectDefinedStringOrNull(user.twitter_username) - expectDefinedStringOrNull(user.github_username) - expectDefinedStringOrNull(user.website_url) - expectDefinedStringOrNull(user.location) - expectDefinedStringOrNull(user.summary) - expectDefinedString(user.profile_image) - }) -}) diff --git a/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts b/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts deleted file mode 100644 index 698b3c5008..0000000000 --- a/backend/src/serverless/integrations/usecases/__tests__/isInvalid.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import isInvalid from '../isInvalid' - -describe('Is invalid tests', () => { - it('It should return valid when the result is correct', async () => { - const result = { - value: { - followers: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'followers')).toBe(false) - }) - - it('It should also work for other keys', async () => { - const result = { - value: { - mentions: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'mentions')).toBe(false) - }) - - it('It return invalid when no value also work for other keys', async () => { - const result = { - broken: true, - } - expect(isInvalid(result, 'mentions')).toBe(true) - }) - - it('It return invalid when no key', async () => { - const result = { - value: { - broken: true, - }, - } - expect(isInvalid(result, 'mentions')).toBe(true) - }) - - it('It return invalid when wrong key', async () => { - const result = { - value: { - mentions: [1, 2, 3], - nextPage: '', - }, - } - expect(isInvalid(result, 'followers')).toBe(true) - }) - - it('It return valid when empty list', async () => { - const result = { - value: { - mentions: [], - nextPage: '', - }, - } - expect(isInvalid(result, 'mentions')).toBe(false) - }) -}) diff --git a/backend/src/serverless/integrations/webhooks/__tests__/events.ts b/backend/src/serverless/integrations/webhooks/__tests__/events.ts deleted file mode 100644 index d736d70c8a..0000000000 --- a/backend/src/serverless/integrations/webhooks/__tests__/events.ts +++ /dev/null @@ -1,10585 +0,0 @@ -export default class TestEvents { - public static issues = { - event: 'issues', - opened: { - action: 'opened', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T16:07:31Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T15:36:54Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7101, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - edited: { - action: 'edited', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 267, - title: 'test issue (EDITED)', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:54:22Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - changes: { - title: { - from: 'test issue no3', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - reopened: { - action: 'reopened', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T16:07:31Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T15:36:54Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7101, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - closed: { - action: 'closed', - issue: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29', - repository_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/labels{/name}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/comments', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/events', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/issues/29', - id: 1173761480, - node_id: 'I_kwDOGsy6M85F9i3I', - number: 29, - title: 'New title', - user: { - login: 'mariobalca', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'closed', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 0, - created_at: '2022-03-18T16:07:31Z', - updated_at: '2022-03-18T19:01:48Z', - closed_at: '2022-03-18T19:01:48Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'Body here', - reactions: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/29/timeline', - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T18:07:14Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7121, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - } - - static pullRequestReviews = { - event: 'pull_request_review', - submitted: { - action: 'submitted', - review: { - id: 1420752578, - node_id: 'PRR_kwDOHksjGM5UrvbC', - user: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - body: '', - commit_id: '22db2e60206ee7445e457685367407f540553e50', - submitted_at: '2023-05-10T14:15:24Z', - state: 'approved', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843#pullrequestreview-1420752578', - pull_request_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - author_association: 'CONTRIBUTOR', - _links: { - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/843#pullrequestreview-1420752578', - }, - pull_request: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - }, - }, - }, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - id: 1344074031, - node_id: 'PR_kwDOHksjGM5QHPEv', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/843.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843', - number: 843, - state: 'open', - locked: false, - title: 'Fix custom activity type filter', - user: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nBug: Due to this previous change ([PR](https://github.com/CrowdDotDev/crowd.dev/pull/823/files#diff-987f06572bad79c47ef62d518a7a4e419dda775d942d365da55dc5509acf5291R44)), the `activityType` and `platform` were being stored in lowerCase in all activities. However, in the settings object it remained with the original value. So currently there is an inconsistency between custom activity type keys in activities and settings.\r\nTo fix this I\'m lowerCasing custom activity type and platform keys on their creation and migrating the existing ones to lowerCase as well.\r\n\r\nExample of problem:\r\nTenant settings:\r\n```\r\n{\r\n "Conference": {\r\n "Registered to a conference": {\r\n "display": {\r\n "short": "Registered to a conference",\r\n "channel": "",\r\n "default": "Registered to a conference"\r\n },\r\n "isContribution": false\r\n },\r\n },\r\n "other": {\r\n "This is a test": {\r\n "display": {\r\n "short": "This is a test",\r\n "channel": "",\r\n "default": "This is a test"\r\n },\r\n "isContribution": false\r\n }\r\n }\r\n}\r\n```\r\nActivity payload:\r\n`platform` and `activityType` fields:\r\n```\r\nplatform: \'luma\',\r\nactivityType: \'registered to a conference\'\r\n\r\nor\r\n\r\nplatform: \'other\',\r\nactivityType: \'this is a test\'\r\n```\r\n\r\nWith these changes the activity payload should remain the same but the tenant settings should be fixed to:\r\n```\r\n{\r\n "conference": {\r\n "registered to a conference": {\r\n "display": {\r\n "short": "Registered to a conference",\r\n "channel": "",\r\n "default": "Registered to a conference"\r\n },\r\n "isContribution": false\r\n },\r\n },\r\n "other": {\r\n "this is a test": {\r\n "display": {\r\n "short": "This is a test",\r\n "channel": "",\r\n "default": "This is a test"\r\n },\r\n "isContribution": false\r\n }\r\n }\r\n}\r\n```\r\n\r\n### What\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\nThe pull request standardizes the keys for custom activity types to use lowercase in the `settings` table and the `SettingsService` class. This avoids case sensitivity issues and improves data consistency across the application.\r\n​\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\n> _`customActivityTypes`_\r\n> _Lowercase keys for all seasons_\r\n> _`typeKey` follows_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\r\n### 🤖 Generated by Copilot at b11dfa1\r\n\r\n* Standardize the keys for custom activity types to use lowercase in the database and the service layer ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-a9626422cfa5c6888ed594d5114bffc0c4113699b7f39d1c4c456da8bd72c812L1-R13), [link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-2908c7bb18ca4494942ee153161abc5555bdd9516fc2d225a406d785b5787711L24-R24))\r\n* Update the `customActivityTypes` column in the `settings` table using the SQL script `V1683627959__customActivityTypesKeys.sql` ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-a9626422cfa5c6888ed594d5114bffc0c4113699b7f39d1c4c456da8bd72c812L1-R13))\r\n* Modify the `typeKey` variable in the `SettingsService` class to match the database format ([link](https://github.com/CrowdDotDev/crowd.dev/pull/843/files?diff=unified&w=0#diff-2908c7bb18ca4494942ee153161abc5555bdd9516fc2d225a406d785b5787711L24-R24))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-09T16:52:03Z', - updated_at: '2023-05-10T14:15:25Z', - closed_at: null, - merged_at: null, - merge_commit_sha: '358d960c38d27bb9377ff2ed71f2a416433656c8', - assignee: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/22db2e60206ee7445e457685367407f540553e50', - head: { - label: 'CrowdDotDev:bug/activity-type-filter', - ref: 'bug/activity-type-filter', - sha: '22db2e60206ee7445e457685367407f540553e50', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: 'ceb9b6ed619cf09ddd4c5f6c41913d1e1a4607e7', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/843', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/843/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/843/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/22db2e60206ee7445e457685367407f540553e50', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-10T07:55:10Z', - pushed_at: '2023-05-10T13:25:16Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24469, - stargazers_count: 535, - watchers_count: 535, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 48, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 81, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 48, - open_issues: 81, - watchers: 535, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static pullRequestReviewThreadComment = { - event: '', - created: { - action: 'created', - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420', - pull_request_review_id: 1424328394, - id: 1192271420, - node_id: 'PRRC_kwDOHksjGM5HEJ48', - diff_hunk: - '@@ -29,9 +29,7 @@\n \n \n \n- \n+ ', - path: 'frontend/src/modules/activity/pages/activity-list-page.vue', - commit_id: '97bd5109ff1eed738baa728cc03c91687be393bc', - original_commit_id: '97bd5109ff1eed738baa728cc03c91687be393bc', - user: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - body: "Shouldn't we only add this once we have everything ready? So that you can merge this PR without any blocker because it doesn't update anything in the UI?", - created_at: '2023-05-12T11:57:03Z', - updated_at: '2023-05-12T12:01:35Z', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853#discussion_r1192271420', - pull_request_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - author_association: 'CONTRIBUTOR', - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/853#discussion_r1192271420', - }, - pull_request: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - }, - }, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments/1192271420/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - start_line: null, - original_start_line: null, - start_side: null, - line: 32, - original_line: 32, - side: 'RIGHT', - original_position: 7, - position: 7, - subject_type: 'line', - }, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - id: 1347649482, - node_id: 'PR_kwDOHksjGM5QU3_K', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/853.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853', - number: 853, - state: 'open', - locked: false, - title: 'Filters configs & basic logic & typescript', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: "# Changes proposed ✍️\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\nThis pull request adds TypeScript support to the frontend and introduces a new `cr-filter` component to enable more flexible filtering options for the activity and member modules. It also updates some dependencies and adjusts the code accordingly. The pull request affects the following files: `frontend/.eslintrc.js`, `frontend/package.json`, `frontend/src/main.ts`, `frontend/src/modules/activity/pages/activity-list-page.vue`, and several files under `frontend/src/modules/activity/config/filters` and `frontend/src/modules/member/config/filters`.\r\n​\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\n> _We're breaking the chains of the old code base_\n> _We're rising from the ashes with TypeScript and Vue_\n> _We're filtering the data with custom components_\n> _We're the masters of the frontend, we're the chosen few_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at 950f2ce\n\n* Added TypeScript support and linting rules to the project ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dL11-R16), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dL39-R47), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dR71), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-093b2d5625f20b6d845fe73f1af12762fc64ba9d0b9d94b14e8aa287e2d2ed1dR80), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655R100), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL3-R5), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-880807f0963877836924cebde24ebb161aefc0458ab92f7dadf867e12b7075b4L22-R22))\n* Updated `vue` and downgraded `webpack` dependencies to avoid compatibility issues ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L62-R62), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-da6498268e99511d9ba0df3c13e439d10556a812881c9d03955b2ef7c6c1c655L71-R83))\n* Added custom filter components and configurations for the activity and member modules using the `cr-filter` component ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-f7e958bff5ca82a24a2bcc809b0d5868c76f6c7378c61dcb0b8bd287a646edd2R1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-cd8e780c8488cffad4194da57b9385504fa0d90b0b7084b6523dc9a05795a651R1-R22), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-69b2549353f117ee8aa475b258a2b7e5c4d1dabf070389e6fb0cc4515961bf2bR1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a9caf8fef766c417b77c51900f046e8bbab2f34427bd53ac5a3ee24fa50febe8R1-R22), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-000975af376ec51052285dec3052d0d2b7b3b5bb5a32c937973df3719fab0af7R1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-eef270ab4e86feefead9e965f6749b81fabc04515ae0d016c9b617b49c67bfc4R1-R32), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-23ca53439be6cf4f27ec40bc2c06f04ea84246c16d275d2f87590ec836448089R1-R16), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-e2ef4c4cfd836c42d5d9305bfb8920729b1ac2076a16d0d9a1d431429732b038R1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-acaec04bcea58bbc89fb09cdcf9c13486baeb8031fbf36443bdf9a63194cee40R1-R35), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-3a3c6c5d10f2d009d72f8eba26e51c75b589b33911727ad0e566568b574e201aR1-R21), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-7bb1b7dc4c35d5e952e9185fe927581811c55dc23a9dc510d496abff0b41aeb1R1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L32-R32), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L67-R65), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R72-R73), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R79), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58L85-R86), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0ce6547bee0155186a048d673be3dafaab869e51b92902f7c48d524d07467c58R96), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-0b2256f166e26e7a5fe115419cf20ec23d7cea5aed42e3b7fc9cc00292678fedR1-R31), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-59768c401b689d0104e67273c59a560ab299fbcb88d3c52dc2b4cd3e42ed02ccR1-R23), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-f95205f0059ef9af70f69e3d1e744b7255e6892e74e4938b214149a612524223R1-R22))\n* Added type annotations and casts to avoid TypeScript errors in the `main.ts` file and the Hotjar script ([link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL50-R56), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL62-R64), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL84-R87), [link](https://github.com/CrowdDotDev/crowd.dev/pull/853/files?diff=unified&w=0#diff-a235fca1f464b6af0fb049401b64264244eda9d624234c1fee162f6c663cee7bL94-R113))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n", - created_at: '2023-05-11T19:54:58Z', - updated_at: '2023-05-12T12:01:36Z', - closed_at: null, - merged_at: null, - merge_commit_sha: '3e84dcd09fc73ba6abffbed93ce874f833eddc9b', - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/97bd5109ff1eed738baa728cc03c91687be393bc', - head: { - label: 'CrowdDotDev:feature/filters-config', - ref: 'feature/filters-config', - sha: '97bd5109ff1eed738baa728cc03c91687be393bc', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:feature/filters', - ref: 'feature/filters', - sha: '3d953d3fa7f92c9ccdaf2247e5af0ea4b877651b', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/853', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/853/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/853/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/97bd5109ff1eed738baa728cc03c91687be393bc', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data for DevTool companies.', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-11T09:41:38Z', - pushed_at: '2023-05-12T11:26:53Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 24949, - stargazers_count: 536, - watchers_count: 536, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 49, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 82, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'community', - 'community-led-growth', - 'community-management', - 'developer-advocacy', - 'devrel', - 'javascript', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 49, - open_issues: 82, - watchers: 536, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static pullRequestReviewComments = { - event: 'pull_request_review_comment', - created: {}, - } - - static pullRequests = { - event: 'pull_request', - opened: { - action: 'opened', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'open', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:15:59Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - edited: { - action: 'edited', - number: 266, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - id: 1028781659, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - number: 266, - state: 'open', - locked: false, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints (edited)\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T15:06:38Z', - closed_at: null, - merged_at: null, - merge_commit_sha: 'b5e53951e4340c9c4ab03799d8a52450843d7c1b', - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/e7879626e65089f4ebbc0a1f36cdab54f54cef31', - head: { - label: 'CrowdDotDev:feature/nodejs-github-integration', - ref: 'feature/nodejs-github-integration', - sha: 'e7879626e65089f4ebbc0a1f36cdab54f54cef31', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - use_squash_pr_title_as_default: false, - squash_merge_commit_message: 'COMMIT_MESSAGES', - squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: '974da23a0edcb771de968cce75d2e8770a99e751', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - use_squash_pr_title_as_default: false, - squash_merge_commit_message: 'COMMIT_MESSAGES', - squash_merge_commit_title: 'COMMIT_OR_PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/e7879626e65089f4ebbc0a1f36cdab54f54cef31', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: true, - mergeable_state: 'unstable', - merged_by: null, - comments: 3, - review_comments: 0, - maintainer_can_modify: false, - commits: 13, - additions: 3005, - deletions: 126, - changed_files: 25, - }, - changes: { - body: { - from: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25880, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - reopened: { - action: 'reopened', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'open', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:15:59Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - closed: { - action: 'closed', - number: 30, - pull_request: { - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - id: 883733019, - node_id: 'PR_kwDOGsy6M840rLIb', - html_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - diff_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.diff', - patch_url: 'https://github.com/CrowdHQ/crowd-postgres/pull/30.patch', - issue_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - number: 30, - state: 'closed', - locked: false, - title: 'Feature/webhooks', - user: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - body: '# Description\r\n\r\n\r\n## Checklist 🔏\r\n\r\nSome important checks that must be ensured before merging:\r\n\r\n### Deploy to staging\r\n\r\n#### Deployment checklist\r\n\r\n- [ ] Make sure all AWS λ functions are up to date with your version (if applicable)\r\n- [ ] Make sure the anton-environment and soa-environment are updated and pushed\r\n\r\n\r\n### Functionality\r\n\r\n- [ ] Has the functionality been checked in a normal staging tenant? (not local)\r\n- [ ] Has the functionality been checked in the large tenant? (team+large@crowd.dev)\r\n- [ ] Has the functionality been checked in an empty tenant? (team+empty@crowd.dev)\r\n- [ ] Is there any more edge cases that should be taken into account?\r\n\r\n### Code quality\r\n\r\n- [ ] Are there comments in the main functionality of the code?\r\n- [ ] Are all tests passing?\r\n- [ ] Are all URLs to external services in `.env` files? Never hard-coded\r\n- [ ] Are all secrets in `anton-environment`? Never, ever hard-coded\r\n\r\n🔥🚀💪🏼\r\n', - created_at: '2022-03-18T19:15:59Z', - updated_at: '2022-03-18T19:18:54Z', - closed_at: '2022-03-18T19:18:54Z', - merged_at: null, - merge_commit_sha: '8b114a07a097b4903bde26918780edc0fc8ff707', - assignee: null, - assignees: [], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - head: { - label: 'CrowdHQ:feature/webhooks', - ref: 'feature/webhooks', - sha: 'a36398d1b6aa55a6614db6dec1690c1f21baaef1', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'CrowdHQ:main', - ref: 'main', - sha: 'cc785a28b2dfc28f58bf457669ce5859f8c30593', - user: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30', - }, - html: { - href: 'https://github.com/CrowdHQ/crowd-postgres/pull/30', - }, - issue: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30', - }, - comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/30/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls/30/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/a36398d1b6aa55a6614db6dec1690c1f21baaef1', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: false, - mergeable_state: 'clean', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 5, - additions: 1053, - deletions: 22, - changed_files: 11, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-09T10:53:04Z', - pushed_at: '2022-03-18T19:16:00Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7125, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - assigned: { - action: 'assigned', - number: 898, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898', - id: 1362932082, - node_id: 'PR_kwDOHksjGM5RPLFy', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/898.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898', - number: 898, - state: 'open', - locked: false, - title: 'Github issue closed support and improvements', - user: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\nThis pull request improves the GitHub integration service by using the correct source identifier for pull request and issue activities, and by fetching and parsing the closed events of the issues. It modifies the `githubIntegrationService.ts` and the `issues.ts` files to implement these changes.\r\n​\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\n> _`IssuesQuery` grows_\n> _Fetching timeline items now_\n> _Winter of closed bugs_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at ff1b5de\n\n* Modify the `sourceId` of the activities generated from pull request events to use the `sender.login` instead of the `user.login` of the pull request author, to reflect the user who performed the action ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L909-R909), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L919-R921), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L937-R937))\n* Declare and assign four variables (`sourceId`, `sourceParentId`, `body`, and `title`) at the beginning of the `parseWebhook` method in `githubIntegrationService.ts`, to be used later for generating activities based on the issue events, to avoid repeating the same logic and to make the code more readable and consistent ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1424-R1427), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1436-R1439), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1446-R1451), [link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L1459-R1474))\n* Modify the `parseWebhookMember` method call in `githubIntegrationService.ts`, to use the `sender.login` instead of the `issue.user.login`, to get the member information of the user who triggered the issue event, to match the logic of using the `sender.login` for the `sourceId` of the activity ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30L1449-R1459))\n* Add a call to the `parseIssueEvents` method in `githubIntegrationService.ts`, which is a new method that parses the timeline items of the issue and generates activities for each supported event type, such as `CLOSED_EVENT`, and concatenates the output array with the existing output array ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1514-R1522))\n* Add the implementation of the `parseIssueEvents` method in `githubIntegrationService.ts`, which iterates over the records of the timeline items, and uses a switch statement to handle different event types, and pushes a new activity object to the output array, with the appropriate properties and attributes based on the event type, and logs a warning message for any unsupported event type ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-93f2daaa968f5bec5c40cd31fc27ac6422ea3dcbdabecf3ef80f3c16802d1c30R1528-R1572))\n* Add a new field to the `IssuesQuery` class in `issues.ts`, which is a GraphQL query that fetches the issues from the GitHub API, to get the information of the closed events of the issues, such as the actor and the created date, which are needed for generating the activities in the `parseIssueEvents` method, and uses a fragment to select the relevant fields of the actor ([link](https://github.com/CrowdDotDev/crowd.dev/pull/898/files?diff=unified&w=0#diff-75aae4f028e206f107d0ea6dd3e99f71271f27f46f4cb260b199ba9dfdc9340bR24-R35))\r\n\r\n## Checklist ✅\r\n- [ ] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T11:40:14Z', - updated_at: '2023-05-24T13:11:07Z', - closed_at: null, - merged_at: null, - merge_commit_sha: 'c51ad2b5557f314f5567cc2c0be85bfc6a36c95e', - assignee: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [], - milestone: null, - draft: true, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/ff1b5de3e7ad22bba4941484bd5723b561374f98', - head: { - label: 'CrowdDotDev:improvement/gh-integration-issues-closed', - ref: 'improvement/gh-integration-issues-closed', - sha: 'ff1b5de3e7ad22bba4941484bd5723b561374f98', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: '73d23c92e24621a5153d3c378deca3c8e6f050b7', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/898', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/898/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/898/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/ff1b5de3e7ad22bba4941484bd5723b561374f98', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: true, - rebaseable: true, - mergeable_state: 'unstable', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 92, - deletions: 16, - changed_files: 2, - }, - assignee: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T11:40:15Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25942, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'epipav', - id: 12017738, - node_id: 'MDQ6VXNlcjEyMDE3NzM4', - avatar_url: 'https://avatars.githubusercontent.com/u/12017738?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/epipav', - html_url: 'https://github.com/epipav', - followers_url: 'https://api.github.com/users/epipav/followers', - following_url: 'https://api.github.com/users/epipav/following{/other_user}', - gists_url: 'https://api.github.com/users/epipav/gists{/gist_id}', - starred_url: 'https://api.github.com/users/epipav/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/epipav/subscriptions', - organizations_url: 'https://api.github.com/users/epipav/orgs', - repos_url: 'https://api.github.com/users/epipav/repos', - events_url: 'https://api.github.com/users/epipav/events{/privacy}', - received_events_url: 'https://api.github.com/users/epipav/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - review_requested: { - action: 'review_requested', - number: 897, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897', - id: 1362645865, - node_id: 'PR_kwDOHksjGM5ROFNp', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/897.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897', - number: 897, - state: 'open', - locked: false, - title: 'Boolean base filter', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nimage\r\n\r\n\r\n### What\r\ncopilot:summary\r\n​\r\ncopilot:poem\r\n\r\n### Why\r\n\r\n\r\n### How\r\ncopilot:walkthrough\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T08:36:33Z', - updated_at: '2023-05-24T08:36:33Z', - closed_at: null, - merged_at: null, - merge_commit_sha: null, - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/bf1e4d306886ce988cbe085f7496127c360362a0', - head: { - label: 'CrowdDotDev:feature/boolean-base', - ref: 'feature/boolean-base', - sha: 'bf1e4d306886ce988cbe085f7496127c360362a0', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:feature/filters', - ref: 'feature/filters', - sha: 'cdf5317cc76c0943a23827e4b06499c84d46fcbf', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/897', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/897/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/897/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/bf1e4d306886ce988cbe085f7496127c360362a0', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: false, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: null, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 179, - deletions: 32, - changed_files: 14, - }, - requested_reviewer: { - login: 'joanagmaia', - id: 20134207, - node_id: 'MDQ6VXNlcjIwMTM0MjA3', - avatar_url: 'https://avatars.githubusercontent.com/u/20134207?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanagmaia', - html_url: 'https://github.com/joanagmaia', - followers_url: 'https://api.github.com/users/joanagmaia/followers', - following_url: 'https://api.github.com/users/joanagmaia/following{/other_user}', - gists_url: 'https://api.github.com/users/joanagmaia/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanagmaia/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanagmaia/subscriptions', - organizations_url: 'https://api.github.com/users/joanagmaia/orgs', - repos_url: 'https://api.github.com/users/joanagmaia/repos', - events_url: 'https://api.github.com/users/joanagmaia/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanagmaia/received_events', - type: 'User', - site_admin: false, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:36:33Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 86, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 86, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - merged: { - action: 'merged', - number: 896, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896', - id: 1362439701, - node_id: 'PR_kwDOHksjGM5RNS4V', - html_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896', - diff_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd.dev/pull/896.patch', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896', - number: 896, - state: 'merged', - locked: false, - title: 'Hubspot book a call', - user: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - body: '# Changes proposed ✍️\r\nimage\r\n\r\n\r\n### What\r\n\n### 🤖 Generated by Copilot at a54ff06\n\nThis pull request adds the initial implementation of the HubSpot integration for the app, which allows users to book a custom plan consultation with CrowdDotDev. It adds a new `hubspot` module with its configuration and a custom connect component, and modifies the `integrations-config.js` and `integration-connect.vue` files to enable and render the integration.\r\n​\r\n\n### 🤖 Generated by Copilot at a54ff06\n\n> _We\'re building a bridge to the HubSpot realm_\n> _With a button of doom and a custom component_\n> _We\'re loading it dynamically with a conditional spell_\n> _We\'re adding it to the config, it\'s our final ascent_\r\n\r\n### Why\r\n\r\n\r\n### How\r\n\n### 🤖 Generated by Copilot at a54ff06\n\n* Add a new HubSpot integration that allows users to book a custom plan consultation with CrowdDotDev ([link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-7078523d220bab5012a66ec2faabe0cef0ed4d714385e62a16ff802afd2cce41R1-R13), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-de6ef036cf00503f519f1f15f641613fe22da0177f1e0b345e8aea6ac288a45eR1-R12), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-16679253103a6ebf5da26212a4a7bd72a61d6ee8c5e53776f43ba5b8756cc10eR1-R3), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-f07d5ed683ecfc23d0c65f6e2f467b3a7e5cf2aa76be9cb71d83f0994940b569R7), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-f07d5ed683ecfc23d0c65f6e2f467b3a7e5cf2aa76be9cb71d83f0994940b569R33), [link](https://github.com/CrowdDotDev/crowd.dev/pull/896/files?diff=unified&w=0#diff-24fb48f9bdeccd0436bd8106f405e6a176e9ae94a8a44295c48ad4c6c6b6974cR35-R39))\r\n\r\n## Checklist ✅\r\n- [x] Label appropriately with `Feature`, `Improvement`, or `Bug`.\r\n- [ ] Add screehshots to the PR description for relevant FE changes\r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)).\r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met.\r\n', - created_at: '2023-05-24T06:03:07Z', - updated_at: '2023-05-24T08:37:07Z', - closed_at: '2023-05-24T08:37:06Z', - merged_at: '2023-05-24T08:37:06Z', - merge_commit_sha: '73d23c92e24621a5153d3c378deca3c8e6f050b7', - assignee: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [], - requested_teams: [], - labels: [ - { - id: 4771856507, - node_id: 'LA_kwDOHksjGM8AAAABHGzAew', - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels/Feature', - name: 'Feature', - color: 'BB87FC', - default: false, - description: 'Created by Linear-GitHub Sync', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/commits', - review_comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/comments', - review_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896/comments', - statuses_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/a54ff069ff46231c30406d2d88d5af16b57894ef', - head: { - label: 'CrowdDotDev:feature/hubspot-book-call', - ref: 'feature/hubspot-book-call', - sha: 'a54ff069ff46231c30406d2d88d5af16b57894ef', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - base: { - label: 'CrowdDotDev:main', - ref: 'main', - sha: 'a9493e2d08ae7b53625a32db59fc0cce0a9f2d01', - user: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - allow_squash_merge: true, - allow_merge_commit: false, - allow_rebase_merge: false, - allow_auto_merge: false, - delete_branch_on_merge: true, - allow_update_branch: true, - use_squash_pr_title_as_default: true, - squash_merge_commit_message: 'BLANK', - squash_merge_commit_title: 'PR_TITLE', - merge_commit_message: 'PR_TITLE', - merge_commit_title: 'MERGE_MESSAGE', - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896', - }, - html: { - href: 'https://github.com/CrowdDotDev/crowd.dev/pull/896', - }, - issue: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896', - }, - comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/896/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls/896/commits', - }, - statuses: { - href: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/a54ff069ff46231c30406d2d88d5af16b57894ef', - }, - }, - author_association: 'CONTRIBUTOR', - auto_merge: null, - active_lock_reason: null, - merged: true, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - comments: 0, - review_comments: 0, - maintainer_can_modify: false, - commits: 1, - additions: 35, - deletions: 0, - changed_files: 6, - }, - repository: { - id: 508240664, - node_id: 'R_kgDOHksjGA', - name: 'crowd.dev', - full_name: 'CrowdDotDev/crowd.dev', - private: false, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd.dev', - description: - 'An open-source platform to centralize community, product, and customer data in one place', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/merges', - archive_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd.dev/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd.dev/deployments', - created_at: '2022-06-28T09:46:29Z', - updated_at: '2023-05-24T08:35:21Z', - pushed_at: '2023-05-24T08:37:06Z', - git_url: 'git://github.com/CrowdDotDev/crowd.dev.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd.dev.git', - clone_url: 'https://github.com/CrowdDotDev/crowd.dev.git', - svn_url: 'https://github.com/CrowdDotDev/crowd.dev', - homepage: 'https://crowd.dev', - size: 25886, - stargazers_count: 554, - watchers_count: 554, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - has_discussions: true, - forks_count: 51, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 85, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: true, - is_template: false, - web_commit_signoff_required: false, - topics: [ - 'analytics', - 'cdp', - 'community', - 'community-driven', - 'community-led-growth', - 'community-management', - 'customer-data-platform', - 'developer-advocacy', - 'developer-led-growth', - 'developer-marketing', - 'developer-relations', - 'devrel', - 'javascript', - 'postgres', - 'python', - 'typescript', - 'vue', - ], - visibility: 'public', - forks: 51, - open_issues: 85, - watchers: 554, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: - 'Open-source community and data tools built to unlock community-led growth for developer tools.', - }, - sender: { - login: 'gaspergrom', - id: 15195228, - node_id: 'MDQ6VXNlcjE1MTk1MjI4', - avatar_url: 'https://avatars.githubusercontent.com/u/15195228?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/gaspergrom', - html_url: 'https://github.com/gaspergrom', - followers_url: 'https://api.github.com/users/gaspergrom/followers', - following_url: 'https://api.github.com/users/gaspergrom/following{/other_user}', - gists_url: 'https://api.github.com/users/gaspergrom/gists{/gist_id}', - starred_url: 'https://api.github.com/users/gaspergrom/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/gaspergrom/subscriptions', - organizations_url: 'https://api.github.com/users/gaspergrom/orgs', - repos_url: 'https://api.github.com/users/gaspergrom/repos', - events_url: 'https://api.github.com/users/gaspergrom/events{/privacy}', - received_events_url: 'https://api.github.com/users/gaspergrom/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 29211772, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjkyMTE3NzI=', - }, - }, - } - - static star = { - event: 'star', - created: { - action: 'created', - starred_at: '2022-03-20T16:13:07Z', - repository: { - id: 462431237, - node_id: 'R_kgDOG5AkBQ', - name: 'core', - full_name: 'CrowdHQ/core', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/core', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/core', - forks_url: 'https://api.github.com/repos/CrowdHQ/core/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/core/keys{/key_id}', - collaborators_url: 'https://api.github.com/repos/CrowdHQ/core/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/core/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/core/hooks', - issue_events_url: 'https://api.github.com/repos/CrowdHQ/core/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/core/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/core/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/core/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/core/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/core/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/core/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/core/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/core/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/core/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/core/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/core/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/core/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/core/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/core/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/core/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/core/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/core/comments{/number}', - issue_comment_url: 'https://api.github.com/repos/CrowdHQ/core/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/core/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/core/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/core/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/core/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/core/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/core/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/core/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/core/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/core/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/core/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/core/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/core/deployments', - created_at: '2022-02-22T18:47:48Z', - updated_at: '2022-03-20T16:13:07Z', - pushed_at: '2022-03-20T16:12:45Z', - git_url: 'git://github.com/CrowdHQ/core.git', - ssh_url: 'git@github.com:CrowdHQ/core.git', - clone_url: 'https://github.com/CrowdHQ/core.git', - svn_url: 'https://github.com/CrowdHQ/core', - homepage: null, - size: 37251, - stargazers_count: 1, - watchers_count: 1, - language: 'Python', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - deleted: { - action: 'deleted', - starred_at: null, - repository: { - id: 462431237, - node_id: 'R_kgDOG5AkBQ', - name: 'core', - full_name: 'CrowdHQ/core', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/core', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/core', - forks_url: 'https://api.github.com/repos/CrowdHQ/core/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/core/keys{/key_id}', - collaborators_url: 'https://api.github.com/repos/CrowdHQ/core/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/core/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/core/hooks', - issue_events_url: 'https://api.github.com/repos/CrowdHQ/core/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/core/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/core/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/core/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/core/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/core/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/core/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/core/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/core/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/core/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/core/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/core/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/core/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/core/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/core/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/core/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/core/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/core/comments{/number}', - issue_comment_url: 'https://api.github.com/repos/CrowdHQ/core/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/core/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/core/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/core/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/core/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/core/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/core/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/core/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/core/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/core/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/core/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/core/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/core/deployments', - created_at: '2022-02-22T18:47:48Z', - updated_at: '2022-03-20T16:13:05Z', - pushed_at: '2022-03-20T16:12:45Z', - git_url: 'git://github.com/CrowdHQ/core.git', - ssh_url: 'git@github.com:CrowdHQ/core.git', - clone_url: 'https://github.com/CrowdHQ/core.git', - svn_url: 'https://github.com/CrowdHQ/core', - homepage: null, - size: 37251, - stargazers_count: 0, - watchers_count: 0, - language: 'Python', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 2, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 2, - watchers: 0, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - }, - } - - static fork = { - event: 'fork', - created: { - forkee: { - id: 472040578, - node_id: 'R_kgDOHCLEgg', - name: 'awesome-community-building', - full_name: 'PipeTestOrg/awesome-community-building', - private: false, - owner: { - login: 'PipeTestOrg', - id: 99810325, - node_id: 'O_kgDOBfL8FQ', - avatar_url: 'https://avatars.githubusercontent.com/u/99810325?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/PipeTestOrg', - html_url: 'https://github.com/PipeTestOrg', - followers_url: 'https://api.github.com/users/PipeTestOrg/followers', - following_url: 'https://api.github.com/users/PipeTestOrg/following{/other_user}', - gists_url: 'https://api.github.com/users/PipeTestOrg/gists{/gist_id}', - starred_url: 'https://api.github.com/users/PipeTestOrg/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/PipeTestOrg/subscriptions', - organizations_url: 'https://api.github.com/users/PipeTestOrg/orgs', - repos_url: 'https://api.github.com/users/PipeTestOrg/repos', - events_url: 'https://api.github.com/users/PipeTestOrg/events{/privacy}', - received_events_url: 'https://api.github.com/users/PipeTestOrg/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/PipeTestOrg/awesome-community-building', - description: 'A curated list of awesome resources on building developer communities. 🥑', - fork: true, - url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building', - forks_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/forks', - keys_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/teams', - hooks_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/hooks', - issue_events_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues/events{/number}', - events_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/events', - assignees_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/assignees{/user}', - branches_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/branches{/branch}', - tags_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/tags', - blobs_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/blobs{/sha}', - git_tags_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/tags{/sha}', - git_refs_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/refs{/sha}', - trees_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/trees{/sha}', - statuses_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/statuses/{sha}', - languages_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/languages', - stargazers_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/stargazers', - contributors_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/contributors', - subscribers_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/subscribers', - subscription_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/subscription', - commits_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/contents/{+path}', - compare_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/merges', - archive_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/{archive_format}{/ref}', - downloads_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/downloads', - issues_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/issues{/number}', - pulls_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/notifications{?since,all,participating}', - labels_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/labels{/name}', - releases_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/releases{/id}', - deployments_url: - 'https://api.github.com/repos/PipeTestOrg/awesome-community-building/deployments', - created_at: '2022-03-20T16:42:47Z', - updated_at: '2022-02-14T11:14:39Z', - pushed_at: '2021-11-22T10:48:14Z', - git_url: 'git://github.com/PipeTestOrg/awesome-community-building.git', - ssh_url: 'git@github.com:PipeTestOrg/awesome-community-building.git', - clone_url: 'https://github.com/PipeTestOrg/awesome-community-building.git', - svn_url: 'https://github.com/PipeTestOrg/awesome-community-building', - homepage: null, - size: 4, - stargazers_count: 0, - watchers_count: 0, - language: null, - has_issues: false, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 0, - license: null, - allow_forking: true, - is_template: false, - topics: [], - visibility: 'public', - forks: 0, - open_issues: 0, - watchers: 0, - default_branch: 'main', - public: true, - }, - repository: { - id: 389910135, - node_id: 'MDEwOlJlcG9zaXRvcnkzODk5MTAxMzU=', - name: 'awesome-community-building', - full_name: 'CrowdHQ/awesome-community-building', - private: false, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/awesome-community-building', - description: 'A curated list of awesome resources on building developer communities. 🥑', - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building', - forks_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/events', - assignees_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/assignees{/user}', - branches_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/tags', - blobs_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/blobs{/sha}', - git_tags_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/tags{/sha}', - git_refs_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/refs{/sha}', - trees_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/trees{/sha}', - statuses_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/languages', - stargazers_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/stargazers', - contributors_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/contributors', - subscribers_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/subscribers', - subscription_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/subscription', - commits_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/git/commits{/sha}', - comments_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues/comments{/number}', - contents_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/merges', - archive_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/downloads', - issues_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/awesome-community-building/labels{/name}', - releases_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/releases{/id}', - deployments_url: - 'https://api.github.com/repos/CrowdHQ/awesome-community-building/deployments', - created_at: '2021-07-27T08:43:58Z', - updated_at: '2022-02-14T11:14:39Z', - pushed_at: '2021-11-22T10:48:14Z', - git_url: 'git://github.com/CrowdHQ/awesome-community-building.git', - ssh_url: 'git@github.com:CrowdHQ/awesome-community-building.git', - clone_url: 'https://github.com/CrowdHQ/awesome-community-building.git', - svn_url: 'https://github.com/CrowdHQ/awesome-community-building', - homepage: null, - size: 4, - stargazers_count: 6, - watchers_count: 6, - language: null, - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 3, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 0, - license: null, - allow_forking: true, - is_template: false, - topics: [], - visibility: 'public', - forks: 3, - open_issues: 0, - watchers: 6, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 99810325, - node_id: 'O_kgDOBfL8FQ', - avatar_url: 'https://avatars.githubusercontent.com/u/99810325?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/PipeTestOrg', - html_url: 'https://github.com/PipeTestOrg', - followers_url: 'https://api.github.com/users/PipeTestOrg/followers', - following_url: 'https://api.github.com/users/PipeTestOrg/following{/other_user}', - gists_url: 'https://api.github.com/users/PipeTestOrg/gists{/gist_id}', - starred_url: 'https://api.github.com/users/PipeTestOrg/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/PipeTestOrg/subscriptions', - organizations_url: 'https://api.github.com/users/PipeTestOrg/orgs', - repos_url: 'https://api.github.com/users/PipeTestOrg/repos', - events_url: 'https://api.github.com/users/PipeTestOrg/events{/privacy}', - received_events_url: 'https://api.github.com/users/PipeTestOrg/received_events', - type: 'Organization', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMyODkwMjI=', - }, - }, - } - - static comment = { - event: 'issue_comment', - issue: { - created: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85P-Pt6', - number: 267, - title: 'test issue no3', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:22:44Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/issues/267#issuecomment-1221555775', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - id: 1221555775, - node_id: 'IC_kwDOGsy6M85Iz3Y_', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:22:44Z', - updated_at: '2022-08-21T14:22:44Z', - author_association: 'CONTRIBUTOR', - body: 'A test comment', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - changes: { - body: { - from: 'A test comment', - }, - }, - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/issues/267', - id: 1341717370, - node_id: 'I_kwDOGsy6M85P-Pt6', - number: 267, - title: 'test issue no3', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 2, - created_at: '2022-08-17T12:50:27Z', - updated_at: '2022-08-21T14:24:48Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: null, - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267/timeline', - performed_via_github_app: null, - state_reason: 'reopened', - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/issues/267#issuecomment-1221555775', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/267', - id: 1221555775, - node_id: 'IC_kwDOGsy6M85Iz3Y_', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:22:44Z', - updated_at: '2022-08-21T14:24:48Z', - author_association: 'CONTRIBUTOR', - body: 'A test comment (EDITED)', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221555775/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - }, - pullRequest: { - created: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - id: 1341697727, - node_id: 'PR_kwDOGsy6M849UfZb', - number: 266, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 3, - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T14:34:09Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - draft: false, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - merged_at: null, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/timeline', - performed_via_github_app: null, - state_reason: null, - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/pull/266#issuecomment-1221557738', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - id: 1221557738, - node_id: 'IC_kwDOGsy6M85Iz33q', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:34:09Z', - updated_at: '2022-08-21T14:34:09Z', - author_association: 'CONTRIBUTOR', - body: 'a test pr comment', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - changes: { - body: { - from: 'a test pr comment', - }, - }, - issue: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - labels_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/labels{/name}', - comments_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/comments', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/events', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - id: 1341697727, - node_id: 'PR_kwDOGsy6M849UfZb', - number: 266, - title: 'Feature/nodejs GitHub integration', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 4374821209, - node_id: 'LA_kwDOGsy6M88AAAABBMJ5WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:enhancement%20%E2%9C%A8', - name: 'type:enhancement ✨', - color: 'B57798', - default: false, - description: '', - }, - { - id: 4374821465, - node_id: 'LA_kwDOGsy6M88AAAABBMJ6WQ', - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels/type:feature%20%F0%9F%9A%80', - name: 'type:feature 🚀', - color: 'A3DF2C', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: null, - assignees: [], - milestone: null, - comments: 3, - created_at: '2022-08-17T12:35:00Z', - updated_at: '2022-08-21T14:35:08Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - draft: false, - pull_request: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls/266', - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266', - diff_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.diff', - patch_url: 'https://github.com/CrowdDotDev/crowd-postgres/pull/266.patch', - merged_at: null, - }, - body: "# Changes proposed ✍️\r\n- github integration moved from python to nodejs\r\n- new github endpoint: discussions for webhooks and iterator\r\n- issue comments, pr comments and discussion comments now processed as separate endpoints\r\n \r\n## Checklist ✅\r\n- [x] Label appropriately with `type:feature 🚀`, `type:enhancement ✨`, `type:bug 🐞`, or `type:documentation 📜`.\r\n- [ ] Tests are passing. \r\n- [ ] New backend functionality has been unit-tested.\r\n- [ ] Environment variables have been updated\r\n - [ ] Front-end: `frontend/.env.dist`\r\n - [ ] Backend: `backend/.env.dist`, `backend/.env.dist.staging`, `backend/.env.dist.staging`.\r\n - [ ] [Configuration docs](https://docs.crowd.dev/docs/configuration) have been updated.\r\n - [ ] Team members only: update environment variables in Password manager and update the team\r\n- [ ] API documentation has been updated (if necessary) (see [docs on API documentation](https://docs.crowd.dev/docs/updating-api-documentation)). \r\n- [ ] [Quality standards](https://github.com/CrowdDotDev/crowd-github-test-public/blob/main/CONTRIBUTING.md#quality-standards) are met. \r\n- [ ] All changes have been tested in a staging site. \r\n- [ ] All changes are working locally running crowd.dev's Docker local environment.", - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266/timeline', - performed_via_github_app: null, - state_reason: null, - }, - comment: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/pull/266#issuecomment-1221557738', - issue_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/266', - id: 1221557738, - node_id: 'IC_kwDOGsy6M85Iz33q', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-21T14:34:09Z', - updated_at: '2022-08-21T14:35:08Z', - author_association: 'CONTRIBUTOR', - body: 'a test pr comment (EDITED)', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/1221557738/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-21T13:49:08Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25872, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 6, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 6, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - }, - } - - static failed = [ - { - event: 'pull_request', - payload: { - action: 'closed', - number: 18, - pull_request: { - url: 'https://api.github.com/repos/verida/vault-common/pulls/18', - id: 879216015, - node_id: 'PR_kwDOEPAOdM40Z8WP', - html_url: 'https://github.com/verida/vault-common/pull/18', - diff_url: 'https://github.com/verida/vault-common/pull/18.diff', - patch_url: 'https://github.com/verida/vault-common/pull/18.patch', - issue_url: 'https://api.github.com/repos/verida/vault-common/issues/18', - number: 18, - state: 'closed', - locked: false, - title: '#315: Better error handling', - user: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - body: null, - created_at: '2022-03-14T15:39:38Z', - updated_at: '2022-03-20T15:16:47Z', - closed_at: '2022-03-20T15:16:47Z', - merged_at: '2022-03-20T15:16:47Z', - merge_commit_sha: 'b92db9efdf320ef414cc7437ab9bda2436093b2d', - assignee: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_reviewers: [ - { - login: 'saadibrahim', - id: 4365774, - node_id: 'MDQ6VXNlcjQzNjU3NzQ=', - avatar_url: 'https://avatars.githubusercontent.com/u/4365774?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/saadibrahim', - html_url: 'https://github.com/saadibrahim', - followers_url: 'https://api.github.com/users/saadibrahim/followers', - following_url: 'https://api.github.com/users/saadibrahim/following{/other_user}', - gists_url: 'https://api.github.com/users/saadibrahim/gists{/gist_id}', - starred_url: 'https://api.github.com/users/saadibrahim/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/saadibrahim/subscriptions', - organizations_url: 'https://api.github.com/users/saadibrahim/orgs', - repos_url: 'https://api.github.com/users/saadibrahim/repos', - events_url: 'https://api.github.com/users/saadibrahim/events{/privacy}', - received_events_url: 'https://api.github.com/users/saadibrahim/received_events', - type: 'User', - site_admin: false, - }, - { - login: 'ram-verida', - id: 97266181, - node_id: 'U_kgDOBcwqBQ', - avatar_url: 'https://avatars.githubusercontent.com/u/97266181?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ram-verida', - html_url: 'https://github.com/ram-verida', - followers_url: 'https://api.github.com/users/ram-verida/followers', - following_url: 'https://api.github.com/users/ram-verida/following{/other_user}', - gists_url: 'https://api.github.com/users/ram-verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ram-verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ram-verida/subscriptions', - organizations_url: 'https://api.github.com/users/ram-verida/orgs', - repos_url: 'https://api.github.com/users/ram-verida/repos', - events_url: 'https://api.github.com/users/ram-verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/ram-verida/received_events', - type: 'User', - site_admin: false, - }, - ], - requested_teams: [], - labels: [ - { - id: 2245374491, - node_id: 'MDU6TGFiZWwyMjQ1Mzc0NDkx', - url: 'https://api.github.com/repos/verida/vault-common/labels/enhancement', - name: 'enhancement', - color: 'a2eeef', - default: true, - description: 'New feature or request', - }, - ], - milestone: null, - draft: false, - commits_url: 'https://api.github.com/repos/verida/vault-common/pulls/18/commits', - review_comments_url: 'https://api.github.com/repos/verida/vault-common/pulls/18/comments', - review_comment_url: - 'https://api.github.com/repos/verida/vault-common/pulls/comments{/number}', - comments_url: 'https://api.github.com/repos/verida/vault-common/issues/18/comments', - statuses_url: - 'https://api.github.com/repos/verida/vault-common/statuses/e2605b438d4e3910613e914b07937e304cdce3ce', - head: { - label: 'verida:feature/315_better_error_handling', - ref: 'feature/315_better_error_handling', - sha: 'e2605b438d4e3910613e914b07937e304cdce3ce', - user: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: - 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - base: { - label: 'verida:develop', - ref: 'develop', - sha: '25fc567eb360c30e0496aa62abaa305721a70776', - user: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - repo: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: - 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - allow_squash_merge: true, - allow_merge_commit: true, - allow_rebase_merge: true, - allow_auto_merge: false, - delete_branch_on_merge: false, - allow_update_branch: false, - }, - }, - _links: { - self: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18', - }, - html: { - href: 'https://github.com/verida/vault-common/pull/18', - }, - issue: { - href: 'https://api.github.com/repos/verida/vault-common/issues/18', - }, - comments: { - href: 'https://api.github.com/repos/verida/vault-common/issues/18/comments', - }, - review_comments: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18/comments', - }, - review_comment: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/comments{/number}', - }, - commits: { - href: 'https://api.github.com/repos/verida/vault-common/pulls/18/commits', - }, - statuses: { - href: 'https://api.github.com/repos/verida/vault-common/statuses/e2605b438d4e3910613e914b07937e304cdce3ce', - }, - }, - author_association: 'COLLABORATOR', - auto_merge: null, - active_lock_reason: null, - merged: true, - mergeable: null, - rebaseable: null, - mergeable_state: 'unknown', - merged_by: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - comments: 0, - review_comments: 2, - maintainer_can_modify: false, - commits: 3, - additions: 512, - deletions: 1141, - changed_files: 4, - }, - repository: { - id: 284167796, - node_id: 'MDEwOlJlcG9zaXRvcnkyODQxNjc3OTY=', - name: 'vault-common', - full_name: 'verida/vault-common', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/vault-common', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/vault-common', - forks_url: 'https://api.github.com/repos/verida/vault-common/forks', - keys_url: 'https://api.github.com/repos/verida/vault-common/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/vault-common/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/vault-common/teams', - hooks_url: 'https://api.github.com/repos/verida/vault-common/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/vault-common/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/vault-common/events', - assignees_url: 'https://api.github.com/repos/verida/vault-common/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/vault-common/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/vault-common/tags', - blobs_url: 'https://api.github.com/repos/verida/vault-common/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/vault-common/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/vault-common/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/vault-common/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/vault-common/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/vault-common/languages', - stargazers_url: 'https://api.github.com/repos/verida/vault-common/stargazers', - contributors_url: 'https://api.github.com/repos/verida/vault-common/contributors', - subscribers_url: 'https://api.github.com/repos/verida/vault-common/subscribers', - subscription_url: 'https://api.github.com/repos/verida/vault-common/subscription', - commits_url: 'https://api.github.com/repos/verida/vault-common/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/verida/vault-common/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/vault-common/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/vault-common/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/vault-common/contents/{+path}', - compare_url: 'https://api.github.com/repos/verida/vault-common/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/vault-common/merges', - archive_url: 'https://api.github.com/repos/verida/vault-common/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/vault-common/downloads', - issues_url: 'https://api.github.com/repos/verida/vault-common/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/vault-common/pulls{/number}', - milestones_url: 'https://api.github.com/repos/verida/vault-common/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/vault-common/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/vault-common/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/vault-common/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/vault-common/deployments', - created_at: '2020-08-01T02:07:23Z', - updated_at: '2022-02-20T22:38:56Z', - pushed_at: '2022-03-20T15:16:47Z', - git_url: 'git://github.com/verida/vault-common.git', - ssh_url: 'git@github.com:verida/vault-common.git', - clone_url: 'https://github.com/verida/vault-common.git', - svn_url: 'https://github.com/verida/vault-common', - homepage: null, - size: 994, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 0, - default_branch: 'develop', - }, - organization: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - url: 'https://api.github.com/orgs/verida', - repos_url: 'https://api.github.com/orgs/verida/repos', - events_url: 'https://api.github.com/orgs/verida/events', - hooks_url: 'https://api.github.com/orgs/verida/hooks', - issues_url: 'https://api.github.com/orgs/verida/issues', - members_url: 'https://api.github.com/orgs/verida/members{/member}', - public_members_url: 'https://api.github.com/orgs/verida/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - description: '', - }, - sender: { - login: 'pkhien95', - id: 12524553, - node_id: 'MDQ6VXNlcjEyNTI0NTUz', - avatar_url: 'https://avatars.githubusercontent.com/u/12524553?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/pkhien95', - html_url: 'https://github.com/pkhien95', - followers_url: 'https://api.github.com/users/pkhien95/followers', - following_url: 'https://api.github.com/users/pkhien95/following{/other_user}', - gists_url: 'https://api.github.com/users/pkhien95/gists{/gist_id}', - starred_url: 'https://api.github.com/users/pkhien95/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/pkhien95/subscriptions', - organizations_url: 'https://api.github.com/users/pkhien95/orgs', - repos_url: 'https://api.github.com/users/pkhien95/repos', - events_url: 'https://api.github.com/users/pkhien95/events{/privacy}', - received_events_url: 'https://api.github.com/users/pkhien95/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMzMzkwNjQ=', - }, - }, - }, - { - event: 'issue_comment', - payload: { - action: 'created', - issue: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/16', - repository_url: 'https://api.github.com/repos/verida/blockchain-research', - labels_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/labels{/name}', - comments_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/comments', - events_url: 'https://api.github.com/repos/verida/blockchain-research/issues/16/events', - html_url: 'https://github.com/verida/blockchain-research/issues/16', - id: 1147572838, - node_id: 'I_kwDOGO8S3s5EZpJm', - number: 16, - title: '[research] Biconomy gasless transactions', - user: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - labels: [ - { - id: 3461134068, - node_id: 'LA_kwDOGO8S3s7OTLb0', - url: 'https://api.github.com/repos/verida/blockchain-research/labels/research', - name: 'research', - color: '97EB12', - default: false, - description: '', - }, - ], - state: 'open', - locked: false, - assignee: { - login: 'ITStar10', - id: 15656252, - node_id: 'MDQ6VXNlcjE1NjU2MjUy', - avatar_url: 'https://avatars.githubusercontent.com/u/15656252?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ITStar10', - html_url: 'https://github.com/ITStar10', - followers_url: 'https://api.github.com/users/ITStar10/followers', - following_url: 'https://api.github.com/users/ITStar10/following{/other_user}', - gists_url: 'https://api.github.com/users/ITStar10/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ITStar10/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ITStar10/subscriptions', - organizations_url: 'https://api.github.com/users/ITStar10/orgs', - repos_url: 'https://api.github.com/users/ITStar10/repos', - events_url: 'https://api.github.com/users/ITStar10/events{/privacy}', - received_events_url: 'https://api.github.com/users/ITStar10/received_events', - type: 'User', - site_admin: false, - }, - assignees: [ - { - login: 'ITStar10', - id: 15656252, - node_id: 'MDQ6VXNlcjE1NjU2MjUy', - avatar_url: 'https://avatars.githubusercontent.com/u/15656252?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/ITStar10', - html_url: 'https://github.com/ITStar10', - followers_url: 'https://api.github.com/users/ITStar10/followers', - following_url: 'https://api.github.com/users/ITStar10/following{/other_user}', - gists_url: 'https://api.github.com/users/ITStar10/gists{/gist_id}', - starred_url: 'https://api.github.com/users/ITStar10/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/ITStar10/subscriptions', - organizations_url: 'https://api.github.com/users/ITStar10/orgs', - repos_url: 'https://api.github.com/users/ITStar10/repos', - events_url: 'https://api.github.com/users/ITStar10/events{/privacy}', - received_events_url: 'https://api.github.com/users/ITStar10/received_events', - type: 'User', - site_admin: false, - }, - ], - milestone: null, - comments: 4, - created_at: '2022-02-23T02:50:27Z', - updated_at: '2022-03-20T06:45:13Z', - closed_at: null, - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: "The Verida network will enable end users to access Verida smart contracts by either:\r\n\r\n- Paying no gas (Sponsored gas fees)\r\n- Paying gas with Verida tokens (VDA token gas fees)\r\n\r\nBiconomy supports both of these strategies. However, Verida does not have a tradeable token so it's not feasible to pay gas fees using the VDA token when we launch.\r\n\r\nAs such, we need to implement sponsored gas fees for phase 1 and in the future look at paying gas with Verida tokens.\r\n\r\n## Research required\r\n\r\n1. Biconomy supports [multiple implementation methods](https://docs.biconomy.io/products/enable-gasless-transactions) for gasless transactions where Verida pays for transactions. We need to investigate and list the pros and cons of each.\r\n2. Create a proof of concept that implements gasless transactions on Polygon to pay for the creation of a DID document using the [vda-did-registry](https://github.com/verida/blockchain-vda-did-registry) smart contract.\r\n3. Give some thought on how we can protect ourselves from spammers. ie: Someone creating 1 million DID documents to exhaust our token pool that has been deposited to fund these transactions.\r\n\r\n## Background information\r\n\r\n- [Biconomy Getting Started Documentation](https://docs.biconomy.io/)\r\n- @tahpot has spoken with Biconomy and we have access to their engineering team if we have questions or require support", - reactions: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/16/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/16/timeline', - performed_via_github_app: null, - }, - comment: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/comments/1073180195', - html_url: - 'https://github.com/verida/blockchain-research/issues/16#issuecomment-1073180195', - issue_url: 'https://api.github.com/repos/verida/blockchain-research/issues/16', - id: 1073180195, - node_id: 'IC_kwDOGO8S3s4_924j', - user: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-03-20T06:45:13Z', - updated_at: '2022-03-20T06:45:13Z', - author_association: 'CONTRIBUTOR', - body: '@ITStar10 How much work will it be to make a generic server that will pay for any transaction on a whitelist of smart contracts?', - reactions: { - url: 'https://api.github.com/repos/verida/blockchain-research/issues/comments/1073180195/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - performed_via_github_app: null, - }, - repository: { - id: 418321118, - node_id: 'R_kgDOGO8S3g', - name: 'blockchain-research', - full_name: 'verida/blockchain-research', - private: true, - owner: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/verida', - html_url: 'https://github.com/verida', - followers_url: 'https://api.github.com/users/verida/followers', - following_url: 'https://api.github.com/users/verida/following{/other_user}', - gists_url: 'https://api.github.com/users/verida/gists{/gist_id}', - starred_url: 'https://api.github.com/users/verida/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/verida/subscriptions', - organizations_url: 'https://api.github.com/users/verida/orgs', - repos_url: 'https://api.github.com/users/verida/repos', - events_url: 'https://api.github.com/users/verida/events{/privacy}', - received_events_url: 'https://api.github.com/users/verida/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/verida/blockchain-research', - description: null, - fork: false, - url: 'https://api.github.com/repos/verida/blockchain-research', - forks_url: 'https://api.github.com/repos/verida/blockchain-research/forks', - keys_url: 'https://api.github.com/repos/verida/blockchain-research/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/verida/blockchain-research/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/verida/blockchain-research/teams', - hooks_url: 'https://api.github.com/repos/verida/blockchain-research/hooks', - issue_events_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/events{/number}', - events_url: 'https://api.github.com/repos/verida/blockchain-research/events', - assignees_url: 'https://api.github.com/repos/verida/blockchain-research/assignees{/user}', - branches_url: 'https://api.github.com/repos/verida/blockchain-research/branches{/branch}', - tags_url: 'https://api.github.com/repos/verida/blockchain-research/tags', - blobs_url: 'https://api.github.com/repos/verida/blockchain-research/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/verida/blockchain-research/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/verida/blockchain-research/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/verida/blockchain-research/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/verida/blockchain-research/statuses/{sha}', - languages_url: 'https://api.github.com/repos/verida/blockchain-research/languages', - stargazers_url: 'https://api.github.com/repos/verida/blockchain-research/stargazers', - contributors_url: 'https://api.github.com/repos/verida/blockchain-research/contributors', - subscribers_url: 'https://api.github.com/repos/verida/blockchain-research/subscribers', - subscription_url: 'https://api.github.com/repos/verida/blockchain-research/subscription', - commits_url: 'https://api.github.com/repos/verida/blockchain-research/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/verida/blockchain-research/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/verida/blockchain-research/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/verida/blockchain-research/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/verida/blockchain-research/contents/{+path}', - compare_url: - 'https://api.github.com/repos/verida/blockchain-research/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/verida/blockchain-research/merges', - archive_url: - 'https://api.github.com/repos/verida/blockchain-research/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/verida/blockchain-research/downloads', - issues_url: 'https://api.github.com/repos/verida/blockchain-research/issues{/number}', - pulls_url: 'https://api.github.com/repos/verida/blockchain-research/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/verida/blockchain-research/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/verida/blockchain-research/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/verida/blockchain-research/labels{/name}', - releases_url: 'https://api.github.com/repos/verida/blockchain-research/releases{/id}', - deployments_url: 'https://api.github.com/repos/verida/blockchain-research/deployments', - created_at: '2021-10-18T02:48:02Z', - updated_at: '2022-02-18T00:33:59Z', - pushed_at: '2022-03-20T06:32:29Z', - git_url: 'git://github.com/verida/blockchain-research.git', - ssh_url: 'git@github.com:verida/blockchain-research.git', - clone_url: 'https://github.com/verida/blockchain-research.git', - svn_url: 'https://github.com/verida/blockchain-research', - homepage: null, - size: 753, - stargazers_count: 0, - watchers_count: 0, - language: 'TypeScript', - has_issues: true, - has_projects: false, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 13, - license: null, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 13, - watchers: 0, - default_branch: 'main', - }, - organization: { - login: 'verida', - id: 59584064, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjU5NTg0MDY0', - url: 'https://api.github.com/orgs/verida', - repos_url: 'https://api.github.com/orgs/verida/repos', - events_url: 'https://api.github.com/orgs/verida/events', - hooks_url: 'https://api.github.com/orgs/verida/hooks', - issues_url: 'https://api.github.com/orgs/verida/issues', - members_url: 'https://api.github.com/orgs/verida/members{/member}', - public_members_url: 'https://api.github.com/orgs/verida/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/59584064?v=4', - description: '', - }, - sender: { - login: 'tahpot', - id: 164973, - node_id: 'MDQ6VXNlcjE2NDk3Mw==', - avatar_url: 'https://avatars.githubusercontent.com/u/164973?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/tahpot', - html_url: 'https://github.com/tahpot', - followers_url: 'https://api.github.com/users/tahpot/followers', - following_url: 'https://api.github.com/users/tahpot/following{/other_user}', - gists_url: 'https://api.github.com/users/tahpot/gists{/gist_id}', - starred_url: 'https://api.github.com/users/tahpot/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/tahpot/subscriptions', - organizations_url: 'https://api.github.com/users/tahpot/orgs', - repos_url: 'https://api.github.com/users/tahpot/repos', - events_url: 'https://api.github.com/users/tahpot/events{/privacy}', - received_events_url: 'https://api.github.com/users/tahpot/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjMzMzkwNjQ=', - }, - }, - }, - ] - - static webhookVerify = { - action: 'started', - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdHQ/crowd-postgres', - private: true, - owner: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdHQ', - html_url: 'https://github.com/CrowdHQ', - followers_url: 'https://api.github.com/users/CrowdHQ/followers', - following_url: 'https://api.github.com/users/CrowdHQ/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdHQ/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdHQ/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdHQ/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdHQ/orgs', - repos_url: 'https://api.github.com/users/CrowdHQ/repos', - events_url: 'https://api.github.com/users/CrowdHQ/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdHQ/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdHQ/crowd-postgres', - description: null, - fork: false, - url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/commits{/sha}', - git_commits_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/contents/{+path}', - compare_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/merges', - archive_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/pulls{/number}', - milestones_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdHQ/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdHQ/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-03-21T19:12:53Z', - pushed_at: '2022-03-21T18:06:08Z', - git_url: 'git://github.com/CrowdHQ/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdHQ/crowd-postgres.git', - clone_url: 'https://github.com/CrowdHQ/crowd-postgres.git', - svn_url: 'https://github.com/CrowdHQ/crowd-postgres', - homepage: null, - size: 7741, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 1, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 1, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdHQ', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdHQ', - repos_url: 'https://api.github.com/orgs/CrowdHQ/repos', - events_url: 'https://api.github.com/orgs/CrowdHQ/events', - hooks_url: 'https://api.github.com/orgs/CrowdHQ/hooks', - issues_url: 'https://api.github.com/orgs/CrowdHQ/issues', - members_url: 'https://api.github.com/orgs/CrowdHQ/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdHQ/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'joanreyero', - id: 37874460, - node_id: 'MDQ6VXNlcjM3ODc0NDYw', - avatar_url: 'https://avatars.githubusercontent.com/u/37874460?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/joanreyero', - html_url: 'https://github.com/joanreyero', - followers_url: 'https://api.github.com/users/joanreyero/followers', - following_url: 'https://api.github.com/users/joanreyero/following{/other_user}', - gists_url: 'https://api.github.com/users/joanreyero/gists{/gist_id}', - starred_url: 'https://api.github.com/users/joanreyero/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/joanreyero/subscriptions', - organizations_url: 'https://api.github.com/users/joanreyero/orgs', - repos_url: 'https://api.github.com/users/joanreyero/repos', - events_url: 'https://api.github.com/users/joanreyero/events{/privacy}', - received_events_url: 'https://api.github.com/users/joanreyero/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjM1ODU4MTY=', - }, - } - - static discussion = { - event: 'discussion', - created: { - action: 'created', - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: null, - answer_chosen_at: null, - answer_chosen_by: null, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 0, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:05:56Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:02:43Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - comment: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEBl', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-19T07:14:13Z', - body: 'answer to a question - EDITED', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - changes: { - body: { - from: 'answer to a question', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-19T06:50:13Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25359, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 7, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 7, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - answered: { - action: 'answered', - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - answer: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEBl', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-18T16:06:05Z', - body: 'answer to a question', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:26:28Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - } - - static discussionComment = { - event: 'discussion_comment', - created: { - action: 'created', - comment: { - id: 3424532, - node_id: 'DC_kwDOGsy6M84ANEEU', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424532', - parent_id: 3424357, - child_comment_count: 0, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:26:10Z', - updated_at: '2022-08-18T16:26:10Z', - body: 'a reply\r\n', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424532/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: null, - answer_chosen_at: null, - answer_chosen_by: null, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:26:10Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-18T16:24:41Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 24891, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 5, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 5, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - edited: { - action: 'edited', - comment: { - id: 3424357, - node_id: 'DC_kwDOGsy6M84ANEEU', - html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - parent_id: null, - child_comment_count: 1, - repository_url: 'CrowdDotDev/crowd-postgres', - discussion_id: 4315208, - author_association: 'CONTRIBUTOR', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - created_at: '2022-08-18T16:06:04Z', - updated_at: '2022-08-19T07:14:13Z', - body: 'answer to a question - EDITED', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments/3424357/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - }, - discussion: { - repository_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - category: { - id: 37972861, - node_id: 'DIC_kwDOGsy6M84CQ2t9', - repository_id: 449624627, - emoji: ':pray:', - name: 'Q&A', - description: 'Ask the community for help', - created_at: '2022-08-16T11:48:28.000+02:00', - updated_at: '2022-08-16T11:48:28.000+02:00', - slug: 'q-a', - is_answerable: true, - }, - answer_html_url: - 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270#discussioncomment-3424357', - answer_chosen_at: '2022-08-18T18:41:39.000+02:00', - answer_chosen_by: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres/discussions/270', - id: 4315208, - node_id: 'D_kwDOGsy6M84AQdhI', - number: 270, - title: 'test dicussion - QA', - user: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - state: 'open', - locked: false, - comments: 2, - created_at: '2022-08-18T16:05:56Z', - updated_at: '2022-08-18T16:41:39Z', - author_association: 'CONTRIBUTOR', - active_lock_reason: null, - body: 'asdasd', - reactions: { - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/reactions', - total_count: 0, - '+1': 0, - '-1': 0, - laugh: 0, - hooray: 0, - confused: 0, - heart: 0, - rocket: 0, - eyes: 0, - }, - timeline_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/discussions/270/timeline', - }, - changes: { - body: { - from: 'answer to a question', - }, - }, - repository: { - id: 449624627, - node_id: 'R_kgDOGsy6Mw', - name: 'crowd-postgres', - full_name: 'CrowdDotDev/crowd-postgres', - private: true, - owner: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/CrowdDotDev', - html_url: 'https://github.com/CrowdDotDev', - followers_url: 'https://api.github.com/users/CrowdDotDev/followers', - following_url: 'https://api.github.com/users/CrowdDotDev/following{/other_user}', - gists_url: 'https://api.github.com/users/CrowdDotDev/gists{/gist_id}', - starred_url: 'https://api.github.com/users/CrowdDotDev/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/CrowdDotDev/subscriptions', - organizations_url: 'https://api.github.com/users/CrowdDotDev/orgs', - repos_url: 'https://api.github.com/users/CrowdDotDev/repos', - events_url: 'https://api.github.com/users/CrowdDotDev/events{/privacy}', - received_events_url: 'https://api.github.com/users/CrowdDotDev/received_events', - type: 'Organization', - site_admin: false, - }, - html_url: 'https://github.com/CrowdDotDev/crowd-postgres', - description: 'temporary monorepo (until oss launch)', - fork: false, - url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres', - forks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/forks', - keys_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/keys{/key_id}', - collaborators_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/collaborators{/collaborator}', - teams_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/teams', - hooks_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/hooks', - issue_events_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/events{/number}', - events_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/events', - assignees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/assignees{/user}', - branches_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/branches{/branch}', - tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/tags', - blobs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/blobs{/sha}', - git_tags_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/tags{/sha}', - git_refs_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/refs{/sha}', - trees_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/trees{/sha}', - statuses_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/statuses/{sha}', - languages_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/languages', - stargazers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/stargazers', - contributors_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contributors', - subscribers_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscribers', - subscription_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/subscription', - commits_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/commits{/sha}', - git_commits_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/git/commits{/sha}', - comments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/comments{/number}', - issue_comment_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues/comments{/number}', - contents_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/contents/{+path}', - compare_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/compare/{base}...{head}', - merges_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/merges', - archive_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/{archive_format}{/ref}', - downloads_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/downloads', - issues_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/issues{/number}', - pulls_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/pulls{/number}', - milestones_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/milestones{/number}', - notifications_url: - 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/notifications{?since,all,participating}', - labels_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/labels{/name}', - releases_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/releases{/id}', - deployments_url: 'https://api.github.com/repos/CrowdDotDev/crowd-postgres/deployments', - created_at: '2022-01-19T09:22:07Z', - updated_at: '2022-08-12T18:29:08Z', - pushed_at: '2022-08-19T06:50:13Z', - git_url: 'git://github.com/CrowdDotDev/crowd-postgres.git', - ssh_url: 'git@github.com:CrowdDotDev/crowd-postgres.git', - clone_url: 'https://github.com/CrowdDotDev/crowd-postgres.git', - svn_url: 'https://github.com/CrowdDotDev/crowd-postgres', - homepage: '', - size: 25359, - stargazers_count: 1, - watchers_count: 1, - language: 'TypeScript', - has_issues: true, - has_projects: true, - has_downloads: true, - has_wiki: true, - has_pages: false, - forks_count: 0, - mirror_url: null, - archived: false, - disabled: false, - open_issues_count: 7, - license: { - key: 'other', - name: 'Other', - spdx_id: 'NOASSERTION', - url: null, - node_id: 'MDc6TGljZW5zZTA=', - }, - allow_forking: false, - is_template: false, - web_commit_signoff_required: false, - topics: [], - visibility: 'private', - forks: 0, - open_issues: 7, - watchers: 1, - default_branch: 'main', - }, - organization: { - login: 'CrowdDotDev', - id: 85551972, - node_id: 'MDEyOk9yZ2FuaXphdGlvbjg1NTUxOTcy', - url: 'https://api.github.com/orgs/CrowdDotDev', - repos_url: 'https://api.github.com/orgs/CrowdDotDev/repos', - events_url: 'https://api.github.com/orgs/CrowdDotDev/events', - hooks_url: 'https://api.github.com/orgs/CrowdDotDev/hooks', - issues_url: 'https://api.github.com/orgs/CrowdDotDev/issues', - members_url: 'https://api.github.com/orgs/CrowdDotDev/members{/member}', - public_members_url: 'https://api.github.com/orgs/CrowdDotDev/public_members{/member}', - avatar_url: 'https://avatars.githubusercontent.com/u/85551972?v=4', - description: '', - }, - sender: { - login: 'anilb0stanci', - id: 94853297, - node_id: 'U_kgDOBadYsQ', - avatar_url: 'https://avatars.githubusercontent.com/u/94853297?v=4', - gravatar_id: '', - url: 'https://api.github.com/users/anilb0stanci', - html_url: 'https://github.com/anilb0stanci', - followers_url: 'https://api.github.com/users/anilb0stanci/followers', - following_url: 'https://api.github.com/users/anilb0stanci/following{/other_user}', - gists_url: 'https://api.github.com/users/anilb0stanci/gists{/gist_id}', - starred_url: 'https://api.github.com/users/anilb0stanci/starred{/owner}{/repo}', - subscriptions_url: 'https://api.github.com/users/anilb0stanci/subscriptions', - organizations_url: 'https://api.github.com/users/anilb0stanci/orgs', - repos_url: 'https://api.github.com/users/anilb0stanci/repos', - events_url: 'https://api.github.com/users/anilb0stanci/events{/privacy}', - received_events_url: 'https://api.github.com/users/anilb0stanci/received_events', - type: 'User', - site_admin: false, - }, - installation: { - id: 23585816, - node_id: 'MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjQzMTA5NTc=', - }, - }, - } -} diff --git a/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts b/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts index f66833339c..645407dd04 100644 --- a/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts +++ b/backend/src/serverless/integrations/workers/sendgridWebhookWorker.ts @@ -1,15 +1,13 @@ import { getServiceChildLogger } from '@crowd/logging' -import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook' import { PlatformType } from '@crowd/types' +import { EventWebhook, EventWebhookHeader } from '@sendgrid/eventwebhook' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import { IS_PROD_ENV, SENDGRID_CONFIG } from '../../../conf' import SequelizeRepository from '../../../database/repositories/sequelizeRepository' import UserRepository from '../../../database/repositories/userRepository' import getUserContext from '../../../database/utils/getUserContext' import EagleEyeContentService from '../../../services/eagleEyeContentService' -import { NodeWorkerMessageBase } from '../../../types/mq/nodeWorkerMessageBase' import { SendgridWebhookEvent, SendgridWebhookEventType } from '../../../types/webhooks' -import { NodeWorkerMessageType } from '../../types/workerTypes' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' const log = getServiceChildLogger('sendgridWebhookWorker') @@ -46,13 +44,10 @@ export default async function sendgridWebhookWorker(req) { } } + const emitter = await getNodejsWorkerEmitter() for (const event of events) { if (event.sg_template_id === SENDGRID_CONFIG.templateEagleEyeDigest) { - await sendNodeWorkerMessage(event.sg_event_id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - event, - service: 'sendgrid-webhooks', - } as NodeWorkerMessageBase) + await emitter.sendgridWebhook(event) } } diff --git a/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts b/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts index 813d1c2f3a..b100860c50 100644 --- a/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts +++ b/backend/src/serverless/integrations/workers/stripeWebhookWorker.ts @@ -1,15 +1,13 @@ +import { timeout } from '@crowd/common' import { getServiceChildLogger } from '@crowd/logging' import { getRedisClient, RedisPubSubEmitter } from '@crowd/redis' +import { ApiWebsocketMessage } from '@crowd/types' import moment from 'moment' import { Stripe } from 'stripe' -import { timeout } from '@crowd/common' -import { ApiWebsocketMessage } from '@crowd/types' +import { getNodejsWorkerEmitter } from '@/serverless/utils/serviceSQS' import { PLANS_CONFIG, REDIS_CONFIG } from '../../../conf' import SequelizeRepository from '../../../database/repositories/sequelizeRepository' import Plans from '../../../security/plans' -import { NodeWorkerMessageBase } from '../../../types/mq/nodeWorkerMessageBase' -import { NodeWorkerMessageType } from '../../types/workerTypes' -import { sendNodeWorkerMessage } from '../../utils/nodeWorkerSQS' const log = getServiceChildLogger('stripeWebhookWorker') @@ -24,11 +22,8 @@ export default async function stripeWebhookWorker(req) { try { event = stripe.webhooks.constructEvent(req.rawBody, sig, PLANS_CONFIG.stripWebhookSigningSecret) - await sendNodeWorkerMessage(event.id, { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - event, - service: 'stripe-webhooks', - } as NodeWorkerMessageBase) + const emitter = await getNodejsWorkerEmitter() + await emitter.stripeWebhook(event) } catch (err) { log.error(`Webhook Error: ${err.message}`) return { diff --git a/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts b/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts index 0d1e77eee2..768ea45f1d 100644 --- a/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts +++ b/backend/src/serverless/microservices/nodejs/csv-export/csvExportWorker.ts @@ -6,10 +6,11 @@ import { Hash } from '@aws-sdk/hash-node' import { parseUrl } from '@aws-sdk/url-parser' import { formatUrl } from '@aws-sdk/util-format-url' import { getServiceChildLogger } from '@crowd/logging' +import { ExportableEntity } from '@crowd/types' import getUserContext from '../../../../database/utils/getUserContext' import EmailSender from '../../../../services/emailSender' import { S3_CONFIG } from '../../../../conf' -import { BaseOutput, ExportableEntity } from '../messageTypes' +import { BaseOutput } from '../messageTypes' import getStage from '../../../../services/helpers/getStage' import { s3 } from '../../../../services/aws' import MemberService from '../../../../services/memberService' diff --git a/backend/src/serverless/microservices/nodejs/messageTypes.ts b/backend/src/serverless/microservices/nodejs/messageTypes.ts index 30b9aa44dc..acf34ad08b 100644 --- a/backend/src/serverless/microservices/nodejs/messageTypes.ts +++ b/backend/src/serverless/microservices/nodejs/messageTypes.ts @@ -1,4 +1,4 @@ -import { AutomationTrigger, AutomationType } from '@crowd/types' +import { AutomationTrigger, AutomationType, ExportableEntity } from '@crowd/types' export type BaseNodeMicroserviceMessage = { service: string @@ -68,10 +68,6 @@ export interface AnalyticsEmailsOutput extends BaseOutput { emailSent: boolean } -export enum ExportableEntity { - MEMBERS = 'members', -} - export type BulkEnrichMessage = { service: string tenant: string diff --git a/backend/src/serverless/types/workerTypes.ts b/backend/src/serverless/types/workerTypes.ts index 9ab75a0917..91c9bc3dbd 100644 --- a/backend/src/serverless/types/workerTypes.ts +++ b/backend/src/serverless/types/workerTypes.ts @@ -1,9 +1,7 @@ export enum NodeWorkerMessageType { INTEGRATION_CHECK = 'integration_check', - INTEGRATION_PROCESS = 'integration_process', NODE_MICROSERVICE = 'node_microservice', DB_OPERATIONS = 'db_operations', - PROCESS_WEBHOOK = 'process_webhook', } export enum PythonWorkerMessageType { diff --git a/backend/src/serverless/utils/nodeWorkerSQS.ts b/backend/src/serverless/utils/nodeWorkerSQS.ts deleted file mode 100644 index 0bbe22ddd7..0000000000 --- a/backend/src/serverless/utils/nodeWorkerSQS.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { getServiceChildLogger } from '@crowd/logging' -import { SqsMessageAttributes, sendMessage } from '@crowd/sqs' -import { AutomationTrigger } from '@crowd/types' -import moment from 'moment' -import { IS_TEST_ENV, SQS_CONFIG } from '../../conf' -import { NodeWorkerMessageBase } from '../../types/mq/nodeWorkerMessageBase' -import { ExportableEntity } from '../microservices/nodejs/messageTypes' -import { NodeWorkerMessageType } from '../types/workerTypes' -import { SQS_CLIENT } from './serviceSQS' - -const log = getServiceChildLogger('nodeWorkerSQS') - -// 15 minute limit for delaying is max for SQS -const limitSeconds = 15 * 60 - -export const sendNodeWorkerMessage = async ( - tenantId: string, - body: NodeWorkerMessageBase, - delaySeconds?: number, - targetQueueUrl?: string, -): Promise => { - if (IS_TEST_ENV) { - return - } - - // we can only delay for 15 minutes then we have to re-delay message - let attributes: SqsMessageAttributes - let delay: number - let delayed = false - if (delaySeconds) { - if (delaySeconds > limitSeconds) { - // delay for 15 minutes and add the remaineder to the attributes - const remainedSeconds = delaySeconds - limitSeconds - attributes = { - tenantId: { - DataType: 'String', - StringValue: tenantId, - }, - remainingDelaySeconds: { - DataType: 'Number', - StringValue: `${remainedSeconds}`, - }, - } - - if (targetQueueUrl) { - attributes.targetQueueUrl = { DataType: 'String', StringValue: targetQueueUrl } - } - delay = limitSeconds - } else { - attributes = { - tenantId: { - DataType: 'String', - StringValue: tenantId, - }, - } - if (targetQueueUrl) { - attributes.targetQueueUrl = { DataType: 'String', StringValue: targetQueueUrl } - } - delay = delaySeconds - } - - delayed = true - } - - const now = moment().valueOf() - - const params = { - QueueUrl: delayed ? SQS_CONFIG.nodejsWorkerDelayableQueue : SQS_CONFIG.nodejsWorkerQueue, - MessageGroupId: delayed ? undefined : `${now}`, - MessageDeduplicationId: delayed ? undefined : `${tenantId}-${now}`, - MessageBody: JSON.stringify(body), - MessageAttributes: attributes, - DelaySeconds: delay, - } - - log.debug( - { - messageType: body.type, - body, - }, - 'Sending nodejs-worker sqs message!', - ) - await sendMessage(SQS_CLIENT(), params) -} - -export const sendNewActivityNodeSQSMessage = async ( - tenant: string, - activityId: string, - segmentId: string, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant, - activityId, - segmentId, - trigger: AutomationTrigger.NEW_ACTIVITY, - service: 'automation', - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendNewMemberNodeSQSMessage = async ( - tenant: string, - memberId: string, - segmentId: string, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - tenant, - memberId, - segmentId, - trigger: AutomationTrigger.NEW_MEMBER, - service: 'automation', - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendExportCSVNodeSQSMessage = async ( - tenant: string, - user: string, - entity: ExportableEntity, - segmentIds: string[], - criteria: any, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'csv-export', - user, - tenant, - entity, - criteria, - segmentIds, - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendBulkEnrichMessage = async ( - tenant: string, - memberIds: string[], - segmentIds: string[], - notifyFrontend: boolean = true, - skipCredits: boolean = false, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'bulk-enrich', - memberIds, - tenant, - segmentIds, - notifyFrontend, - skipCredits, - } - await sendNodeWorkerMessage(tenant, payload as NodeWorkerMessageBase) -} - -export const sendOrgMergeMessage = async ( - tenantId: string, - primaryOrgId: string, - secondaryOrgId: string, - notifyFrontend: boolean = true, -): Promise => { - const payload = { - type: NodeWorkerMessageType.NODE_MICROSERVICE, - service: 'org-merge', - tenantId, - primaryOrgId, - secondaryOrgId, - notifyFrontend, - } - await sendNodeWorkerMessage(tenantId, payload as NodeWorkerMessageBase) -} diff --git a/backend/src/serverless/utils/serviceSQS.ts b/backend/src/serverless/utils/serviceSQS.ts index 2f25f478b2..cfdf20d01c 100644 --- a/backend/src/serverless/utils/serviceSQS.ts +++ b/backend/src/serverless/utils/serviceSQS.ts @@ -1,15 +1,20 @@ +import { SqsClient, getSqsClient } from '@crowd/sqs' +import { getServiceChildLogger } from '@crowd/logging' +import { getServiceTracer } from '@crowd/tracing' import { IntegrationRunWorkerEmitter, IntegrationStreamWorkerEmitter, - IntegrationSyncWorkerEmitter, SearchSyncWorkerEmitter, DataSinkWorkerEmitter, - SqsClient, - getSqsClient, -} from '@crowd/sqs' -import { getServiceChildLogger } from '@crowd/logging' -import { getServiceTracer } from '@crowd/tracing' -import { SQS_CONFIG } from '../../conf' + IntegrationSyncWorkerEmitter, + QueuePriorityContextLoader, + NodejsWorkerEmitter, +} from '@crowd/common_services' +import { UnleashClient, getUnleashClient } from '@crowd/feature-flags' +import { RedisClient, getRedisClient } from '@crowd/redis' +import { REDIS_CONFIG, SERVICE, SQS_CONFIG, UNLEASH_CONFIG } from '../../conf' +import SequelizeRepository from '@/database/repositories/sequelizeRepository' +import { PriorityLevelContextRepository } from '@/database/repositories/priorityLevelContextRepository' const tracer = getServiceTracer() const log = getServiceChildLogger('service.sqs') @@ -29,11 +34,58 @@ export const SQS_CLIENT = (): SqsClient => { return sqsClient } +let unleashClient: UnleashClient | undefined +let unleashClientInitialized = false +const UNLEASH_CLIENT = async (): Promise => { + if (unleashClientInitialized) { + return unleashClient + } + + unleashClient = await getUnleashClient({ + url: UNLEASH_CONFIG.url, + apiKey: UNLEASH_CONFIG.backendApiKey, + appName: SERVICE, + }) + unleashClientInitialized = true + return unleashClient +} + +let redisClient: RedisClient +const REDIS_CLIENT = async (): Promise => { + if (redisClient) { + return redisClient + } + + redisClient = await getRedisClient(REDIS_CONFIG, true) + + return redisClient +} + +let loader: QueuePriorityContextLoader +export const QUEUE_PRIORITY_LOADER = async (): Promise => { + if (loader) { + return loader + } + + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + const repo = new PriorityLevelContextRepository(options) + + loader = (tenantId: string) => repo.loadPriorityLevelContext(tenantId) + return loader +} + let runWorkerEmitter: IntegrationRunWorkerEmitter export const getIntegrationRunWorkerEmitter = async (): Promise => { if (runWorkerEmitter) return runWorkerEmitter - runWorkerEmitter = new IntegrationRunWorkerEmitter(SQS_CLIENT(), tracer, log) + runWorkerEmitter = new IntegrationRunWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) await runWorkerEmitter.init() return runWorkerEmitter } @@ -43,7 +95,14 @@ export const getIntegrationStreamWorkerEmitter = async (): Promise => { if (streamWorkerEmitter) return streamWorkerEmitter - streamWorkerEmitter = new IntegrationStreamWorkerEmitter(SQS_CLIENT(), tracer, log) + streamWorkerEmitter = new IntegrationStreamWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) await streamWorkerEmitter.init() return streamWorkerEmitter } @@ -52,7 +111,14 @@ let searchSyncWorkerEmitter: SearchSyncWorkerEmitter export const getSearchSyncWorkerEmitter = async (): Promise => { if (searchSyncWorkerEmitter) return searchSyncWorkerEmitter - searchSyncWorkerEmitter = new SearchSyncWorkerEmitter(SQS_CLIENT(), tracer, log) + searchSyncWorkerEmitter = new SearchSyncWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) await searchSyncWorkerEmitter.init() return searchSyncWorkerEmitter } @@ -61,7 +127,14 @@ let integrationSyncWorkerEmitter: IntegrationSyncWorkerEmitter export const getIntegrationSyncWorkerEmitter = async (): Promise => { if (integrationSyncWorkerEmitter) return integrationSyncWorkerEmitter - integrationSyncWorkerEmitter = new IntegrationSyncWorkerEmitter(SQS_CLIENT(), tracer, log) + integrationSyncWorkerEmitter = new IntegrationSyncWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) await integrationSyncWorkerEmitter.init() return integrationSyncWorkerEmitter } @@ -70,7 +143,30 @@ let dataSinkWorkerEmitter: DataSinkWorkerEmitter export const getDataSinkWorkerEmitter = async (): Promise => { if (dataSinkWorkerEmitter) return dataSinkWorkerEmitter - dataSinkWorkerEmitter = new DataSinkWorkerEmitter(SQS_CLIENT(), tracer, log) + dataSinkWorkerEmitter = new DataSinkWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) await dataSinkWorkerEmitter.init() return dataSinkWorkerEmitter } + +let nodejsWorkerEmitter: NodejsWorkerEmitter +export const getNodejsWorkerEmitter = async (): Promise => { + if (nodejsWorkerEmitter) return nodejsWorkerEmitter + + nodejsWorkerEmitter = new NodejsWorkerEmitter( + SQS_CLIENT(), + await REDIS_CLIENT(), + tracer, + await UNLEASH_CLIENT(), + await QUEUE_PRIORITY_LOADER(), + log, + ) + await nodejsWorkerEmitter.init() + return nodejsWorkerEmitter +} diff --git a/backend/src/services/__tests__/activityService.test.ts b/backend/src/services/__tests__/activityService.test.ts deleted file mode 100644 index 72edf61759..0000000000 --- a/backend/src/services/__tests__/activityService.test.ts +++ /dev/null @@ -1,3193 +0,0 @@ -import { v4 as uuid } from 'uuid' - -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import MemberService from '../memberService' -import ActivityService from '../activityService' -import MemberRepository from '../../database/repositories/memberRepository' -import ActivityRepository from '../../database/repositories/activityRepository' -import ConversationService from '../conversationService' -import SequelizeRepository from '../../database/repositories/sequelizeRepository' -import { MemberAttributeName, PlatformType, SegmentStatus } from '@crowd/types' -import SettingsRepository from '../../database/repositories/settingsRepository' -import ConversationSettingsRepository from '../../database/repositories/conversationSettingsRepository' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import { IServiceOptions } from '../../services/IServiceOptions' -import { GITHUB_MEMBER_ATTRIBUTES, TWITTER_MEMBER_ATTRIBUTES } from '@crowd/integrations' -import { populateSegments, switchSegments } from '../../database/utils/segmentTestUtils' -import SegmentRepository from '../../database/repositories/segmentRepository' -import OrganizationRepository from '../../database/repositories/organizationRepository' -import OrganizationService from '../organizationService' -import MemberSegmentAffiliationRepository from '../../database/repositories/memberSegmentAffiliationRepository' -import SegmentService from '../segmentService' -import MemberAffiliationService from '../memberAffiliationService' - -const db = null -const searchEngine = null - -describe('ActivityService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should create non existent activity with no parent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - } - - const activityCreated = await new ActivityService(mockIRepositoryOptions).upsert(activity) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated.createdAt = activityCreated.createdAt.toISOString().split('T')[0] - activityCreated.updatedAt = activityCreated.updatedAt.toISOString().split('T')[0] - delete activityCreated.member - delete activityCreated.objectMember - - const expectedActivityCreated = { - id: activityCreated.id, - attributes: activity.attributes, - type: 'activity', - timestamp: new Date('2020-05-27T15:13:30Z'), - platform: PlatformType.GITHUB, - isContribution: true, - score: 1, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - channel: null, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - tasks: [], - parent: null, - parentId: null, - conversationId: null, - sourceId: activity.sourceId, - sourceParentId: null, - display: { - default: activityCreated.type, - short: activityCreated.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityCreated).toStrictEqual(expectedActivityCreated) - }) - - it('Should create non existent activity with parent', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity1 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - platform: 'non-existing-platform', - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - const activityCreated1 = await new ActivityService(mockIRepositoryOptions).upsert(activity1) - - const activity2 = { - type: 'answer', - timestamp: '2020-05-28T15:13:30Z', - platform: 'non-existing-platform', - body: 'Baby dont hurt me', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: activityCreated1.sourceId, - } - - const activityCreated2 = await new ActivityService(mockIRepositoryOptions).upsert(activity2) - - // Since an activity with a parent is created, a Conversation entity should be created at this point - // with both parent and the child activities. Try finding it using the slug - - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'what-is-love' }) - - delete activityCreated2.member - delete activityCreated2.parent - delete activityCreated2.objectMember - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityCreated2.createdAt = activityCreated2.createdAt.toISOString().split('T')[0] - activityCreated2.updatedAt = activityCreated2.updatedAt.toISOString().split('T')[0] - - const expectedActivityCreated = { - id: activityCreated2.id, - body: activity2.body, - type: activity2.type, - channel: null, - attributes: {}, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - url: null, - title: null, - timestamp: new Date(activity2.timestamp), - platform: activity2.platform, - isContribution: activity2.isContribution, - score: activity2.score, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - tasks: [], - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityCreated1.id, - sourceParentId: activity1.sourceId, - sourceId: activity2.sourceId, - conversationId: conversationCreated.rows[0].id, - display: { - default: activity2.type, - short: activity2.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityCreated2).toStrictEqual(expectedActivityCreated) - }) - - it('Should update already existing activity succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity1 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - body: 'What is love?', - title: 'Song', - platform: 'non-existing-platform', - attributes: { - nested_1: { - attribute_1: '1', - nested_2: { - attribute_2: '2', - attribute_array: [1, 2, 3], - }, - }, - }, - isContribution: true, - score: 1, - sourceId: '#sourceId1', - } - - const activityCreated1 = await new ActivityService(mockIRepositoryOptions).upsert(activity1) - - const activity2 = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: memberCreated.id, - platform: 'non-existing-platform', - body: 'Test', - attributes: { - nested_1: { - attribute_1: '1', - nested_2: { - attribute_2: '5', - attribute_3: 'test', - attribute_array: [3, 4, 5], - }, - }, - one: 'Baby dont hurt me', - two: 'Dont hurt me', - three: 'No more', - }, - isContribution: false, - score: 2, - sourceId: '#sourceId1', - } - - const activityUpserted = await new ActivityService(mockIRepositoryOptions).upsert(activity2) - - // Trim the hour part from timestamp so we can atleast test if the day is correct for createdAt and joinedAt - activityUpserted.createdAt = activityUpserted.createdAt.toISOString().split('T')[0] - activityUpserted.updatedAt = activityUpserted.updatedAt.toISOString().split('T')[0] - - // delete models before expect because we already have ids (memberId, parentId) - delete activityUpserted.member - delete activityUpserted.parent - delete activityUpserted.objectMember - - const attributesExpected = { - ...activity1.attributes, - ...activity2.attributes, - } - - attributesExpected.nested_1.nested_2.attribute_array = [1, 2, 3, 4, 5] - - const expectedActivityCreated = { - id: activityCreated1.id, - attributes: attributesExpected, - type: activity2.type, - timestamp: new Date(activity2.timestamp), - platform: activity2.platform, - isContribution: activity2.isContribution, - score: activity2.score, - title: activity1.title, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - url: null, - body: activity2.body, - channel: null, - username: 'test', - objectMemberUsername: null, - memberId: memberCreated.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - sourceParentId: null, - sourceId: activity1.sourceId, - conversationId: null, - display: { - default: activity2.type, - short: activity2.type, - channel: '', - }, - organizationId: null, - organization: null, - } - - expect(activityUpserted).toStrictEqual(expectedActivityCreated) - }) - - it('Should create various conversations successfully with given parent-child relationships of activities [ascending timestamp order]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const member1Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - const member2Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test2', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - // Simulate a reply chain in discord - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: member1Created.id, - platform: PlatformType.DISCORD, - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - let activityCreated1 = await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2020-05-28T15:14:30Z', - platform: PlatformType.DISCORD, - body: 'Baby dont hurt me', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: activityCreated1.sourceId, - } - - const activityCreated2 = await activityService.upsert(activity2) - - const activity3 = { - type: 'message', - timestamp: '2020-05-28T15:15:30Z', - platform: PlatformType.DISCORD, - body: 'Dont hurt me', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#3', - sourceParentId: activityCreated2.sourceId, - } - - const activityCreated3 = await activityService.upsert(activity3) - - const activity4 = { - type: 'message', - timestamp: '2020-05-28T15:16:30Z', - platform: PlatformType.DISCORD, - body: 'No more', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#4', - sourceParentId: activityCreated3.sourceId, - } - - const activityCreated4 = await activityService.upsert(activity4) - - // Get the conversation using slug (generated using the chain starter activity attributes.body) - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'what-is-love', - }) - ).rows[0] - - // We have to get activity1 again because conversation creation happens - // after creation of the first activity that has a parent (activity2) - activityCreated1 = await activityService.findById(activityCreated1.id) - - // All activities (including chain starter) should belong to the same conversation - expect(activityCreated1.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated2.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated3.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated4.conversationId).toStrictEqual(conversationCreated.id) - - // Emulate a thread in discord - - const activity5 = { - type: 'message', - timestamp: '2020-05-28T15:17:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna give you up', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#5', - } - let activityCreated5 = await activityService.upsert(activity5) - - const activity6 = { - type: 'message', - timestamp: '2020-05-28T15:18:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna let you down', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#6', - sourceParentId: activityCreated5.sourceId, - } - const activityCreated6 = await activityService.upsert(activity6) - - const activity7 = { - type: 'message', - timestamp: '2020-05-28T15:19:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna run around and desert you', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#7', - sourceParentId: activityCreated5.sourceId, - } - const activityCreated7 = await activityService.upsert(activity7) - - const conversationCreated2 = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'never-gonna-give-you-up', - }) - ).rows[0] - - activityCreated5 = await activityService.findById(activityCreated5.id) - - // All activities (including thread starter) should belong to the same conversation - expect(activityCreated5.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated6.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated7.conversationId).toStrictEqual(conversationCreated2.id) - }) - - it('Should keep old timestamp', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const cm = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - }) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - } - - const activityCreated1 = await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2022-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - body: 'What is love?', - } - - const activityCreated2 = await activityService.upsert(activity2) - - expect(activityCreated2.timestamp).toStrictEqual(activityCreated1.timestamp) - expect(activityCreated2.body).toBe(activity2.body) - }) - - it('Should keep isMainBranch as true', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const cm = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - }) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - attributes: { - isMainBranch: true, - other: 'other', - }, - } - - await activityService.upsert(activity1) - - const activity2 = { - type: 'message', - timestamp: '2022-05-27T15:13:30Z', - username: 'test', - member: cm.id, - platform: PlatformType.DISCORD, - sourceId: 'sourceId#1', - body: 'What is love?', - attributes: { - isMainBranch: false, - other2: 'other2', - }, - } - - const activityCreated2 = await activityService.upsert(activity2) - - expect(activityCreated2.attributes).toStrictEqual({ - isMainBranch: true, - other: 'other', - other2: 'other2', - }) - }) - - it('Should create various conversations successfully with given parent-child relationships of activities [descending timestamp order]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberService = new MemberService(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - - const member1Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const member2Created = await memberService.upsert({ - username: { - [PlatformType.DISCORD]: 'test2', - }, - platform: PlatformType.DISCORD, - joinedAt: '2020-05-27T15:13:30Z', - }) - - // Simulate a reply chain in discord in reverse order (child activities come first) - - const activity4 = { - type: 'message', - timestamp: '2020-05-28T15:16:30Z', - platform: PlatformType.DISCORD, - body: 'No more', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#4', - sourceParentId: 'sourceId#3', - } - - let activityCreated4 = await activityService.upsert(activity4) - - const activity3 = { - type: 'message', - timestamp: '2020-05-28T15:15:30Z', - platform: PlatformType.DISCORD, - body: 'Dont hurt me', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#3', - sourceParentId: 'sourceId#2', - } - - let activityCreated3 = await activityService.upsert(activity3) - - const activity2 = { - type: 'message', - timestamp: '2020-05-28T15:14:30Z', - platform: PlatformType.DISCORD, - body: 'Baby dont hurt me', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#2', - sourceParentId: 'sourceId#1', - } - - let activityCreated2 = await activityService.upsert(activity2) - - const activity1 = { - type: 'message', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - member: member1Created.id, - platform: PlatformType.DISCORD, - body: 'What is love?', - isContribution: true, - score: 1, - sourceId: 'sourceId#1', - } - - // main parent activity that starts the reply chain - let activityCreated1 = await activityService.upsert(activity1) - - // get activities again - activityCreated1 = await activityService.findById(activityCreated1.id) - activityCreated2 = await activityService.findById(activityCreated2.id) - activityCreated3 = await activityService.findById(activityCreated3.id) - activityCreated4 = await activityService.findById(activityCreated4.id) - - // expect parentIds - expect(activityCreated4.parentId).toBe(activityCreated3.id) - expect(activityCreated3.parentId).toBe(activityCreated2.id) - expect(activityCreated2.parentId).toBe(activityCreated1.id) - - // Get the conversation using slug (generated using the chain starter activity attributes.body -last added activityCreated1-) - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'what-is-love', - }) - ).rows[0] - - // All activities (including chain starter) should belong to the same conversation - expect(activityCreated1.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated2.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated3.conversationId).toStrictEqual(conversationCreated.id) - expect(activityCreated4.conversationId).toStrictEqual(conversationCreated.id) - - // Simulate a thread in reverse order - - const activity6 = { - type: 'message', - timestamp: '2020-05-28T15:18:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna let you down', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#6', - sourceParentId: 'sourceId#5', - } - let activityCreated6 = await activityService.upsert(activity6) - - const activity7 = { - type: 'message', - timestamp: '2020-05-28T15:19:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna run around and desert you', - - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#7', - sourceParentId: 'sourceId#5', - } - let activityCreated7 = await activityService.upsert(activity7) - - const activity5 = { - type: 'message', - timestamp: '2020-05-28T15:17:30Z', - platform: PlatformType.DISCORD, - body: 'Never gonna give you up', - isContribution: true, - username: 'test', - member: member1Created.id, - score: 2, - sourceId: 'sourceId#5', - } - let activityCreated5 = await activityService.upsert(activity5) - - const conversationCreated2 = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'never-gonna-give-you-up', - }) - ).rows[0] - - // get activities again - activityCreated5 = await activityService.findById(activityCreated5.id) - activityCreated6 = await activityService.findById(activityCreated6.id) - activityCreated7 = await activityService.findById(activityCreated7.id) - - // expect parentIds - expect(activityCreated6.parentId).toBe(activityCreated5.id) - expect(activityCreated7.parentId).toBe(activityCreated5.id) - - expect(activityCreated5.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated6.conversationId).toStrictEqual(conversationCreated2.id) - expect(activityCreated7.conversationId).toStrictEqual(conversationCreated2.id) - - // Add some more childs to the conversation1 and conversation2 - // After setting child-parent in reverse order, we're now adding - // some more childiren in normal order - - // add a new reply to the chain-starter activity - const activity8 = { - type: 'message', - timestamp: '2020-05-28T15:21:30Z', - platform: PlatformType.DISCORD, - body: 'additional reply to the reply chain', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#8', - sourceParentId: 'sourceId#1', - } - - const activityCreated8 = await activityService.upsert(activity8) - - expect(activityCreated8.parentId).toBe(activityCreated1.id) - expect(activityCreated8.conversationId).toStrictEqual(conversationCreated.id) - - // add a new activity to the thread - const activity9 = { - type: 'message', - timestamp: '2020-05-28T15:35:30Z', - platform: PlatformType.DISCORD, - body: 'additional message to the thread', - isContribution: true, - username: 'test2', - member: member2Created.id, - score: 2, - sourceId: 'sourceId#9', - sourceParentId: 'sourceId#5', - } - - const activityCreated9 = await activityService.upsert(activity9) - - expect(activityCreated9.parentId).toBe(activityCreated5.id) - expect(activityCreated9.conversationId).toStrictEqual(conversationCreated2.id) - }) - - // Tests for checking channel logic when creating activity - // Settings should get updated only when a new channel is sent alog while creating activity. - it('Should create an activity with a channel which is not present in settings and add it to settings', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test1', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test1', - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - const segmentRepository = new SegmentRepository(mockIRepositoryOptions) - const subprojectIds = (await segmentRepository.querySubprojects({})).rows.map((s) => s.id) - const activityChannels = await segmentRepository.fetchTenantActivityChannels(subprojectIds) - expect(activityChannels[activity.platform].includes(activity.channel)).toBe(true) - }) - - it('Should not create a duplicate channel when a channel is present in settings', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test1', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - username: 'test1', - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - let settings = await SettingsRepository.findOrCreateDefault({}, mockIRepositoryOptions) - const activity1 = { - type: 'activity1', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Body', - title: 'Title', - url: 'URL', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - channel: 'TestChannel', - attributes: { - replies: 12, - }, - sourceId: '#sourceId', - isContribution: true, - member: memberCreated.id, - score: 1, - } - - await new ActivityService(mockIRepositoryOptions).upsert(activity) - const segmentRepository = new SegmentRepository(mockIRepositoryOptions) - const subprojectIds = (await segmentRepository.querySubprojects({})).rows.map((s) => s.id) - const activityChannels = await segmentRepository.fetchTenantActivityChannels(subprojectIds) - expect(activityChannels[activity1.platform].length).toBe(1) - }) - }) - - describe('createWithMember method', () => { - it('Create an activity with given member [no parent activity]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - sentiment: 0.98, - label: 'positive', - }, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember( - data, - ) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - type: data.type, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: data.sentiment, - attributes: {}, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceParentId: null, - sourceId: data.sourceId, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - }) - - it('Create an activity with given member [with parent activity, upsert member, new activity] [parent first, child later]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: 'anil_github', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember1 = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - const data2 = { - member, - body: 'Description\nMinor pull request that fixes the order by Score and # of activities in the members list page', - title: 'Add order by score and # of activities', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/30', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-11-30T14:20:27.000Z', - type: 'pull_request-open', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId2', - sourceParentId: data.sourceId, - } - - const activityWithMember2 = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data2) - - // Since an activity with a parent is created, a Conversation entity should be created at this point - // with both parent and the child activities. Try finding it using the slug (slug is generated using parent.attributes.body) - - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'description-this-pull-request-adds-a-new-dashboard-and-related' }) - - // delete models before expect because we already have ids (memberId, parentId) - delete activityWithMember2.member - delete activityWithMember2.parent - delete activityWithMember2.display - delete activityWithMember2.objectMember - - activityWithMember2.createdAt = activityWithMember2.createdAt.toISOString().split('T')[0] - activityWithMember2.updatedAt = activityWithMember2.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember1.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember2.id, - body: data2.body, - title: data2.title, - url: data2.url, - channel: data2.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data2.type, - timestamp: new Date(data2.timestamp), - platform: data2.platform, - tasks: [], - isContribution: data2.isContribution, - score: data2.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityWithMember1.id, - sourceParentId: data2.sourceParentId, - sourceId: data2.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMember2).toStrictEqual(expectedActivityCreated) - }) - - it('Create an activity with given member [with parent activity, upsert member, new activity] [child first, parent later]', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const activityService = new ActivityService(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: 'anil_github', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const dataChild = { - member, - body: 'Description\nMinor pull request that fixes the order by Score and # of activities in the members list page', - title: 'Add order by score and # of activities', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/30', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-11-30T14:20:27.000Z', - type: 'pull_request-open', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceParentId: '#sourceId1', - sourceId: '#childSourceId', - } - - let activityWithMemberChild = await activityService.createWithMember(dataChild) - - const dataParent = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: dataChild.sourceParentId, - } - - let activityWithMemberParent = await activityService.createWithMember(dataParent) - - // after creating parent, conversation should be started - const conversationCreated = await new ConversationService( - mockIRepositoryOptions, - ).findAndCountAll({ slug: 'description-this-pull-request-adds-a-new-dashboard-and-related' }) - - // get child and parent activity again - activityWithMemberChild = await activityService.findById(activityWithMemberChild.id) - activityWithMemberParent = await activityService.findById(activityWithMemberParent.id) - - // delete models before expect because we already have ids (memberId, parentId) - delete activityWithMemberChild.member - delete activityWithMemberChild.parent - delete activityWithMemberChild.display - delete activityWithMemberChild.objectMember - delete activityWithMemberParent.member - delete activityWithMemberParent.parent - delete activityWithMemberParent.display - delete activityWithMemberParent.objectMember - - activityWithMemberChild.createdAt = activityWithMemberChild.createdAt - .toISOString() - .split('T')[0] - activityWithMemberChild.updatedAt = activityWithMemberChild.updatedAt - .toISOString() - .split('T')[0] - activityWithMemberParent.createdAt = activityWithMemberParent.createdAt - .toISOString() - .split('T')[0] - activityWithMemberParent.updatedAt = activityWithMemberParent.updatedAt - .toISOString() - .split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMemberChild.memberId, - mockIRepositoryOptions, - ) - - const expectedParentActivityCreated = { - id: activityWithMemberParent.id, - body: dataParent.body, - title: dataParent.title, - url: dataParent.url, - channel: dataParent.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: dataParent.type, - timestamp: new Date(dataParent.timestamp), - platform: dataParent.platform, - isContribution: dataParent.isContribution, - tasks: [], - score: dataParent.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - sourceParentId: null, - sourceId: dataParent.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMemberParent).toStrictEqual(expectedParentActivityCreated) - - const expectedChildActivityCreated = { - id: activityWithMemberChild.id, - body: dataChild.body, - title: dataChild.title, - url: dataChild.url, - channel: dataChild.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: dataChild.type, - timestamp: new Date(dataChild.timestamp), - platform: dataChild.platform, - isContribution: dataChild.isContribution, - score: dataChild.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: activityWithMemberParent.id, - sourceParentId: dataChild.sourceParentId, - sourceId: dataChild.sourceId, - conversationId: conversationCreated.rows[0].id, - organizationId: null, - organization: null, - } - - expect(activityWithMemberChild).toStrictEqual(expectedChildActivityCreated) - }) - - it(`Should respect the affiliation settings when setting an activity's organization with multiple member organizations`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const segmentRepo = new SegmentRepository(mockIRepositoryOptions) - - const segment1 = await segmentRepo.create({ - name: 'Crowd.dev - Segment1', - url: '', - parentName: 'Crowd.dev - Segment1', - grandparentName: 'Crowd.dev - Segment1', - slug: 'crowd.dev-1', - parentSlug: 'crowd.dev-1', - grandparentSlug: 'crowd.dev-1', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - const segment2 = await segmentRepo.create({ - name: 'Crowd.dev - Segment2', - url: '', - parentName: 'Crowd.dev - Segment2', - grandparentName: 'Crowd.dev - Segment2', - slug: 'crowd.dev-2', - parentSlug: 'crowd.dev-2', - grandparentSlug: 'crowd.dev-2', - status: SegmentStatus.ACTIVE, - sourceId: null, - sourceParentId: null, - }) - - await populateSegments(mockIRepositoryOptions) - const org1 = await OrganizationRepository.create( - { - displayName: 'tesla', - }, - mockIRepositoryOptions, - ) - - const org2 = await OrganizationRepository.create( - { - displayName: 'crowd.dev', - }, - mockIRepositoryOptions, - ) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - organizations: [org1, org2], - affiliations: [ - { - segmentId: segment1.id, - organizationId: org2.id, - dateStart: '2021-09-01', - }, - { - segmentId: segment2.id, - organizationId: null, - dateStart: '2021-09-01', - }, - ], - } - - const data = { - member, - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - platform: PlatformType.GITHUB, - sourceId: '#sourceId1', - } - - switchSegments(mockIRepositoryOptions, [segment1]) - - let activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember( - data, - ) - - let activity = await ActivityRepository.findById( - activityWithMember.id, - mockIRepositoryOptions, - ) - - // org2 should be set as organization because it's in member's affiliated organizations - expect(activity.organization.name).toEqual(org2.name) - - // add another activity to segment2 for the same member - switchSegments(mockIRepositoryOptions, [segment2]) - - data.sourceId = '#sourceId2' - data.member = member // createWithMember modifies member, reset it - - activityWithMember = await new ActivityService(mockIRepositoryOptions).createWithMember(data) - - activity = await ActivityRepository.findById(activityWithMember.id, mockIRepositoryOptions) - - // this member had a null affiliation(meaning no organizations should be set) in segment 2 - expect(activity.organization).toBeNull() - }) - - describe('Member tests in createWithMember', () => { - it('Should set the joinedAt to the time of the activity when the member does not exist', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceParentId: null, - sourceId: data.sourceId, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should replace joinedAt when activity ts is earlier than existing joinedAt', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2022-05-27T15:13:30Z', - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - sentiment: 0.42, - mixed: 0.42, - label: 'positive', - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tasks: [], - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should not replace joinedAt when activity ts is later than existing joinedAt', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - organisation: 'Crowd', - joinedAt: '2020-05-27T15:13:30Z', - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tasks: [], - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(new Date('2020-05-27T15:13:30Z')) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('It should replace joinedAt if the original was in year 1970', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member = { - username: { - [PlatformType.GITHUB]: 'anil_github', - }, - displayName: 'Anil', - email: 'lala@l.com', - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://twitter.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Computer Science', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Istanbul', - }, - }, - organisation: 'Crowd', - joinedAt: new Date('1970-01-01T00:00:00Z'), - } - - await MemberRepository.create(member, mockIRepositoryOptions) - - const data = { - member, - body: 'Description\nThis pull request adds a new Dashboard and related widgets. This work will probably have to be revisited as soon as possible since a lot of decisions were made, without having too much time to think about different outcomes/possibilities. We rushed these changes so that we can demo a working dashboard to YC and to our Investors.\nChanges Proposed\n\nUpdate Chart.js\nAdd two different type of widgets (number and graph)\nRemove older/default widgets from dashboard and add our own widgets\nHide some items from the menu\nAdd all widget infrastructure (actions, services, etc) to integrate with the backend\nAdd a few more CSS tweaks\n\nScreenshots', - title: 'Dashboard widgets and some other tweaks/adjustments', - state: 'merged', - url: 'https://github.com/CrowdDevHQ/crowd-web/pull/16', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - timestamp: '2021-09-30T14:20:27.000Z', - type: 'pull_request-closed', - isContribution: true, - platform: PlatformType.GITHUB, - score: 4, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - delete activityWithMember.member - delete activityWithMember.display - delete activityWithMember.objectMember - - activityWithMember.createdAt = activityWithMember.createdAt.toISOString().split('T')[0] - activityWithMember.updatedAt = activityWithMember.updatedAt.toISOString().split('T')[0] - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - - const expectedActivityCreated = { - id: activityWithMember.id, - body: data.body, - title: data.title, - url: data.url, - channel: data.channel, - sentiment: { - positive: 0.42, - negative: 0.42, - neutral: 0.42, - mixed: 0.42, - label: 'positive', - sentiment: 0.42, - }, - attributes: {}, - type: data.type, - timestamp: new Date(data.timestamp), - platform: data.platform, - isContribution: data.isContribution, - score: data.score, - username: 'anil_github', - objectMemberUsername: null, - memberId: memberFound.id, - objectMemberId: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tasks: [], - tenantId: mockIRepositoryOptions.currentTenant.id, - segmentId: mockIRepositoryOptions.currentSegments[0].id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - importHash: null, - parentId: null, - parent: null, - sourceId: data.sourceId, - sourceParentId: null, - conversationId: null, - organizationId: null, - organization: null, - } - - expect(activityWithMember).toStrictEqual(expectedActivityCreated) - expect(memberFound.joinedAt).toStrictEqual(expectedActivityCreated.timestamp) - expect(memberFound.username).toStrictEqual({ - [PlatformType.GITHUB]: ['anil_github'], - }) - }) - - it('Should respect joinedAt when an existing activity comes in with a different timestamp', async () => { - // This can happen in cases like the Twitter integration. - // For follow activities, if we are onboarding we set the timestamp to 1970, - // but if we are not onboarding, we set the timestamp to the current time. - // This can cause having 2 activities with different timestamps, but the same sourceId. - // The joinedAt should stay untouched in this case. - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(mockIRepositoryOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService( - mockIRepositoryOptions, - ) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const data = { - member: { - username: 'anil', - }, - timestamp: '1970-01-01T00:00:00.000Z', - type: 'follow', - platform: PlatformType.TWITTER, - sourceId: '#sourceId1', - } - - const activityWithMember = await new ActivityService( - mockIRepositoryOptions, - ).createWithMember(data) - - const data2 = { - member: { - username: 'anil', - }, - timestamp: '2021-09-30T14:20:27.000Z', - type: 'follow', - platform: PlatformType.TWITTER, - sourceId: '#sourceId1', - } - // Upsert the same activity with a different timestamp - await new ActivityService(mockIRepositoryOptions).createWithMember(data2) - - const memberFound = await MemberRepository.findById( - activityWithMember.memberId, - mockIRepositoryOptions, - ) - // The joinedAt should stay untouched - expect(memberFound.joinedAt).toStrictEqual(new Date('1970-01-01T00:00:00.000Z')) - }) - }) - }) - - describe('addToConversation method', () => { - it('Should create a new conversation and add the activities in, when parent and child has no conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - const conversationCreated = ( - await new ConversationService(mockIRepositoryOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - }) - - it('Should add the child activity to parents conversation, when parent already has a conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - conversationId: conversation.id, - sourceId: '#sourceId1', - } - - const activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get child activity again - activityChildCreated = await activityService.findById(activityChildCreated.id) - - // child should be added to already existing conservation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should add the parent activity to childs conversation and update conversation [published=false] title&slug, when child already has a conversation', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - let conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - channel: 'https://github.com/CrowdDevHQ/crowd-web', - body: 'Some Parent Activity', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - conversationId: conversation.id, - sourceId: '#sourceId2', - } - - const activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get the conversation again - conversation = await conversationService.findById(conversation.id) - - // conversation should be updated with newly added parents body - expect(conversation.title).toBe('Some Parent Activity') - expect(conversation.slug).toBe('some-parent-activity') - - // get parent activity again - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // parent should be added to the conversation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should add the parent activity to childs conversation and NOT update conversation [published=true] title&slug, when child already has a conversation', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - const conversationService = new ConversationService(mockIRepositoryOptions) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - let conversation = await conversationService.create({ - slug: 'some-slug', - title: 'some title', - published: true, - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - conversationId: conversation.id, - sourceId: '#sourceId2', - } - - const activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - await SequelizeRepository.commitTransaction(transaction) - - // get the conversation again - conversation = await conversationService.findById(conversation.id) - - // conversation fields should NOT be updated because it's already published - expect(conversation.title).toBe('some title') - expect(conversation.slug).toBe('some-slug') - - // get parent activity again - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // parent should be added to the conversation - expect(activityChildCreated.conversationId).toBe(conversation.id) - expect(activityParentCreated.conversationId).toBe(conversation.id) - }) - - it('Should always auto-publish when conversationSettings.autoPublish.status is set to all', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'all', - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(true) - }) - - it('Should never auto-publish when conversationSettings.autoPublish.status is set to disabled', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'disabled', - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(false) - }) - - it('Should auto-publish when conversationSettings.autoPublish.status is set to custom and rules match', async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'custom', - channelsByPlatform: { - [PlatformType.GITHUB]: ['crowd-web'], - }, - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversationCreated = ( - await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - ).rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(true) - }) - - it("Should not auto-publish when conversationSettings.autoPublish.status is set to custom and rules don't match", async () => { - let mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const activityService = new ActivityService(mockIRepositoryOptions) - await SettingsRepository.findOrCreateDefault( - { website: 'https://some-website' }, - mockIRepositoryOptions, - ) - await ConversationSettingsRepository.findOrCreateDefault( - { - autoPublish: { - status: 'custom', - channelsByPlatform: { - [PlatformType.GITHUB]: ['a-different-test-channel'], - }, - }, - }, - mockIRepositoryOptions, - ) - - const memberCreated = await new MemberService(mockIRepositoryOptions).upsert({ - username: { - [PlatformType.GITHUB]: 'test', - }, - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - }) - - const activityParent = { - type: 'activity', - timestamp: '2020-05-27T14:13:30Z', - platform: PlatformType.GITHUB, - body: 'Some Parent Activity', - channel: 'https://github.com/CrowdDevHQ/crowd-web', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityParentCreated = await ActivityRepository.create( - activityParent, - mockIRepositoryOptions, - ) - - const activityChild = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - body: 'Here', - isContribution: true, - username: 'test', - member: memberCreated.id, - score: 1, - parent: activityParentCreated.id, - sourceId: '#sourceId2', - } - - let activityChildCreated = await ActivityRepository.create( - activityChild, - mockIRepositoryOptions, - ) - - const transaction = await SequelizeRepository.createTransaction(mockIRepositoryOptions) - - await activityService.addToConversation( - activityChildCreated.id, - activityParentCreated.id, - transaction, - ) - - const conversations = await new ConversationService({ - ...mockIRepositoryOptions, - transaction, - } as IServiceOptions).findAndCountAll({ - slug: 'some-parent-activity', - }) - - const conversationCreated = conversations.rows[0] - - await SequelizeRepository.commitTransaction(transaction) - - // get activities again - activityChildCreated = await activityService.findById(activityChildCreated.id) - activityParentCreated = await activityService.findById(activityParentCreated.id) - - // activities should belong to the newly created conversation - expect(activityChildCreated.conversationId).toBe(conversationCreated.id) - expect(activityParentCreated.conversationId).toBe(conversationCreated.id) - - expect(conversationCreated.published).toStrictEqual(false) - }) - }) - - describe('affiliations', () => { - let options - let activityService: ActivityService - let memberService - let organizationService - let segmentService: SegmentService - let memberAffiliationService: MemberAffiliationService - let memberSegmentAffiliationRepository: MemberSegmentAffiliationRepository - - let defaultActivity - let defaultMember - - beforeEach(async () => { - options = await SequelizeTestUtils.getTestIRepositoryOptions(db) - await populateSegments(options) - - activityService = new ActivityService(options) - memberService = new MemberService(options) - organizationService = new OrganizationService(options) - segmentService = new SegmentService(options) - memberAffiliationService = new MemberAffiliationService(options) - memberSegmentAffiliationRepository = new MemberSegmentAffiliationRepository(options) - - defaultActivity = { - type: 'question', - timestamp: '2020-05-27T15:13:30Z', - username: 'test', - platform: PlatformType.GITHUB, - } - defaultMember = { - platform: PlatformType.GITHUB, - joinedAt: '2020-05-27T15:13:30Z', - } - }) - - async function createMember(data = {}) { - return await memberService.upsert({ - ...defaultMember, - username: { - [PlatformType.GITHUB]: uuid(), - }, - ...data, - }) - } - - async function createActivity(memberId, data: any = {}) { - const activity = await activityService.upsert({ - ...defaultActivity, - sourceId: uuid(), - member: memberId, - ...data, - }) - - if (data.organizationId) { - await ActivityRepository.update( - activity.id, - { - organizationId: data.organizationId, - }, - options, - ) - return await activityService.findById(activity.id) - } - - return activity - } - - async function findActivity(id) { - return await activityService.findById(id) - } - - async function createOrg(name, data = {}) { - return await organizationService.createOrUpdate({ - identities: [ - { - name, - platform: 'crowd', - }, - ], - ...data, - }) - } - - async function createSegment(slug, data = {}) { - const db1 = await SequelizeTestUtils.getDatabase(db) - const tenant = options.currentTenant - try { - const segment = ( - await db1.segment.create({ - url: tenant.url, - name: slug, - parentName: tenant.name, - grandparentName: tenant.name, - slug: slug, - parentSlug: 'default', - grandparentSlug: 'default', - status: SegmentStatus.ACTIVE, - description: null, - sourceId: null, - sourceParentId: null, - tenantId: tenant.id, - }) - ).get({ plain: true }) - return segment - } catch (error) { - console.log(error) - throw error - } - } - - async function addWorkExperience(memberId, orgId, data = {}) { - return await MemberRepository.createOrUpdateWorkExperience( - { - memberId, - organizationId: orgId, - updateAffiliation: false, - source: 'test', - ...data, - }, - options, - ) - } - - describe('new activities', () => { - it('Should affiliate nothing if member has no organizations and no affiliations', async () => { - const member = await createMember() - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - - it('Should affiliate work experience if member has organizations', async () => { - const member = await createMember() - const organization = await createOrg('hello') - await addWorkExperience(member.id, organization.id, { - dateStart: '2020-01-01', - }) - const activity = await createActivity(member.id, { - timestamp: '2023-01-01', - }) - - expect(activity.organization.id).toBe(organization.id) - }) - - it('Should affiliate with matching work experience if activity is from the past', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - dateEnd: '2020-02-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id, { - timestamp: '2020-01-15', - }) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should affiliate with most recent open work experience if member has multiple organizations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id, { - timestamp: '2020-03-01', - }) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should affiliate with most recent open work experience, even if there is a more recent closed one', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - await addWorkExperience(member.id, org2.id, { - dateStart: '2020-02-01', - dateEnd: '2020-03-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should affiliate with manual affiliation if member has organizations and affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const org2 = await createOrg('org2') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org2.id, - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should affiliate to invidiual if member has organizations and affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: null, - dateStart: '2020-02-01', - }) - - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - - it('Should not affiliate if there are no relevant manual affiliations', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const segment1 = await createSegment('segment1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: segment1.id, - organizationId: org1.id, - }) - - const activity = await createActivity(member.id) - - expect(activity.organization).toBeNull() - }) - }) - - describe('existing activities', () => { - it('Should clear affiliation if there is a manual individual affiliation', async () => { - const member = await createMember() - const org1 = await createOrg('org1') - const segment1 = await createSegment('segment1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: segment1.id, - organizationId: null, - }) - - let activity = await createActivity(member.id, { - organizationId: org1.id, - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization).toBeNull() - }) - - it('Should affiliate activities', async () => { - const member = await createMember() - - let activity1 = await createActivity(member.id) - let activity2 = await createActivity(member.id) - - const org = await createOrg('org') - await addWorkExperience(member.id, org.id, { - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity1 = await findActivity(activity1.id) - activity2 = await findActivity(activity2.id) - - expect(activity1.organization.id).toBe(org.id) - expect(activity2.organization.id).toBe(org.id) - }) - - it('Should only affiliate activities of specific member', async () => { - const member1 = await createMember() - const member2 = await createMember() - - let activity1 = await createActivity(member1.id) - let activity2 = await createActivity(member2.id) - - const org1 = await createOrg('org1') - await addWorkExperience(member1.id, org1.id, { - dateStart: '2020-01-01', - }) - const org2 = await createOrg('org2') - await addWorkExperience(member2.id, org2.id, { - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member1.id) - - activity2 = await findActivity(activity2.id) - - expect(activity2.organization).toBeNull() - }) - - it('Should affiliate with matching recent work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-01-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id, { - dateStart: '2023-01-01', - }) - const org3 = await createOrg('org2') - await addWorkExperience(member.id, org3.id, { - dateStart: '2019-01-01', - dateEnd: '2022-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org3.id) - }) - - it('Should affiliate first created org to past activities', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2022-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id, { - dateStart: '2023-01-01', - }) - const org3 = await createOrg('org2') - await addWorkExperience(member.id, org3.id, { - dateStart: '2019-01-01', - dateEnd: '2022-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should prefer manual affiliation over work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - const org2 = await createOrg('org2') - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org2.id, - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org2.id) - }) - - it('Should prefer manual individual affiliation over work experience', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - }) - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: null, - dateStart: '2020-01-01', - }) - - await memberAffiliationService.updateAffiliation(member.id) - - activity = await findActivity(activity.id) - - expect(activity.organization).toBeNull() - }) - - it('Should trigger affiliation update when changing member organizations', async () => { - const member = await createMember() - - let activity = await createActivity(member.id, { - timestamp: '2020-05-01', - }) - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id, { - dateStart: '2020-01-01', - updateAffiliation: true, - }) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - - it('Should trigger affiliation update when changing member manual affiliations', async () => { - const member = await createMember() - - let activity = await createActivity(member.id) - - const org1 = await createOrg('org1') - - await memberSegmentAffiliationRepository.createOrUpdate({ - memberId: member.id, - segmentId: options.currentSegments[0].id, - organizationId: org1.id, - }) - - activity = await findActivity(activity.id) - - expect(activity.organization.id).toBe(org1.id) - }) - }) - - it('Should filter by organization based on organizationId', async () => { - const member = await createMember() - - const org1 = await createOrg('org1') - await addWorkExperience(member.id, org1.id) - - const org2 = await createOrg('org2') - await addWorkExperience(member.id, org2.id) - - let activity1 = await createActivity(member.id, { - organizationId: org1.id, - }) - let activity2 = await createActivity(member.id, { - organizationId: org2.id, - }) - - const { rows } = await activityService.query({ - filter: { - organizations: [org1.id], - }, - }) - - expect(rows.length).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/eagleEyeContentService.test.ts b/backend/src/services/__tests__/eagleEyeContentService.test.ts deleted file mode 100644 index e70d41dc15..0000000000 --- a/backend/src/services/__tests__/eagleEyeContentService.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { EagleEyeActionType, EagleEyeContent } from '@crowd/types' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import EagleEyeContentService from '../eagleEyeContentService' - -const db = null - -describe('EagleEyeContentService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should create or update a single content using URL field', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const content: EagleEyeContent = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'post title', - body: 'post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - actions: [ - { - type: EagleEyeActionType.BOOKMARK, - timestamp: '2022-06-27T14:13:30Z', - }, - ], - } - - const service = new EagleEyeContentService(mockIRepositoryOptions) - const c1 = await service.upsert(content) - - let contents = await service.query({}) - - expect(contents.count).toBe(1) - expect(contents.rows).toStrictEqual([c1]) - - // upsert previous url with some new fields - const contentWithSameUrl: EagleEyeContent = { - platform: 'reddit', - url: 'https://some-post-url', - post: { - title: 'a brand new post title', - body: 'better post body', - }, - postedAt: '2020-05-27T15:13:30Z', - tenantId: mockIRepositoryOptions.currentTenant.id, - } - - const c1Upserted = await service.upsert(contentWithSameUrl) - - contents = await service.query({}) - expect(contents.count).toBe(1) - expect(contents.rows).toStrictEqual([c1Upserted]) - expect(c1Upserted.id).toEqual(c1.id) - expect(contents.rows[0].post).toStrictEqual(contentWithSameUrl.post) - }) - }) -}) diff --git a/backend/src/services/__tests__/integrationService.test.ts b/backend/src/services/__tests__/integrationService.test.ts deleted file mode 100644 index cc8d13e8b6..0000000000 --- a/backend/src/services/__tests__/integrationService.test.ts +++ /dev/null @@ -1,121 +0,0 @@ -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import IntegrationService from '../integrationService' -import { PlatformType } from '@crowd/types' - -const db = null - -describe('IntegrationService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('createOrUpdate', () => { - it('Should create a new integration because platform does not exist yet', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const integrationService = new IntegrationService(mockIServiceOptions) - - const integrationToCreate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'in-progress', - } - - let integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(0) - - await integrationService.createOrUpdate(integrationToCreate) - integrations = await integrationService.findAndCountAll({}) - - expect(integrations.count).toEqual(1) - }) - - it('Should update existing integration if platform already exists', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const integrationService = new IntegrationService(mockIServiceOptions) - - const integrationToCreate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'in-progress', - } - - await integrationService.createOrUpdate(integrationToCreate) - let integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(1) - expect(integrations.rows[0].status).toEqual('in-progress') - - const integrationToUpdate = { - platform: PlatformType.GITHUB, - token: '1234', - integrationIdentifier: '1234', - status: 'done', - } - - await integrationService.createOrUpdate(integrationToUpdate) - integrations = await integrationService.findAndCountAll({}) - expect(integrations.count).toEqual(1) - expect(integrations.rows[0].status).toEqual('done') - }) - }) - - describe('Find all active integrations tests', () => { - it('Should find an empty list when there are no integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(0) - }) - - it('Should return n for n active integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.SLACK, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(1) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.GITHUB, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(2) - }) - - it('Should return n for n active integrations when there are other integrations', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.SLACK, - status: 'done', - }) - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.DISCORD, - status: 'in-progress', - }) - - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(1) - - await new IntegrationService(mockIServiceOptions).createOrUpdate({ - platform: PlatformType.GITHUB, - status: 'done', - }) - expect( - (await new IntegrationService(mockIServiceOptions).getAllActiveIntegrations()).count, - ).toBe(2) - }) - }) -}) diff --git a/backend/src/services/__tests__/memberAttributeSettingsService.test.ts b/backend/src/services/__tests__/memberAttributeSettingsService.test.ts deleted file mode 100644 index 3d3624fb68..0000000000 --- a/backend/src/services/__tests__/memberAttributeSettingsService.test.ts +++ /dev/null @@ -1,905 +0,0 @@ -/* eslint @typescript-eslint/no-unused-vars: 0 */ - -import { - DEVTO_MEMBER_ATTRIBUTES, - DISCORD_MEMBER_ATTRIBUTES, - GITHUB_MEMBER_ATTRIBUTES, - SLACK_MEMBER_ATTRIBUTES, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' -import { Error400 } from '@crowd/common' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import { MemberAttributeType } from '@crowd/types' -import { RedisCache, getRedisClient } from '@crowd/redis' -import { REDIS_CONFIG } from '../../conf' -import { getServiceLogger } from '@crowd/logging' - -const log = getServiceLogger() - -let cache: RedisCache | undefined = undefined -const clearRedisCache = async () => { - if (!cache) { - const redis = await getRedisClient(REDIS_CONFIG) - cache = new RedisCache('memberAttributes', redis, log) - } - - await cache.deleteAll() -} - -const db = null -describe('MemberAttributeSettingService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - await clearRedisCache() - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('createPredefined tests', () => { - it('Should create predefined github attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(GITHUB_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [ - isHireableCreated, - urlCreated, - websiteUrlCreated, - bioCreated, - companyCreated, - locationCreated, - ] = attributes - - const [isHireable, url, websiteUrl, bio, company, location] = GITHUB_MEMBER_ATTRIBUTES - - const expected = [ - { - id: isHireableCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: isHireable.show, - type: isHireable.type, - canDelete: isHireable.canDelete, - name: isHireable.name, - label: isHireable.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: websiteUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: websiteUrl.show, - type: websiteUrl.type, - canDelete: websiteUrl.canDelete, - name: websiteUrl.name, - label: websiteUrl.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: companyCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: company.show, - type: company.type, - canDelete: company.canDelete, - name: company.name, - label: company.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined discord attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(DISCORD_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated] = attributes - - const [id, avatarUrl] = DISCORD_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - - it('Should create predefined devto attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(DEVTO_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, urlCreated, nameCreated, bioCreated, locationCreated] = attributes - - const [id, url, name, bio, location] = DEVTO_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: nameCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: name.show, - type: name.type, - canDelete: name.canDelete, - name: name.name, - label: name.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined twitter attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(TWITTER_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated, urlCreated, bioCreated, locationCreated] = attributes - - const [id, avatarUrl, url, bio, location] = TWITTER_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - { - id: urlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: bioCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: locationCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should create predefined slack attributes', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = (await as.createPredefined(SLACK_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - const [idCreated, avatarUrlCreated, jobTitleCreated, timezoneCreated] = attributes - - const [id, avatarUrl, jobTitle, timezone] = SLACK_MEMBER_ATTRIBUTES - - const expected = [ - { - id: idCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: avatarUrlCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: avatarUrl.show, - type: avatarUrl.type, - canDelete: avatarUrl.canDelete, - name: avatarUrl.name, - label: avatarUrl.label, - }, - { - id: jobTitleCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: jobTitle.show, - type: jobTitle.type, - canDelete: jobTitle.canDelete, - name: jobTitle.name, - label: jobTitle.label, - }, - { - id: timezoneCreated.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: timezone.show, - type: timezone.type, - canDelete: timezone.canDelete, - name: timezone.name, - label: timezone.label, - }, - ] - - expect(attributes).toEqual(expected) - }) - it('Should accept duplicate attributes from different platforms without an exception', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attributes = await as.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const attributes2 = (await as.createPredefined(DEVTO_MEMBER_ATTRIBUTES)).map((attribute) => { - attribute.createdAt = (attribute.createdAt as any).toISOString().split('T')[0] - attribute.updatedAt = (attribute.updatedAt as any).toISOString().split('T')[0] - return attribute - }) - - // create predefined method should still return shared attributes `url` and `id` - const [ - idCreatedTwitter, - _avatarUrlCreated, - urlCreatedTwitter, - bioCreatedTwitter, - locationCreatedTwitter, - ] = attributes - - const [ - _idCreatedDevTo, - _urlCreatedDevTo, - nameCreatedDevTo, - _bioCreatedDevTo, - _locationCreatedDevTo, - ] = attributes2 - - const [id, url, name, bio, location] = DEVTO_MEMBER_ATTRIBUTES - console.log('urlCreatedTwitter', urlCreatedTwitter.id) - console.log('urlCreatedDevTo', _urlCreatedDevTo.id) - console.log('bioCreatedTwitter', bioCreatedTwitter.id) - console.log('bioCreatedDevTo', _bioCreatedDevTo.id) - console.log('locationCreatedTwitter', locationCreatedTwitter.id) - console.log('locationCreatedDevTo', _locationCreatedDevTo.id) - const expected = [ - { - id: idCreatedTwitter.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: id.show, - type: id.type, - canDelete: id.canDelete, - name: id.name, - label: id.label, - }, - { - id: _urlCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: url.show, - type: url.type, - canDelete: url.canDelete, - name: url.name, - label: url.label, - }, - { - id: nameCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: name.show, - type: name.type, - canDelete: name.canDelete, - name: name.name, - label: name.label, - }, - { - id: _bioCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: bio.show, - type: bio.type, - canDelete: bio.canDelete, - name: bio.name, - label: bio.label, - }, - { - id: _locationCreatedDevTo.id, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - tenantId: mockIRepositoryOptions.currentTenant.id, - options: [], - show: location.show, - type: location.type, - canDelete: location.canDelete, - name: location.name, - label: location.label, - }, - ] - - expect(attributes2).toEqual(expected) - - // find all attributes: url, name, id, imgUrl should be present - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(6) - const allAttributeNames = allAttributes.rows.map((attribute) => attribute.name) - - expect(allAttributeNames).toEqual(['name', 'url', 'bio', 'location', 'avatarUrl', 'sourceId']) - }) - }) - describe('create tests', () => { - it('Should add single attribute to member attributes - all fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - } - - const attributeCreated = await as.create(attribute1) - - const attributeExpected = { - ...attributeCreated, - options: [], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: attribute1.canDelete, - show: attribute1.show, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should create a multi-select field with options', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.MULTI_SELECT, - options: ['option1', 'option2'], - canDelete: true, - show: true, - } - - const attributeCreated = await as.create(attribute1) - - const attributeExpected = { - ...attributeCreated, - options: ['option1', 'option2'], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: attribute1.canDelete, - show: attribute1.show, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should add single attribute to member attributes - without default fields', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - } - - const attributeCreated = await as.create(attribute1) - - // canDelete and show should be true by default - const attributeExpected = { - ...attributeCreated, - options: [], - name: attribute1.name, - label: attribute1.label, - type: attribute1.type, - canDelete: true, - show: true, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - - it('Should add single attribute to member attributes - without default fields and name', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = { - label: 'an attribute with multiple words', - type: MemberAttributeType.BOOLEAN, - } - - const attributeCreated = await as.create(attribute1) - - // name should be generated from the label - const attributeExpected = { - ...attributeCreated, - options: [], - name: 'anAttributeWithMultipleWords', - label: attribute1.label, - type: attribute1.type, - canDelete: true, - show: true, - } - - expect(attributeCreated).toStrictEqual(attributeExpected) - }) - }) - - describe('destroyAll tests', () => { - it('Should remove a single attribute succesfully', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await as.destroyAll([attribute.id]) - - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(0) - expect(allAttributes.rows).toStrictEqual([]) - }) - - it('Should remove multiple existing attributes successfully, and should silently accept non existing names and keep the canDelete=false attributes intact', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute1 = await as.create({ - name: 'att1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - const attribute2 = await as.create({ - name: 'att2', - label: 'attribute 2', - type: MemberAttributeType.STRING, - canDelete: false, - show: true, - }) - - const attribute3 = await as.create({ - name: 'att3', - label: 'attribute 3', - type: MemberAttributeType.EMAIL, - canDelete: true, - show: false, - }) - - await as.destroyAll([attribute1.id, attribute2.id, attribute3.id]) - - const allAttributes = await as.findAndCountAll({}) - - expect(allAttributes.count).toBe(1) - expect(allAttributes.rows).toStrictEqual([attribute2]) - }) - }) - - describe('update tests', () => { - it(`Should throw typesNotMatching 400 error when updating an existing attribute's type to another value`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await expect(() => - as.update(attribute.id, { - name: attribute.name, - label: 'some other label', - type: MemberAttributeType.STRING, - }), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.typesNotMatching', attribute.name), - ) - }) - - it(`Should throw canDeleteReadonly 400 error when updating an existing attribute's canDelete field to another value`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - await expect(() => - as.update(attribute.id, { - canDelete: false, - show: true, - }), - ).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.errors.canDeleteReadonly', attribute.name), - ) - }) - - it(`Should should update other cases successfully`, async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const as = new MemberAttributeSettingsService(mockIRepositoryOptions) - - const attribute = await as.create({ - name: 'attribute 1', - label: 'attribute 1', - type: MemberAttributeType.BOOLEAN, - canDelete: true, - show: true, - }) - - const attribute1Update = { - name: attribute.name, - label: 'some other label', - type: attribute.type, - canDelete: true, - show: false, - } - - const updatedAttribute = await as.update(attribute.id, attribute1Update) - - const expectedAttribute = { - ...updatedAttribute, - name: attribute.name, - label: attribute1Update.label, - type: attribute.type, - canDelete: attribute.canDelete, - show: attribute1Update.show, - } - - expect(updatedAttribute).toStrictEqual(expectedAttribute) - }) - }) - - describe('isCorrectType tests', () => { - it(`Should check various types and values successfully`, async () => { - const isCorrectType = MemberAttributeSettingsService.isCorrectType - - // boolean - expect(isCorrectType(true, MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType(false, MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType('true', MemberAttributeType.BOOLEAN)).toBeTruthy() - expect(isCorrectType('false', MemberAttributeType.BOOLEAN)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.BOOLEAN)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.BOOLEAN)).toBeFalsy() - - // string - expect(isCorrectType('', MemberAttributeType.STRING)).toBeTruthy() - expect(isCorrectType('someString', MemberAttributeType.STRING)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.STRING)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.STRING)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.STRING)).toBeFalsy() - - // date - expect(isCorrectType('2022-05-10', MemberAttributeType.DATE)).toBeTruthy() - expect(isCorrectType('2022-06-15T00:00:00', MemberAttributeType.DATE)).toBeTruthy() - expect(isCorrectType('2022-07-14T00:00:00Z', MemberAttributeType.DATE)).toBeTruthy() - - expect(isCorrectType(5, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType('', MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.DATE)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.DATE)).toBeFalsy() - - // email - expect(isCorrectType('anil@crowd.dev', MemberAttributeType.EMAIL)).toBeTruthy() - expect(isCorrectType('anil+123@crowd.dev', MemberAttributeType.EMAIL)).toBeTruthy() - - expect(isCorrectType(15, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType('', MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.EMAIL)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.EMAIL)).toBeFalsy() - - // number - expect(isCorrectType(100, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(5.123, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(0.000001, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType(0, MemberAttributeType.NUMBER)).toBeTruthy() - expect(isCorrectType('125', MemberAttributeType.NUMBER)).toBeTruthy() - - expect(isCorrectType('', MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType('someString', MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType(true, MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType({}, MemberAttributeType.NUMBER)).toBeFalsy() - expect(isCorrectType([], MemberAttributeType.NUMBER)).toBeFalsy() - - // multiselect - expect( - isCorrectType(['a', 'b', 'c'], MemberAttributeType.MULTI_SELECT, { - options: ['a', 'b', 'c', 'd'], - }), - ).toBeTruthy() - expect( - isCorrectType([], MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c', 'd'] }), - ).toBeTruthy() - expect( - isCorrectType(['a'], MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c', 'd'] }), - ).toBeTruthy() - expect( - isCorrectType(['a', '42'], MemberAttributeType.MULTI_SELECT, { - options: ['a', 'b', 'c', 'd'], - }), - ).toBeFalsy() - expect( - isCorrectType('a', MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c'] }), - ).toBeFalsy() - expect( - isCorrectType(5, MemberAttributeType.MULTI_SELECT, { options: ['a', 'b', 'c'] }), - ).toBeFalsy() - }) - }) -}) diff --git a/backend/src/services/__tests__/memberService.test.ts b/backend/src/services/__tests__/memberService.test.ts deleted file mode 100644 index e17404e46d..0000000000 --- a/backend/src/services/__tests__/memberService.test.ts +++ /dev/null @@ -1,3191 +0,0 @@ -import { generateUUIDv1, Error400, Error404 } from '@crowd/common' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import MemberService from '../memberService' -import MemberRepository from '../../database/repositories/memberRepository' -import ActivityRepository from '../../database/repositories/activityRepository' -import TagRepository from '../../database/repositories/tagRepository' -import { MemberAttributeName, MemberAttributeType, PlatformType } from '@crowd/types' -import OrganizationRepository from '../../database/repositories/organizationRepository' -import TaskRepository from '../../database/repositories/taskRepository' -import NoteRepository from '../../database/repositories/noteRepository' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import SettingsRepository from '../../database/repositories/settingsRepository' -import OrganizationService from '../organizationService' -import Plans from '../../security/plans' -import lodash from 'lodash' -import { - DEVTO_MEMBER_ATTRIBUTES, - DISCORD_MEMBER_ATTRIBUTES, - GITHUB_MEMBER_ATTRIBUTES, - SLACK_MEMBER_ATTRIBUTES, - TWITTER_MEMBER_ATTRIBUTES, -} from '@crowd/integrations' - -const db = null - -describe('MemberService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('upsert method', () => { - it('Should throw 400 error when platform does not exist in member data', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - integrationId: generateUUIDv1(), - }, - }, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - await expect(() => - new MemberService(mockIServiceOptions).upsert(member1), - ).rejects.toThrowError(new Error400()) - }) - - it('Should create non existent member - attributes with matching platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - [PlatformType.TWITTER]: 'https://some-twitter-url', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId', - [PlatformType.DISCORD]: '#discordId', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username, attributes } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: attributes[MemberAttributeName.SOURCE_ID][PlatformType.DISCORD], - [PlatformType.TWITTER]: attributes[MemberAttributeName.SOURCE_ID][PlatformType.TWITTER], - default: attributes[MemberAttributeName.SOURCE_ID][PlatformType.TWITTER], - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: - attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: -1 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - lastEnriched: null, - enrichedBy: [], - contributions: null, - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - object type username', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - }, - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - // Save some attributes since they get modified in the upsert function - const { username, attributes } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.TWITTER]: ['anil_twitter'], - }, - displayName: 'anil', - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - reach: { total: -1 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as object, platform in object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - reach: { [PlatformType.GITHUB]: 10, [PlatformType.TWITTER]: 10 }, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 20, [PlatformType.GITHUB]: 10, [PlatformType.TWITTER]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - reach as object, platform not in object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - reach: { [PlatformType.DISCORD]: 10, [PlatformType.TWITTER]: 10 }, - bio: 'Computer Science', - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - // Save some attributes since they get modified in the upsert function - const { platform, username } = member1 - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [platform]: [username], - }, - displayName: username, - attributes: {}, - emails: member1.emails, - score: member1.score, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: 20, [PlatformType.DISCORD]: 10, [PlatformType.TWITTER]: 10 }, - joinedAt: new Date('2020-05-28T15:13:30Z'), - affiliations: [], - manuallyCreated: false, - } - - expect(memberCreated).toStrictEqual(memberExpected) - }) - - it('Should create non existent member - organization as name, no enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: ['crowd.dev'], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: null, - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should create non existent member - organization as object, no enrichment', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: [{ name: 'crowd.dev', url: 'https://crowd.dev', description: 'Here' }], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: 'Here', - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should create non existent member - organization as id', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const oCreated = await new OrganizationService(mockIServiceOptions).createOrUpdate({ - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - }) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - emails: ['lala@gmail.com'], - score: 10, - attributes: {}, - reach: 10, - bio: 'Computer Science', - organizations: [oCreated.id], - joinedAt: '2020-05-28T15:13:30Z', - location: 'Istanbul', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const organization = (await OrganizationRepository.findAndCountAll({}, mockIServiceOptions)) - .rows[0] - - const foundMember = await MemberRepository.findById(memberCreated.id, mockIServiceOptions) - - const o1 = foundMember.organizations[0].get({ plain: true }) - delete o1.createdAt - delete o1.updatedAt - - expect(o1).toStrictEqual({ - id: organization.id, - displayName: 'crowd.dev', - github: null, - location: null, - website: null, - description: null, - emails: null, - phoneNumbers: null, - logo: null, - memberOrganizations: { - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - tags: null, - twitter: null, - linkedin: null, - crunchbase: null, - employees: null, - revenueRange: null, - importHash: null, - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - isTeamOrganization: false, - type: null, - ticker: null, - size: null, - naics: null, - lastEnrichedAt: null, - industry: null, - headline: null, - geoLocation: null, - founded: null, - employeeCountByCountry: null, - address: null, - profiles: null, - attributes: {}, - manuallyCreated: false, - affiliatedProfiles: null, - allSubsidiaries: null, - alternativeDomains: null, - alternativeNames: null, - averageEmployeeTenure: null, - averageTenureByLevel: null, - averageTenureByRole: null, - directSubsidiaries: null, - employeeChurnRate: null, - employeeCountByMonth: null, - employeeGrowthRate: null, - employeeCountByMonthByLevel: null, - employeeCountByMonthByRole: null, - gicsSector: null, - grossAdditionsByMonth: null, - grossDeparturesByMonth: null, - ultimateParent: null, - immediateParent: null, - }) - }) - - it('Should update existent member succesfully - simple', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - emails: ['test@gmail.com', 'test2@yahoo.com'], - platform: PlatformType.GITHUB, - location: 'Ankara', - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: ['lala@l.com', 'test@gmail.com', 'test2@yahoo.com'], - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member successfully - attributes with matching platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes1 = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - attributes: { - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://twitter-url', - }, - }, - } - - const attributes2 = member2.attributes - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes2[MemberAttributeName.URL][PlatformType.TWITTER], - default: attributes2[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: - attributes2[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes2[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes2[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes2[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - emails: member1.emails, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - object type username', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - [PlatformType.DISCORD]: 'anil_discord', - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.TWITTER]: ['anil_twitter'], - [PlatformType.DISCORD]: ['anil_discord'], - }, - displayName: 'anil', - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should throw 400 error when given platform does not match with username object ', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - emails: ['lala@l.com'], - platform: PlatformType.GITHUB, - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.TWITTER]: 'anil_twitter', - [PlatformType.DISCORD]: 'anil_discord', - }, - platform: PlatformType.SLACK, - } - - await expect(() => - new MemberService(mockIServiceOptions).upsert(member2), - ).rejects.toThrowError(new Error400()) - }) - - it('Should update existent member succesfully - JSON fields', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const member1 = { - username: 'anil', - platform: PlatformType.TWITTER, - emails: ['lala@l.com'], - score: 10, - attributes: { - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/imcvampire', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - }, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - const attributes1 = member1.attributes - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.TWITTER, - joinedAt: '2020-05-28T15:13:30Z', - location: 'Ankara', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DEVTO]: '#someDevtoId', - [PlatformType.SLACK]: '#someSlackId', - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Michael Scott', - }, - [MemberAttributeName.URL]: { - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - }, - } - - const attributes2 = member2.attributes - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.TWITTER]: [member1Username], - }, - displayName: member1Username, - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DEVTO]: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.DEVTO], - [PlatformType.SLACK]: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.SLACK], - default: attributes2[MemberAttributeName.SOURCE_ID][PlatformType.DEVTO], - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: attributes2[MemberAttributeName.NAME][PlatformType.DEVTO], - default: attributes2[MemberAttributeName.NAME][PlatformType.DEVTO], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.IS_HIREABLE][PlatformType.GITHUB], - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes2[MemberAttributeName.URL][PlatformType.DEVTO], - default: attributes1[MemberAttributeName.URL][PlatformType.GITHUB], - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: - attributes1[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.WEBSITE_URL][PlatformType.GITHUB], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.BIO][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes1[MemberAttributeName.LOCATION][PlatformType.GITHUB], - default: attributes1[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - }, - emails: member1.emails, - lastEnriched: null, - organizations: [ - { - displayName: 'l.com', - id: memberCreated.organizations[0].id, - memberOrganizations: { - memberId: memberCreated.id, - organizationId: memberCreated.organizations[0].id, - dateEnd: null, - dateStart: null, - title: null, - source: null, - }, - }, - ], - enrichedBy: [], - contributions: null, - score: member1.score, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - reach: { total: -1 }, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - reach from default to complete - sending number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: 10, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - reach from default to complete - sending platform', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: { [PlatformType.GITHUB]: 10 }, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - displayName: member1Username, - reach: { total: 10, [PlatformType.GITHUB]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - complex reach update from object', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - reach: { [PlatformType.TWITTER]: 10, linkedin: 10, total: 20 }, - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: { [PlatformType.GITHUB]: 15, linkedin: 11 }, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - displayName: member1Username, - reach: { total: 36, [PlatformType.GITHUB]: 15, linkedin: 11, [PlatformType.TWITTER]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - - it('Should update existent member succesfully - complex reach update from number', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - reach: { [PlatformType.TWITTER]: 10, linkedin: 10, total: 20 }, - } - - const member1Username = member1.username - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - memberCreated.createdAt = memberCreated.createdAt.toISOString().split('T')[0] - memberCreated.updatedAt = memberCreated.updatedAt.toISOString().split('T')[0] - - const member2 = { - username: 'anil', - platform: PlatformType.GITHUB, - reach: 30, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).upsert(member2) - - memberUpdated.createdAt = memberUpdated.createdAt.toISOString().split('T')[0] - memberUpdated.updatedAt = memberUpdated.updatedAt.toISOString().split('T')[0] - - const memberExpected = { - id: memberCreated.id, - joinedAt: new Date('2020-05-28T15:13:30Z'), - username: { - [PlatformType.GITHUB]: [member1Username], - }, - displayName: member1Username, - lastEnriched: null, - organizations: [], - enrichedBy: [], - contributions: null, - reach: { total: 50, [PlatformType.GITHUB]: 30, linkedin: 10, [PlatformType.TWITTER]: 10 }, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIServiceOptions.currentTenant.id, - segments: mockIServiceOptions.currentSegments, - createdById: mockIServiceOptions.currentUser.id, - updatedById: mockIServiceOptions.currentUser.id, - score: -1, - emails: [], - attributes: {}, - affiliations: [], - manuallyCreated: false, - } - - expect(memberUpdated).toStrictEqual(memberExpected) - }) - }) - - describe('update method', () => { - it('Should update existent member succesfully - removing identities with simple string format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: 'anil', - type: 'member', - platform: PlatformType.GITHUB, - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: 'anil_new', - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new']) - }) - - it('Should update existent member succesfully - removing identities with simple identity format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: { - username: 'anil', - }, - }, - platform: PlatformType.GITHUB, - type: 'member', - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: { - [PlatformType.GITHUB]: { - username: 'anil_new', - }, - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new']) - }) - - it('Should update existent member succesfully - removing identities with array identity format', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const member1 = { - username: { - [PlatformType.GITHUB]: [ - { - username: 'anil', - }, - ], - }, - platform: PlatformType.GITHUB, - type: 'member', - joinedAt: '2020-05-28T15:13:30Z', - } - - const memberCreated = await new MemberService(mockIServiceOptions).upsert(member1) - - const toUpdate = { - username: { - [PlatformType.GITHUB]: [ - { - username: 'anil_new', - }, - { - username: 'anil_new2', - }, - ], - }, - platform: PlatformType.GITHUB, - } - - const memberUpdated = await new MemberService(mockIServiceOptions).update( - memberCreated.id, - toUpdate, - ) - - expect(memberUpdated.username[PlatformType.GITHUB]).toStrictEqual(['anil_new', 'anil_new2']) - }) - }) - - describe('merge method', () => { - it('Should merge', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(SLACK_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - let t1 = await TagRepository.create({ name: 'tag1' }, mockIRepositoryOptions) - let t2 = await TagRepository.create({ name: 'tag2' }, mockIRepositoryOptions) - let t3 = await TagRepository.create({ name: 'tag3' }, mockIRepositoryOptions) - - let o1 = await OrganizationRepository.create({ displayName: 'org1' }, mockIRepositoryOptions) - let o2 = await OrganizationRepository.create({ displayName: 'org2' }, mockIRepositoryOptions) - let o3 = await OrganizationRepository.create({ displayName: 'org3' }, mockIRepositoryOptions) - - let task1 = await TaskRepository.create({ name: 'task1' }, mockIRepositoryOptions) - let task2 = await TaskRepository.create({ name: 'task2' }, mockIRepositoryOptions) - let task3 = await TaskRepository.create({ name: 'task3' }, mockIRepositoryOptions) - - let note1 = await NoteRepository.create({ body: 'note1' }, mockIRepositoryOptions) - let note2 = await NoteRepository.create({ body: 'note2' }, mockIRepositoryOptions) - let note3 = await NoteRepository.create({ body: 'note3' }, mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - emails: ['anil+1@crowd.dev', 'anil+2@crowd.dev'], - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - tags: [t1.id, t2.id], - organizations: [o1.id, o2.id], - tasks: [task1.id, task2.id], - notes: [note1.id, note2.id], - } - - const member2 = { - username: { - [PlatformType.DISCORD]: 'anil', - }, - emails: ['anil+1@crowd.dev', 'anil+3@crowd.dev'], - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Crowd.dev', - default: 'Crowd.dev', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: '#discordId', - default: '#discordId', - }, - }, - tags: [t2.id, t3.id], - organizations: [o2.id, o3.id], - tasks: [task2.id, task3.id], - notes: [note2.id, note3.id], - } - - const member3 = { - username: { - [PlatformType.TWITTER]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://a-twitter-url', - default: 'https://a-twitter-url', - }, - }, - } - const member4 = { - username: { - [PlatformType.SLACK]: 'testt', - }, - displayName: 'Member 4', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.SLACK]: '#slackId', - default: '#slackId', - }, - }, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - const returnedMember2 = await MemberRepository.create(member2, mockIRepositoryOptions) - const returnedMember3 = await MemberRepository.create(member3, mockIRepositoryOptions) - const returnedMember4 = await MemberRepository.create(member4, mockIRepositoryOptions) - - const activity = { - type: 'activity', - timestamp: '2020-05-27T15:13:30Z', - platform: PlatformType.GITHUB, - attributes: { - replies: 12, - body: 'Here', - }, - sentiment: { - positive: 0.98, - negative: 0.0, - neutral: 0.02, - mixed: 0.0, - label: 'positive', - sentiment: 0.98, - }, - isContribution: true, - username: 'anil', - member: returnedMember2.id, - score: 1, - sourceId: '#sourceId1', - } - - let activityCreated = await ActivityRepository.create(activity, mockIRepositoryOptions) - - // toMerge[1] = [(1,2),(1,4)] toMerge[2] = [(2,1)] toMerge[4] = [(4,1)] - // noMerge[2] = [3] - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember4.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember4.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - - await MemberRepository.addNoMerge( - returnedMember2.id, - returnedMember3.id, - mockIRepositoryOptions, - ) - - const response = await memberService.merge(returnedMember1.id, returnedMember2.id) - - const mergedMember = await MemberRepository.findById( - response.mergedId, - mockIRepositoryOptions, - ) - - // Sequelize returns associations as array of models, we need to get plain objects - mergedMember.tags = mergedMember.tags.map((i) => i.get({ plain: true })) - mergedMember.organizations = mergedMember.organizations.map((i) => - SequelizeTestUtils.objectWithoutKey(i.get({ plain: true }), ['memberOrganizations']), - ) - mergedMember.tasks = mergedMember.tasks.map((i) => i.get({ plain: true })) - mergedMember.notes = mergedMember.notes.map((i) => i.get({ plain: true })) - - // get the created activity again, it's member should be updated after merge - activityCreated = await ActivityRepository.findById( - activityCreated.id, - mockIRepositoryOptions, - ) - - // we don't need activity.member because we're already expecting member->activities - activityCreated = SequelizeTestUtils.objectWithoutKey(activityCreated, [ - 'member', - 'objectMember', - 'parent', - 'tasks', - 'display', - 'organization', - ]) - - // get previously created tags - t1 = await TagRepository.findById(t1.id, mockIRepositoryOptions) - t2 = await TagRepository.findById(t2.id, mockIRepositoryOptions) - t3 = await TagRepository.findById(t3.id, mockIRepositoryOptions) - - // get previously created organizations - o1 = await OrganizationRepository.findById(o1.id, mockIRepositoryOptions) - o2 = await OrganizationRepository.findById(o2.id, mockIRepositoryOptions) - o3 = await OrganizationRepository.findById(o3.id, mockIRepositoryOptions) - - // get previously created tasks - task1 = await TaskRepository.findById(task1.id, mockIRepositoryOptions) - task2 = await TaskRepository.findById(task2.id, mockIRepositoryOptions) - task3 = await TaskRepository.findById(task3.id, mockIRepositoryOptions) - - // get previously created notes - note1 = await NoteRepository.findById(note1.id, mockIRepositoryOptions) - note2 = await NoteRepository.findById(note2.id, mockIRepositoryOptions) - note3 = await NoteRepository.findById(note3.id, mockIRepositoryOptions) - - // remove tags->member relations as well (we should be only checking 1-deep relations) - t1 = SequelizeTestUtils.objectWithoutKey(t1, 'members') - t2 = SequelizeTestUtils.objectWithoutKey(t2, 'members') - t3 = SequelizeTestUtils.objectWithoutKey(t3, 'members') - - // remove organizations->member relations as well (we should be only checking 1-deep relations) - o1 = SequelizeTestUtils.objectWithoutKey(o1, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - o2 = SequelizeTestUtils.objectWithoutKey(o2, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - o3 = SequelizeTestUtils.objectWithoutKey(o3, [ - 'memberCount', - 'joinedAt', - 'activityCount', - 'memberOrganizations', - ]) - - // remove tasks->member and tasks->activity tasks->assignees relations as well (we should be only checking 1-deep relations) - task1 = SequelizeTestUtils.objectWithoutKey(task1, ['members', 'activities', 'assignees']) - task2 = SequelizeTestUtils.objectWithoutKey(task2, ['members', 'activities', 'assignees']) - task3 = SequelizeTestUtils.objectWithoutKey(task3, ['members', 'activities', 'assignees']) - - // remove notes->member relations as well (we should be only checking 1-deep relations) - note1 = SequelizeTestUtils.objectWithoutKey(note1, ['members', 'createdBy']) - note2 = SequelizeTestUtils.objectWithoutKey(note2, ['members', 'createdBy']) - note3 = SequelizeTestUtils.objectWithoutKey(note3, ['members', 'createdBy']) - - mergedMember.updatedAt = mergedMember.updatedAt.toISOString().split('T')[0] - - const expectedMember = { - id: returnedMember1.id, - username: { - [PlatformType.GITHUB]: ['anil'], - [PlatformType.DISCORD]: ['anil'], - }, - lastEnriched: null, - enrichedBy: [], - contributions: null, - displayName: 'Anil', - identities: [PlatformType.GITHUB, PlatformType.DISCORD], - attributes: { - ...member1.attributes, - ...member2.attributes, - }, - activeOn: [activityCreated.platform], - activityTypes: [`${activityCreated.platform}:${activityCreated.type}`], - emails: ['anil+1@crowd.dev', 'anil+2@crowd.dev', 'anil+3@crowd.dev'], - score: -1, - importHash: null, - createdAt: returnedMember1.createdAt, - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - tenantId: mockIRepositoryOptions.currentTenant.id, - segments: mockIRepositoryOptions.currentSegments, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - joinedAt: new Date(member1.joinedAt), - reach: { total: -1 }, - tags: [t1, t2, t3], - tasks: [task1, task2, task3], - notes: [note1, note2, note3], - organizations: [ - SequelizeTestUtils.objectWithoutKey(o1, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(o2, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - SequelizeTestUtils.objectWithoutKey(o3, [ - 'activeOn', - 'identities', - 'lastActive', - 'segments', - 'weakIdentities', - ]), - ], - noMerge: [returnedMember3.id], - toMerge: [returnedMember4.id], - activityCount: 1, - activeDaysCount: 1, - averageSentiment: activityCreated.sentiment.sentiment, - lastActive: activityCreated.timestamp, - lastActivity: activityCreated, - numberOfOpenSourceContributions: 0, - affiliations: [], - manuallyCreated: false, - } - - expect( - mergedMember.tasks.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ).toEqual( - expectedMember.tasks.sort((a, b) => { - const nameA = a.name.toLowerCase() - const nameB = b.name.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ) - - expect( - mergedMember.organizations.sort((a, b) => { - const nameA = a.displayName.toLowerCase() - const nameB = b.displayName.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ).toEqual( - expectedMember.organizations.sort((a, b) => { - const nameA = a.displayName.toLowerCase() - const nameB = b.displayName.toLowerCase() - if (nameA < nameB) { - return -1 - } - if (nameA > nameB) { - return 1 - } - return 0 - }), - ) - delete mergedMember.tasks - delete expectedMember.tasks - delete mergedMember.organizations - delete expectedMember.organizations - - expect(mergedMember).toStrictEqual(expectedMember) - }) - - it('Should catch when two members are the same', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const memberCreated = await MemberRepository.create(member1, mockIRepositoryOptions) - const mergeOutput = await memberService.merge(memberCreated.id, memberCreated.id) - - expect(mergeOutput).toStrictEqual({ status: 203, mergedId: memberCreated.id }) - - const found = await memberService.findById(memberCreated.id) - expect(found).toStrictEqual(memberCreated) - }) - }) - - describe('addToNoMerge method', () => { - it('Should add two members to their respective noMerges, these members should be excluded from toMerges respectively', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const member2 = { - username: { - [PlatformType.DISCORD]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.DISCORD]: '#discordId', - default: '#discordId', - }, - }, - } - - const member3 = { - username: { - [PlatformType.TWITTER]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-30T15:14:30Z', - attributes: { - [MemberAttributeName.URL]: { - [PlatformType.TWITTER]: 'https://a-twitter-url', - default: 'https://a-twitter-url', - }, - }, - } - - let returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - let returnedMember2 = await MemberRepository.create(member2, mockIRepositoryOptions) - let returnedMember3 = await MemberRepository.create(member3, mockIRepositoryOptions) - - // toMerge[1] = [(1,2),(1,3)] toMerge[2] = [(2,1),(2,3)] toMerge[3] = [(3,1),(3,2)] - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - - await MemberRepository.addToMerge( - [{ members: [returnedMember1.id, returnedMember3.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember3.id, returnedMember1.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember2.id, returnedMember3.id], similarity: null }], - mockIRepositoryOptions, - ) - await MemberRepository.addToMerge( - [{ members: [returnedMember3.id, returnedMember2.id], similarity: null }], - mockIRepositoryOptions, - ) - - await memberService.addToNoMerge(returnedMember1.id, returnedMember2.id) - - returnedMember1 = await MemberRepository.findById(returnedMember1.id, mockIRepositoryOptions) - - expect(returnedMember1.toMerge).toStrictEqual([returnedMember3.id]) - expect(returnedMember1.noMerge).toStrictEqual([returnedMember2.id]) - - returnedMember2 = await MemberRepository.findById(returnedMember2.id, mockIRepositoryOptions) - - expect(returnedMember2.toMerge).toStrictEqual([returnedMember3.id]) - expect(returnedMember2.noMerge).toStrictEqual([returnedMember1.id]) - - // call addToNoMerge once more, between member1 and member3 - await memberService.addToNoMerge(returnedMember1.id, returnedMember3.id) - - returnedMember1 = await MemberRepository.findById(returnedMember1.id, mockIRepositoryOptions) - - expect(returnedMember1.toMerge).toStrictEqual([]) - expect(returnedMember1.noMerge).toStrictEqual([returnedMember2.id, returnedMember3.id]) - - returnedMember3 = await MemberRepository.findById(returnedMember3.id, mockIRepositoryOptions) - - expect(returnedMember3.toMerge).toStrictEqual([returnedMember2.id]) - expect(returnedMember3.noMerge).toStrictEqual([returnedMember1.id]) - - // only toMerge relation (2,3) left. Testing addToNoMerge(2,3) - await memberService.addToNoMerge(returnedMember3.id, returnedMember2.id) - - returnedMember2 = await MemberRepository.findById(returnedMember2.id, mockIRepositoryOptions) - - expect(returnedMember2.toMerge).toStrictEqual([]) - expect(returnedMember2.noMerge).toStrictEqual([returnedMember1.id, returnedMember3.id]) - - returnedMember3 = await MemberRepository.findById(returnedMember3.id, mockIRepositoryOptions) - - expect(returnedMember3.toMerge).toStrictEqual([]) - expect(returnedMember3.noMerge).toStrictEqual([returnedMember1.id, returnedMember2.id]) - }) - - it('Should throw 404 not found when trying to add non existent members to noMerge', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - - const { randomUUID } = require('crypto') - - await expect(() => - memberService.addToNoMerge(returnedMember1.id, randomUUID()), - ).rejects.toThrowError(new Error404()) - }) - }) - - describe('memberExists method', () => { - it('Should find existing member with string username and default platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const cloned = lodash.cloneDeep(member1) - const returnedMember1 = await MemberRepository.create(cloned, mockIRepositoryOptions) - delete returnedMember1.toMerge - delete returnedMember1.noMerge - delete returnedMember1.tags - delete returnedMember1.activities - delete returnedMember1.tasks - delete returnedMember1.notes - delete returnedMember1.activityCount - delete returnedMember1.averageSentiment - delete returnedMember1.lastActive - delete returnedMember1.lastActivity - delete returnedMember1.activeOn - delete returnedMember1.identities - delete returnedMember1.activityTypes - delete returnedMember1.activeDaysCount - delete returnedMember1.numberOfOpenSourceContributions - delete returnedMember1.affiliations - delete returnedMember1.manuallyCreated - - returnedMember1.segments = returnedMember1.segments.map((s) => s.id) - - const existing = await memberService.memberExists( - member1.username[PlatformType.GITHUB], - PlatformType.GITHUB, - ) - - expect(existing).toStrictEqual(returnedMember1) - }) - - it('Should return null if member is not found - string type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - const existing = await memberService.memberExists('some-random-username', PlatformType.GITHUB) - - expect(existing).toBeNull() - }) - - it('Should return null if member is not found - object type', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - const existing = await memberService.memberExists( - { - ...member1.username, - [PlatformType.SLACK]: 'some-slack-username', - }, - PlatformType.SLACK, - ) - - expect(existing).toBeNull() - }) - - it('Should find existing member with object username and given platform', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'some-other-username', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - const returnedMember1 = await MemberRepository.create(member1, mockIRepositoryOptions) - delete returnedMember1.toMerge - delete returnedMember1.noMerge - delete returnedMember1.tags - delete returnedMember1.activities - delete returnedMember1.tasks - delete returnedMember1.notes - delete returnedMember1.activityCount - delete returnedMember1.averageSentiment - delete returnedMember1.lastActive - delete returnedMember1.lastActivity - delete returnedMember1.activeOn - delete returnedMember1.identities - delete returnedMember1.activityTypes - delete returnedMember1.activeDaysCount - delete returnedMember1.numberOfOpenSourceContributions - delete returnedMember1.affiliations - delete returnedMember1.manuallyCreated - - returnedMember1.segments = returnedMember1.segments.map((s) => s.id) - - const existing = await memberService.memberExists( - { [PlatformType.DISCORD]: 'some-other-username' }, - PlatformType.DISCORD, - ) - - expect(returnedMember1).toStrictEqual(existing) - }) - - it('Should throw 400 error when username is type of object and username[platform] is not present ', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const mas = new MemberAttributeSettingsService(mockIRepositoryOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const memberService = new MemberService(mockIRepositoryOptions) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'some-other-username', - }, - displayName: 'Anil', - joinedAt: '2021-05-27T15:14:30Z', - attributes: {}, - } - - await MemberRepository.create(member1, mockIRepositoryOptions) - - await expect(() => - memberService.memberExists( - { [PlatformType.DISCORD]: 'some-other-username' }, - PlatformType.SLACK, - ), - ).rejects.toThrowError(new Error400()) - }) - }) - - describe('Update Reach method', () => { - it('Should keep as total: -1 for an empty new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const updatedReach = MemberService.calculateReach({}, oldReach) - expect(updatedReach).toStrictEqual({ - total: -1, - }) - }) - it('Should keep as total: -1 for a default new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const updatedReach = MemberService.calculateReach({ total: -1 }, oldReach) - expect(updatedReach).toStrictEqual({ - total: -1, - }) - }) - it('Should update for a new reach and a default old reach', async () => { - const oldReach = { total: -1 } - const newReach = { [PlatformType.TWITTER]: 10 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10, - [PlatformType.TWITTER]: 10, - }) - }) - it('Should update for a new reach and old reach in the same platform', async () => { - const oldReach = { [PlatformType.TWITTER]: 5, total: 5 } - const newReach = { [PlatformType.TWITTER]: 10 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10, - [PlatformType.TWITTER]: 10, - }) - }) - it('Should update for a complex reach with different platforms', async () => { - const oldReach = { - [PlatformType.TWITTER]: 10, - [PlatformType.GITHUB]: 20, - [PlatformType.DISCORD]: 50, - total: 10 + 20 + 50, - } - const newReach = { - [PlatformType.TWITTER]: 20, - [PlatformType.GITHUB]: 2, - linkedin: 10, - total: 20 + 2 + 10, - } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 10 + 20 + 2 + 50, - [PlatformType.TWITTER]: 20, - [PlatformType.GITHUB]: 2, - linkedin: 10, - [PlatformType.DISCORD]: 50, - }) - }) - it('Should work with reach 0', async () => { - const oldReach = { total: -1 } - const newReach = { [PlatformType.TWITTER]: 0 } - const updatedReach = MemberService.calculateReach(oldReach, newReach) - expect(updatedReach).toStrictEqual({ - total: 0, - [PlatformType.TWITTER]: 0, - }) - }) - }) - - describe('getHighestPriorityPlatformForAttributes method', () => { - it('Should return the highest priority platform from a priority array, handling the exceptions', async () => { - const priorityArray = [ - PlatformType.TWITTER, - PlatformType.CROWD, - PlatformType.SLACK, - PlatformType.DEVTO, - PlatformType.DISCORD, - PlatformType.GITHUB, - ] - - let inputPlatforms = [PlatformType.GITHUB, PlatformType.DEVTO] - let highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe(PlatformType.DEVTO) - - inputPlatforms = [PlatformType.GITHUB, 'someOtherPlatform'] as any - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe(PlatformType.GITHUB) - - inputPlatforms = ['somePlatform1', 'somePlatform2'] as any - - // if no match in the priority array, it should return the first platform it finds - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - - expect(highestPriorityPlatform).toBe('somePlatform1') - - inputPlatforms = [] - - // if no platforms are sent to choose from, it should return undefined - highestPriorityPlatform = MemberService.getHighestPriorityPlatformForAttributes( - inputPlatforms, - priorityArray, - ) - expect(highestPriorityPlatform).not.toBeDefined() - }) - }) - - describe('validateAttributes method', () => { - it('Should validate attributes object succesfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - expect(validateAttributes).toEqual(attributes) - }) - - it(`Should accept custom attributes without 'custom' platform key`, async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.BIO]: 'Assistant to the Regional Manager', - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - const expectedValidatedAttributes = { - [MemberAttributeName.BIO]: { - custom: 'Assistant to the Regional Manager', - }, - } - - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it(`Should accept custom attributes both without and with 'custom' platform key`, async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: 'Dwight Schrute', - [MemberAttributeName.URL]: 'https://some-url', - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - custom: 'a custom location', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - custom: 'a custom bio', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - custom: 'a custom image url', - }, - } - - const validateAttributes = await memberService.validateAttributes(attributes) - - const expectedValidatedAttributes = { - [MemberAttributeName.NAME]: { - custom: 'Dwight Schrute', - }, - [MemberAttributeName.URL]: { - custom: 'https://some-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - custom: 'a custom location', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - custom: 'a custom bio', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - custom: 'a custom image url', - }, - } - - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it('Should throw a 400 Error when an attribute does not exist in member attribute settings', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - // in settings name has a string type, inserting an integer should throw an error - const attributes = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - 'non-existing-attribute': { - [PlatformType.TWITTER]: 'some value', - }, - } - const validateAttributes = await memberService.validateAttributes(attributes) - - // member attribute that is non existing in settings, should be omitted after validate - const expectedValidatedAttributes = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - expect(validateAttributes).toEqual(expectedValidatedAttributes) - }) - - it('Should throw a 400 Error when the type of an attribute does not match the type in member attribute settings', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - - // in settings website_url has a url type, inserting an integer should throw an error - const attributes = { - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 55, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - await expect(() => memberService.validateAttributes(attributes)).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.wrongType'), - ) - }) - }) - describe('setAttributesDefaultValues method', () => { - it('Should return the structured attributes object with default values succesfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - const attributesWithDefaultValues = await memberService.setAttributesDefaultValues(attributes) - - // Default platform priority is: custom, twitter, github, devto, slack, discord, crowd - const expectedAttributesWithDefaultValues = { - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.URL][PlatformType.GITHUB], - [PlatformType.TWITTER]: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - [PlatformType.DEVTO]: attributes[MemberAttributeName.URL][PlatformType.DEVTO], - default: attributes[MemberAttributeName.URL][PlatformType.TWITTER], - }, - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: attributes[MemberAttributeName.NAME][PlatformType.DEVTO], - default: attributes[MemberAttributeName.NAME][PlatformType.DEVTO], - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - default: attributes[MemberAttributeName.AVATAR_URL][PlatformType.TWITTER], - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes[MemberAttributeName.BIO][PlatformType.DEVTO], - default: attributes[MemberAttributeName.BIO][PlatformType.GITHUB], - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - [PlatformType.DEVTO]: attributes[MemberAttributeName.LOCATION][PlatformType.DEVTO], - default: attributes[MemberAttributeName.LOCATION][PlatformType.GITHUB], - }, - } - - expect(attributesWithDefaultValues).toEqual(expectedAttributesWithDefaultValues) - }) - - it('Should throw a 400 Error when priority array does not exist', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const memberService = new MemberService(mockIServiceOptions) - const memberAttributeSettingsService = new MemberAttributeSettingsService(mockIServiceOptions) - - await memberAttributeSettingsService.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await memberAttributeSettingsService.createPredefined(DEVTO_MEMBER_ATTRIBUTES) - - // Empty default priority array - const settings = await SettingsRepository.findOrCreateDefault({}, mockIServiceOptions) - - await SettingsRepository.save( - { ...settings, attributeSettings: { priorities: [] } }, - mockIServiceOptions, - ) - const attributes = { - [MemberAttributeName.NAME]: { - [PlatformType.DEVTO]: 'Dweet Srute', - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://some-github-url', - [PlatformType.TWITTER]: 'https://some-twitter-url', - [PlatformType.DEVTO]: 'https://some-devto-url', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - [PlatformType.DEVTO]: 'Istanbul', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Assistant to the Regional Manager', - [PlatformType.DEVTO]: 'Assistant Regional Manager', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://some-image-url', - }, - } - - await expect(() => memberService.setAttributesDefaultValues(attributes)).rejects.toThrowError( - new Error400('en', 'settings.memberAttributes.priorityArrayNotFound'), - ) - }) - }) - - describe('findAndCountAll method', () => { - it('Should filter and sort by dynamic attributes using advanced filters successfully', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - - const ms = new MemberService(mockIServiceOptions) - - const mas = new MemberAttributeSettingsService(mockIServiceOptions) - - await mas.createPredefined(GITHUB_MEMBER_ATTRIBUTES) - await mas.createPredefined(TWITTER_MEMBER_ATTRIBUTES) - await mas.createPredefined(DISCORD_MEMBER_ATTRIBUTES) - - const attribute1 = { - name: 'aNumberAttribute', - label: 'A number Attribute', - type: MemberAttributeType.NUMBER, - canDelete: true, - show: true, - } - - const attribute2 = { - name: 'aDateAttribute', - label: 'A date Attribute', - type: MemberAttributeType.DATE, - canDelete: true, - show: true, - } - - const attribute3 = { - name: 'aMultiSelectAttribute', - label: 'A multi select Attribute', - options: ['a', 'b', 'c'], - type: MemberAttributeType.MULTI_SELECT, - canDelete: true, - show: true, - } - - await mas.create(attribute1) - await mas.create(attribute2) - await mas.create(attribute3) - - const member1 = { - username: { - [PlatformType.GITHUB]: 'anil', - [PlatformType.DISCORD]: 'anil', - [PlatformType.TWITTER]: 'anil', - }, - platform: PlatformType.GITHUB, - emails: ['lala@l.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-01T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['a', 'b'], - github: ['a'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/anil', - [PlatformType.TWITTER]: 'https://twitter.com/anil', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://imcvampire.js.org/', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Lazy geek', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Helsinki, Finland', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId2', - [PlatformType.DISCORD]: '#discordId1', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/anil/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 1, - [PlatformType.TWITTER]: 2, - [PlatformType.DISCORD]: 300000, - }, - }, - contributions: [ - { - id: 112529473, - url: 'https://github.com/bighead/silicon-valley', - topics: ['TV Shows', 'Comedy', 'Startups'], - summary: 'Silicon Valley: 50 commits in 2 weeks', - numberCommits: 50, - lastCommitDate: '02/01/2023', - firstCommitDate: '01/17/2023', - }, - { - id: 112529474, - url: 'https://github.com/bighead/startup-ideas', - topics: ['Ideas', 'Startups'], - summary: 'Startup Ideas: 20 commits in 1 week', - numberCommits: 20, - lastCommitDate: '03/01/2023', - firstCommitDate: '02/22/2023', - }, - ], - joinedAt: '2022-05-28T15:13:30', - } - - const member2 = { - username: { - [PlatformType.GITHUB]: 'michaelScott', - [PlatformType.DISCORD]: 'michaelScott', - [PlatformType.TWITTER]: 'michaelScott', - }, - platform: PlatformType.GITHUB, - emails: ['michael@mifflin.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-06T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['b', 'c'], - github: ['b'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: true, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/michael-scott', - [PlatformType.TWITTER]: 'https://twitter.com/michael', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://website/michael', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Dunder & Mifflin Regional Manager', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Berlin', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId2', - [PlatformType.DISCORD]: '#discordId2', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/michael/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 1500, - [PlatformType.TWITTER]: 2500, - [PlatformType.DISCORD]: 2, - }, - }, - contributions: [ - { - id: 112529472, - url: 'https://github.com/bachman/pied-piper', - topics: ['compression', 'data', 'middle-out', 'Java'], - summary: 'Pied Piper: 10 commits in 1 day', - numberCommits: 10, - lastCommitDate: '2023-03-10', - firstCommitDate: '2023-03-01', - }, - { - id: 112529473, - url: 'https://github.com/bachman/aviato', - topics: ['Python', 'Django'], - summary: 'Aviato: 5 commits in 1 day', - numberCommits: 5, - lastCommitDate: '2023-02-25', - firstCommitDate: '2023-02-20', - }, - { - id: 112529476, - url: 'https://github.com/bachman/erlichbot', - topics: ['Python', 'Slack API'], - summary: 'ErlichBot: 2 commits in 1 day', - numberCommits: 2, - lastCommitDate: '2023-01-25', - firstCommitDate: '2023-01-24', - }, - ], - joinedAt: '2022-09-15T15:13:30', - } - - const member3 = { - username: { - [PlatformType.GITHUB]: 'jimHalpert', - [PlatformType.DISCORD]: 'jimHalpert', - [PlatformType.TWITTER]: 'jimHalpert', - }, - platform: PlatformType.GITHUB, - emails: ['jim@mifflin.com'], - score: 10, - attributes: { - aDateAttribute: { - custom: '2022-08-15T00:00:00', - }, - aMultiSelectAttribute: { - custom: ['a', 'c'], - github: ['c'], - }, - [MemberAttributeName.IS_HIREABLE]: { - [PlatformType.GITHUB]: false, - [PlatformType.DISCORD]: true, - }, - [MemberAttributeName.URL]: { - [PlatformType.GITHUB]: 'https://github.com/jim-halpert', - [PlatformType.TWITTER]: 'https://twitter.com/jim', - }, - [MemberAttributeName.WEBSITE_URL]: { - [PlatformType.GITHUB]: 'https://website/jim', - }, - [MemberAttributeName.BIO]: { - [PlatformType.GITHUB]: 'Sales guy', - }, - [MemberAttributeName.LOCATION]: { - [PlatformType.GITHUB]: 'Scranton', - }, - [MemberAttributeName.SOURCE_ID]: { - [PlatformType.TWITTER]: '#twitterId3', - [PlatformType.DISCORD]: '#discordId3', - }, - [MemberAttributeName.AVATAR_URL]: { - [PlatformType.TWITTER]: 'https://twitter.com/jim/image', - }, - aNumberAttribute: { - [PlatformType.GITHUB]: 15500, - [PlatformType.TWITTER]: 25500, - [PlatformType.DISCORD]: 200000, - }, - }, - joinedAt: '2022-09-16T15:13:30Z', - } - - const member1Created = await ms.upsert(member1) - const member2Created = await ms.upsert(member2) - const member3Created = await ms.upsert(member3) - - await SequelizeTestUtils.refreshMaterializedViews(db) - - // filter and sort by aNumberAttribute default values - let members = await ms.findAndCountAll({ - advancedFilter: { - aNumberAttribute: { - gte: 1000, - }, - }, - orderBy: 'aNumberAttribute_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id, member2Created.id]) - - // filter and sort by aNumberAttribute platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.aNumberAttribute.discord': { - gte: 100000, - }, - }, - orderBy: 'attributes.aNumberAttribute.discord_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member1Created.id, member3Created.id]) - - // filter by isHireable default values - members = await ms.findAndCountAll({ - advancedFilter: { - isHireable: true, - }, - }) - - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id]) - - // filter by isHireable platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.isHireable.discord': true, - }, - }) - - expect(members.count).toBe(3) - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member3Created.id, - member2Created.id, - member1Created.id, - ]) - - // filter and sort by url default values - members = await ms.findAndCountAll({ - advancedFilter: { - url: { - textContains: 'jim', - }, - }, - orderBy: 'url_DESC', - }) - - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id]) - - // filter and sort by url platform specific values - members = await ms.findAndCountAll({ - advancedFilter: { - 'attributes.url.github': { - textContains: 'github', - }, - }, - orderBy: 'attributes.url.github_ASC', - }) - - expect(members.count).toBe(3) - - // results will be sorted by github.url anil -> jim -> michael - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member1Created.id, - member3Created.id, - member2Created.id, - ]) - - // filter and sort by custom aDateAttribute - members = await ms.findAndCountAll({ - advancedFilter: { - aDateAttribute: { - lte: '2022-08-06T00:00:00', - }, - }, - orderBy: 'aDateAttribute_DESC', - }) - - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id, member1Created.id]) - - // filter by custom aMultiSelectAttribute - members = await ms.findAndCountAll({ - advancedFilter: { - aMultiSelectAttribute: { - overlap: ['a'], - }, - }, - orderBy: 'createdAt_DESC', - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member3Created.id, member1Created.id]) - - // filter by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [2, 6], - }, - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toEqual([member2Created.id, member1Created.id]) - - // filter by numberOfOpenSourceContributions only start - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [3], - }, - }) - expect(members.count).toBe(1) - expect(members.rows.map((i) => i.id)).toStrictEqual([member2Created.id]) - - // filter and sort by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - filter: { - numberOfOpenSourceContributionsRange: [2, 6], - }, - orderBy: 'numberOfOpenSourceContributions_ASC', - }) - expect(members.count).toBe(2) - expect(members.rows.map((i) => i.id)).toStrictEqual([member1Created.id, member2Created.id]) - - // sort by numberOfOpenSourceContributions - members = await ms.findAndCountAll({ - orderBy: 'numberOfOpenSourceContributions_ASC', - }) - expect(members.count).toBe(3) - expect(members.rows.map((i) => i.id)).toStrictEqual([ - member3Created.id, - member1Created.id, - member2Created.id, - ]) - }) - }) -}) diff --git a/backend/src/services/__tests__/microserviceService.test.ts b/backend/src/services/__tests__/microserviceService.test.ts deleted file mode 100644 index 0f8cbdeecc..0000000000 --- a/backend/src/services/__tests__/microserviceService.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import MicroserviceService from '../microserviceService' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' - -const db = null - -describe('MicroService Service tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll((done) => { - // Closing the DB connection allows Jest to exit successfully. - SequelizeTestUtils.closeConnection(db) - done() - }) - - describe('CreateIfNotExists method', () => { - it('Should create a microservice succesfully with default values', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await new MicroserviceService( - mockIRepositoryOptions, - ).createIfNotExists(microservice2Add) - - microserviceCreated.createdAt = microserviceCreated.createdAt.toISOString().split('T')[0] - microserviceCreated.updatedAt = microserviceCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(microserviceCreated).toStrictEqual(microserviceExpected) - }) - it('Should return the existing if it does not exist', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const microservice2Add = { type: 'members_score' } - - const microserviceCreated = await new MicroserviceService(mockIRepositoryOptions).create( - microservice2Add, - ) - - const secondCreated = await new MicroserviceService(mockIRepositoryOptions).createIfNotExists( - microservice2Add, - ) - - secondCreated.createdAt = secondCreated.createdAt.toISOString().split('T')[0] - secondCreated.updatedAt = secondCreated.updatedAt.toISOString().split('T')[0] - - const microserviceExpected = { - id: microserviceCreated.id, - init: false, - running: false, - type: microservice2Add.type, - variant: 'default', - settings: {}, - importHash: null, - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - tenantId: mockIRepositoryOptions.currentTenant.id, - createdById: mockIRepositoryOptions.currentUser.id, - updatedById: mockIRepositoryOptions.currentUser.id, - } - - expect(secondCreated).toStrictEqual(microserviceExpected) - const count = (await new MicroserviceService(mockIRepositoryOptions).findAndCountAll({})) - .count - expect(count).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/organizationService.test.ts b/backend/src/services/__tests__/organizationService.test.ts deleted file mode 100644 index 5843e402e1..0000000000 --- a/backend/src/services/__tests__/organizationService.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import organizationCacheRepository from '../../database/repositories/organizationCacheRepository' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import Plans from '../../security/plans' -import OrganizationService from '../organizationService' - -const db = null - -describe('OrganizationService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Create method', () => { - it('Should create organization', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - } - - const added = await service.createOrUpdate(toAdd) - expect(added.identities[0].url).toEqual(null) - }) - - it('Should throw an error when name is not sent', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions( - db, - Plans.values.growth, - ) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = {} - - await expect(service.createOrUpdate(toAdd as any)).rejects.toThrowError( - 'Missing organization identity while creating/updating organization!', - ) - }) - - it('Should not re-create when existing: not enrich and name', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const service = new OrganizationService(mockIServiceOptions) - - const toAdd = { - identities: [ - { - name: 'crowd.dev', - platform: 'crowd', - }, - ], - } - - await service.createOrUpdate(toAdd) - - const added = await service.createOrUpdate(toAdd) - expect(added.identities[0].name).toEqual(toAdd.identities[0].name) - expect(added.identities[0].url).toBeNull() - - const foundAll = await service.findAndCountAll({ - filter: {}, - includeOrganizationsWithoutMembers: true, - }) - expect(foundAll.count).toBe(1) - }) - }) -}) diff --git a/backend/src/services/__tests__/taskService.test.ts b/backend/src/services/__tests__/taskService.test.ts deleted file mode 100644 index 24595833ca..0000000000 --- a/backend/src/services/__tests__/taskService.test.ts +++ /dev/null @@ -1,301 +0,0 @@ -import TaskRepository from '../../database/repositories/taskRepository' -import UserRepository from '../../database/repositories/userRepository' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import Roles from '../../security/roles' -import TaskService from '../taskService' - -const db = null - -describe('TaskService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('Assign to user by ID', () => { - it('Should work when sending null or undefined', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const options2 = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const task2 = await service.create({ - name: 'Task 2', - assignedTo: options2.currentUser.id, - }) - - const updated = await service.assignTo(task.id, null) - expect(updated.assignees).toStrictEqual([]) - - const updated2 = await service.assignTo(task2.id, undefined) - expect(updated2.assignees).toStrictEqual([]) - }) - - it('Should work when sending changing assignee user with another user', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const updated = await service.assignTo(task.id, [user2.id]) - expect(updated.assignees.map((i) => i.id)).toStrictEqual([user2.id]) - }) - }) - - describe('Assign to user by email', () => { - it('Should work when sending null or undefined', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignees: [mockIRepositoryOptions.currentUser.id], - }) - - const task2 = await service.create({ - name: 'Task 2', - assignees: [user2.id], - }) - - const updated = await service.assignToByEmail(task.id, null) - expect(updated.assignees).toStrictEqual([]) - - const updated2 = await service.assignToByEmail(task2.id, undefined) - expect(updated2.assignees).toStrictEqual([]) - }) - - it('Should work when sending changing a user for another', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - const user2 = await UserRepository.create( - await SequelizeTestUtils.getRandomUser(), - mockIRepositoryOptions, - ) - - // add user to tenant - await mockIRepositoryOptions.database.tenantUser.create({ - roles: [Roles.values.admin], - status: 'active', - tenantId: mockIRepositoryOptions.currentTenant.id, - userId: user2.id, - }) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - assignedTo: mockIRepositoryOptions.currentUser.id, - }) - - const updated = await service.assignToByEmail(task.id, user2.email) - expect(updated.assignees.map((i) => i.id)).toStrictEqual([user2.id]) - }) - }) - - describe('Change status', () => { - it('Should work when sending an empty array', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - const task2 = await service.create({ - name: 'Task 2', - status: 'archived', - }) - - const updated = await service.updateStatus(task.id, null) - expect(updated.status).toBeNull() - - const updated2 = await service.updateStatus(task2.id, undefined) - expect(updated2.status).toBeNull() - }) - - it('Should work when sending a different status', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - const task = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - const updated = await service.updateStatus(task.id, 'done') - expect(updated.status).toBe('done') - }) - }) - - describe('findAndUpdateAll', () => { - it('Should find all tasks with given filter, and update found tasks with given payload', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - let task1 = await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - let task2 = await service.create({ - name: 'Task 2', - status: 'in-progress', - }) - - let task3 = await service.create({ - name: 'Task 3', - status: 'archived', - }) - - let task4 = await service.create({ - name: 'Task 4', - status: 'in-progress', - }) - - // change all in-progress to done - let updated = await service.findAndUpdateAll({ - filter: { - status: 'in-progress', - }, - update: { - status: 'done', - }, - }) - - expect(updated.rowsUpdated).toStrictEqual(3) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('done') - expect(task2.status).toStrictEqual('done') - expect(task3.status).toStrictEqual('archived') - expect(task4.status).toStrictEqual('done') - - // change all done to archived - updated = await service.findAndUpdateAll({ - filter: { - status: 'done', - }, - update: { - status: 'archived', - }, - }) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('archived') - expect(task2.status).toStrictEqual('archived') - expect(task3.status).toStrictEqual('archived') - expect(task4.status).toStrictEqual('archived') - - // change all archived to in progress - updated = await service.findAndUpdateAll({ - filter: { - status: 'archived', - }, - update: { - status: 'in-progress', - }, - }) - - task1 = await service.findById(task1.id) - task2 = await service.findById(task2.id) - task3 = await service.findById(task3.id) - task4 = await service.findById(task4.id) - - expect(task1.status).toStrictEqual('in-progress') - expect(task2.status).toStrictEqual('in-progress') - expect(task3.status).toStrictEqual('in-progress') - expect(task4.status).toStrictEqual('in-progress') - }) - }) - describe('findAndUpdateAll', () => { - it('Should find all tasks with given filter, and delete found tasks', async () => { - const mockIRepositoryOptions = await SequelizeTestUtils.getTestIRepositoryOptions(db) - - const service = new TaskService(mockIRepositoryOptions) - - await service.create({ - name: 'Task 1', - status: 'in-progress', - }) - - await service.create({ - name: 'Task 2', - status: 'in-progress', - }) - - const task3 = await service.create({ - name: 'Task 3', - status: 'archived', - }) - - await service.create({ - name: 'Task 4', - status: 'in-progress', - }) - - // change all in-progress to done - const deleted = await service.findAndDeleteAll({ - filter: { - status: 'in-progress', - }, - }) - - expect(deleted.rowsDeleted).toStrictEqual(3) - - // get all tasks and check count - const tasks = await TaskRepository.findAndCountAll({ filter: {} }, mockIRepositoryOptions) - - expect(tasks.count).toBe(1) - expect(tasks.rows[0].id).toStrictEqual(task3.id) - }) - }) -}) diff --git a/backend/src/services/__tests__/tenantService.test.ts b/backend/src/services/__tests__/tenantService.test.ts deleted file mode 100644 index c2cc9666d3..0000000000 --- a/backend/src/services/__tests__/tenantService.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -import moment from 'moment' -import SequelizeTestUtils from '../../database/utils/sequelizeTestUtils' -import TenantService from '../tenantService' -import MemberService from '../memberService' -import { IServiceOptions } from '../IServiceOptions' -import MicroserviceService from '../microserviceService' -import { MemberAttributeName, PlatformType } from '@crowd/types' -import MemberAttributeSettingsService from '../memberAttributeSettingsService' -import TaskService from '../taskService' -import Plans from '../../security/plans' -import { generateUUIDv1 } from '@crowd/common' -import { getRedisClient } from '@crowd/redis' -import { REDIS_CONFIG } from '../../conf' - -const db = null - -describe('TenantService tests', () => { - beforeEach(async () => { - await SequelizeTestUtils.wipeDatabase(db) - }) - - afterAll(async () => { - // Closing the DB connection allows Jest to exit successfully. - await SequelizeTestUtils.closeConnection(db) - }) - - describe('findMembersToMerge', () => { - it('Should show the same merge suggestion once, with reverse order', async () => { - const mockIServiceOptions = await SequelizeTestUtils.getTestIServiceOptions(db) - const memberService = new MemberService(mockIServiceOptions) - const tenantService = new TenantService(mockIServiceOptions) - - const memberToCreate1 = { - username: { - [PlatformType.SLACK]: { - username: 'member 1', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.SLACK, - email: 'member.1@email.com', - joinedAt: '2020-05-27T15:13:30Z', - } - - const memberToCreate2 = { - username: { - [PlatformType.DISCORD]: { - username: 'member 2', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.DISCORD, - email: 'member.2@email.com', - joinedAt: '2020-05-26T15:13:30Z', - } - - const memberToCreate3 = { - username: { - [PlatformType.GITHUB]: { - username: 'member 3', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.GITHUB, - email: 'member.3@email.com', - joinedAt: '2020-05-25T15:13:30Z', - } - - const memberToCreate4 = { - username: { - [PlatformType.TWITTER]: { - username: 'member 4', - integrationId: generateUUIDv1(), - }, - }, - platform: PlatformType.TWITTER, - email: 'member.4@email.com', - joinedAt: '2020-05-24T15:13:30Z', - } - - const member1 = await memberService.upsert(memberToCreate1) - let member2 = await memberService.upsert(memberToCreate2) - const member3 = await memberService.upsert(memberToCreate3) - let member4 = await memberService.upsert(memberToCreate4) - - await memberService.addToMerge([{ members: [member1.id, member2.id], similarity: 1 }]) - await memberService.addToMerge([{ members: [member3.id, member4.id], similarity: 0.5 }]) - - member2 = await memberService.findById(member2.id) - member4 = await memberService.findById(member4.id) - - const memberToMergeSuggestions = await tenantService.findMembersToMerge({}) - - // In the DB there should be: - // - Member 1 should have member 2 in toMerge - // - Member 3 should have member 4 in toMerge - // - Member 4 should have member 3 in toMerge - // - We should get these 4 combinations - // But this function should not return duplicates, so we should get - // only two pairs: [m2, m1] and [m4, m3] - - expect(memberToMergeSuggestions.count).toEqual(1) - - expect( - memberToMergeSuggestions.rows[0].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member1.id, member2.id]) - - expect( - memberToMergeSuggestions.rows[1].members - .sort((a, b) => (a.createdAt > b.createdAt ? 1 : -1)) - .map((m) => m.id), - ).toStrictEqual([member3.id, member4.id]) - }) - }) - - describe('_findAndCountAllForEveryUser method', () => { - it('Should succesfully find all tenants without filtering by currentUser', async () => { - let tenants = await TenantService._findAndCountAllForEveryUser({ filter: {} }) - - expect(tenants.count).toEqual(0) - expect(tenants.rows).toEqual([]) - - // generate 3 tenants - const mockIServiceOptions1 = await SequelizeTestUtils.getTestIServiceOptions(db) - const mockIServiceOptions2 = await SequelizeTestUtils.getTestIServiceOptions(db) - const mockIServiceOptions3 = await SequelizeTestUtils.getTestIServiceOptions(db) - - tenants = await TenantService._findAndCountAllForEveryUser({ filter: {} }) - - expect(tenants.count).toEqual(3) - expect(tenants.rows.map((i) => i.id).sort()).toEqual( - [ - mockIServiceOptions1.currentTenant.id, - mockIServiceOptions2.currentTenant.id, - mockIServiceOptions3.currentTenant.id, - ].sort(), - ) - }) - }) - - describe('create method', () => { - it('Should succesfully create the tenant, related default microservices, settings and suggested tasks', async () => { - const randomUser = await SequelizeTestUtils.getRandomUser() - let db = null - db = await SequelizeTestUtils.getDatabase(db) - - const userModel = await db.user.create(randomUser) - // Get options without currentTenant - const options = { - language: 'en', - currentUser: userModel, - database: db, - redis: await getRedisClient(REDIS_CONFIG, true), - } as IServiceOptions - - const tenantCreated = await new TenantService(options).create({ - name: 'testName', - url: 'testUrl', - integrationsRequired: ['github', 'discord'], - communitySize: '>25000', - }) - - const tenantCreatedPlain = tenantCreated.get({ plain: true }) - - tenantCreatedPlain.createdAt = tenantCreatedPlain.createdAt.toISOString().split('T')[0] - tenantCreatedPlain.updatedAt = tenantCreatedPlain.updatedAt.toISOString().split('T')[0] - - const tenantExpected = { - id: tenantCreatedPlain.id, - name: 'testName', - url: 'testUrl', - plan: Plans.values.essential, - isTrialPlan: false, - trialEndsAt: null, - onboardedAt: null, - integrationsRequired: ['github', 'discord'], - hasSampleData: false, - communitySize: '>25000', - createdAt: SequelizeTestUtils.getNowWithoutTime(), - updatedAt: SequelizeTestUtils.getNowWithoutTime(), - deletedAt: null, - createdById: options.currentUser.id, - updatedById: options.currentUser.id, - settings: [], - conversationSettings: [], - planSubscriptionEndsAt: null, - stripeSubscriptionId: null, - reasonForUsingCrowd: null, - } - - expect(tenantCreatedPlain).toStrictEqual(tenantExpected) - - // Check microservices (members_score should be created with tenantService.create) - const ms = new MicroserviceService({ ...options, currentTenant: tenantCreated }) - const microservicesOfTenant = await ms.findAndCountAll({}) - - expect(microservicesOfTenant.count).toEqual(1) - - // findAndCountAll returns sorted by createdAt (desc) by default, so first one should be members_score - expect(microservicesOfTenant.rows[0].type).toEqual('members_score') - - // Check default member attributes - const mas = new MemberAttributeSettingsService({ ...options, currentTenant: tenantCreated }) - const defaultAttributes = await mas.findAndCountAll({ filter: {} }) - - expect(defaultAttributes.rows.map((i) => i.name).sort()).toEqual([ - MemberAttributeName.BIO, - MemberAttributeName.IS_BOT, - MemberAttributeName.IS_ORGANIZATION, - MemberAttributeName.IS_TEAM_MEMBER, - MemberAttributeName.JOB_TITLE, - MemberAttributeName.LOCATION, - MemberAttributeName.URL, - ]) - - const taskService = new TaskService({ ...options, currentTenant: tenantCreated }) - const suggestedTasks = await taskService.findAndCountAll({ filter: {} }) - expect(suggestedTasks.rows.map((i) => i.name).sort()).toStrictEqual([ - 'Check for negative reactions', - 'Engage with relevant content', - 'Reach out to influential contacts', - 'Reach out to poorly engaged contacts', - 'Set up your team', - 'Set up your workspace integrations', - ]) - }) - }) -}) diff --git a/backend/src/services/__tests__/test-sample-data.json b/backend/src/services/__tests__/test-sample-data.json deleted file mode 100644 index e2657978c3..0000000000 --- a/backend/src/services/__tests__/test-sample-data.json +++ /dev/null @@ -1,286 +0,0 @@ -[ - { - "username": { "discord": "considerate snewkes" }, - "type": "member", - "info": {}, - "attributes": { - "url": { "github": "https://github.com/CrowdHQ" }, - "name": { "github": "considerate snewkes" }, - "isHireable": { "github": false }, - "websiteUrl": { "github": "https://crowd.dev" }, - "sourceId": { "discord": "#sample-discord-id" } - }, - "email": "team@crowd.dev", - "score": 10, - "bio": "Member of the @vuejs core team, hobby frontend hacker. Not a dev by profession.", - "organisation": "crowd.dev", - "location": "Mannheim, Germany", - "signals": null, - "joinedAt": "2021-08-18T17:47:52.000Z", - "importHash": null, - "tagsArray": [null], - "activities": [ - { - "type": "joined_guild", - "timestamp": "2022-03-16T21:00:01.000Z", - "platform": "discord", - "info": {}, - "attributes": {}, - "isContribution": false, - "score": 2, - "sourceId": "#sourceId1" - }, - { - "type": "message", - "timestamp": "2022-03-11T21:00:00.000Z", - "platform": "discord", - "info": {}, - "attributes": { "thread": false, "reactions": [], "attachments": [] }, - "isContribution": true, - "score": 1, - "sourceId": "#sourceId2", - "url": "", - "body": "Laboris cillum aliquip cupidatat dolor nisi culpa. Occaecat fugiat sunt anim.", - "channel": "introductions" - } - ], - "reach": { "total": -1 }, - "tags": [], - "noMerge": [], - "toMerge": [] - }, - { - "username": { "github": "amazing worshipper", "discord": "amazing worshipper" }, - "type": "member", - "info": {}, - "attributes": { - "url": { "github": "https://github.com/CrowdHQ" }, - "name": { "github": "amazing worshipper" }, - "isHireable": { "github": true }, - "websiteUrl": { "github": "https://crowd.dev" }, - "sourceId": { "discord": "#sample-discord-id" } - }, - "email": "team@crowd.dev", - "score": 10, - "bio": "Javascript developer. I love Vue!", - "organisation": "crowd.dev", - "location": "Brazil", - "signals": null, - "joinedAt": "2022-01-05T14:40:47.000Z", - "importHash": null, - "tagsArray": [null], - "activities": [ - { - "type": "pull_request-comment", - "timestamp": "2022-01-14T22:30:03.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/pull/5228" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId3", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/pull/5228#issuecomment-1013516865", - "body": "yes, I have some .native listeners, not sure how many, but I'll definitely search all places and go with your suggestion, thanks again, and sorry for the coffee joke, it wasn't my intention to \"buy\" a position in the queue, but to say how important this fix is for me :)", - "title": "fix(compat): ensure fallthrough *Native events are not dropped during props update (fix #5222)", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-07T21:59:37.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5222" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId4", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222#issuecomment-1007770056", - "body": "here you go: https://github.com/oswaldofreitas/vue-compat-issue", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-07T21:53:15.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5222" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId5", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222#issuecomment-1007766386", - "body": "sure, I can do it. I used that service since it's one of the suggested in the Vue 3 official docs and it was really hard to find one that I could setup the vue compat plugin. I'll create a repo for it now and share as soon as I get it.\nBefore sharing that link here I tested in incognito and it worked for me (without registering), you can click in the play button to run and in the \"Show files\" to see the code", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issues-closed", - "timestamp": "2022-01-07T20:33:21.000Z", - "platform": "github", - "info": {}, - "attributes": { "state": "closed" }, - "isContribution": true, - "score": 4, - "sourceId": "#sourceId6", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5222", - "body": "Version\n3.2.26\nReproduction link\nreplit.com\nSteps to reproduce\n\nclick in the dropdown\nselect an item\nclick in the dropdown again\n\nWhat is expected?\nthe dropdown should open again in the 2nd time\nWhat is actually happening?\nthe dropdown doesn't open\n\nWhen the input event is emitted from the form-dropdown component the event listener for the click event doesn't work anymore. I suppose it's a bug with the vue compat handling the .native modifier", - "title": "@click.native works only first time", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issue-comment", - "timestamp": "2022-01-05T15:01:11.000Z", - "platform": "github", - "info": {}, - "attributes": { "parent_url": "https://github.com/vuejs/core/issues/5210" }, - "isContribution": true, - "score": 3, - "sourceId": "#sourceId7", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5210#issuecomment-1005761356", - "body": "oh I see, you're right! Thank you", - "title": "Wrong detecting missing v-if in named slot template", - "channel": "https://github.com/vuejs/core" - }, - { - "type": "issues-closed", - "timestamp": "2022-01-05T14:40:47.100Z", - "platform": "github", - "info": {}, - "attributes": { "state": "closed" }, - "isContribution": true, - "score": 4, - "sourceId": "#sourceId8", - "importHash": null, - "parentId": null, - "url": "https://github.com/vuejs/core/issues/5210", - "body": "Version\n3.2.26\nReproduction link\nsfc.vuejs.org/\nSteps to reproduce\nIn the reproduction link there is this code:\n

H1

\n \n\nwhich is working, but if you add a v-else to the template tag it breaks\nWhat is expected?\n