From fc9e0e31028c545133a5cb392275015bf30d6509 Mon Sep 17 00:00:00 2001 From: Anil Bostanci Date: Fri, 26 Aug 2022 18:56:41 +0200 Subject: [PATCH 1/5] weekly emails is now sent to all users of a tenant --- .../database/repositories/userRepository.ts | 15 +++++ .../workers/weeklyAnalyticsEmailsWorker.ts | 65 ++++++++++--------- .../nodejs/nodeMicroserviceSQS.ts | 6 +- .../microservices/nodejs/serverless.yml | 2 + start.sh | 2 +- 5 files changed, 55 insertions(+), 35 deletions(-) diff --git a/backend/src/database/repositories/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 16c6b67d24..6d51b5447a 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -31,6 +31,21 @@ export default class UserRepository { return this._populateRelations(record, options) } + static async findAllUsersOfTenant(tenantId){ + const options = await SequelizeRepository.getDefaultIRepositoryOptions() + + const records = await options.database.user.findAll({ + tenants: tenantId + }) + + if (records.length === 0) { + throw new Error404() + } + + return this._populateRelationsForRows(records, options) + + } + static async create(data, options: IRepositoryOptions) { const currentUser = SequelizeRepository.getCurrentUser(options) diff --git a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts index c975af2956..e561981b3e 100644 --- a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts +++ b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts @@ -9,6 +9,7 @@ import { AnalyticsEmailsOutput } from '../../messageTypes' import { platformDisplayNames } from '../../../../../utils/platformDisplayNames' import getStage from '../../../../../services/helpers/getStage' import { s3 } from '../../../../../services/aws' +import UserRepository from '../../../../../database/repositories/userRepository' /** * Sends weekly analytics of a given tenant to the user email. @@ -78,44 +79,48 @@ async function weeklyAnalyticsEmailsWorker(tenantId: string): Promise 0, - hotConversationsCount: hotConversations.length, - }, - tenant: { - name: userContext.currentTenant.name, - }, - user: { - name: userContext.currentUser.firstName, - }, - } + const allTenantUsers = await UserRepository.findAllUsersOfTenant(tenantId) const advancedSuppressionManager = { groupId: parseInt(getConfig().SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID, 10), groupsToDisplay: [parseInt(getConfig().SENDGRID_WEEKLY_ANALYTICS_UNSUBSCRIBE_GROUP_ID, 10)], } - console.log(`SENDING EMAIL!!!! to: ${userContext.currentUser.email}`) - console.log(`sendgrid template object: `) - console.log(data) + for (const user of allTenantUsers) { + if (user.email && user.emailVerified) { + const userFirstName = user.firstName ? user.firstName : user.email.split("@")[0] + + const data = { + analytics: { + dateRangeStart: dateTimeStart.format('D MMMM, YYYY'), + dateRangeEnd: dateTimeEnd.format('D MMMM, YYYY'), + activeMembers, + newMembers, + activitiesTracked: newActivities, + conversationsStarted: newConversations, + hotConversations, + hasHotConversations: hotConversations.length > 0, + hotConversationsCount: hotConversations.length, + }, + tenant: { + name: userContext.currentTenant.name, + }, + user: { + name: userFirstName, + }, + } - new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - userContext.currentUser.email, - advancedSuppressionManager, - ) + await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( + user.email, + advancedSuppressionManager, + ) - new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( - 'team@crowd.dev', - advancedSuppressionManager, - ) + await new EmailSender(EmailSender.TEMPLATES.WEEKLY_ANALYTICS, data).sendTo( + 'team@crowd.dev', + advancedSuppressionManager, + ) + } + } return { status: 200, emailSent: true } } diff --git a/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts b/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts index f11a039fa9..e1b428af2d 100644 --- a/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts +++ b/backend/src/serverless/microservices/nodejs/nodeMicroserviceSQS.ts @@ -1,10 +1,8 @@ import moment from 'moment' import { NodeMicroserviceMessage } from './messageTypes' import { getConfig } from '../../../config' +import { sqs } from '../../../services/aws' -const { SQS } = require('aws-sdk') - -const sqs = new SQS() /** * Send a message to the node microservice queue @@ -23,7 +21,7 @@ async function sendNodeMicroserviceMessage(body: NodeMicroserviceMessage): Promi await sqs .sendMessage({ - QueueUrl: getConfig().QUEUE_URL, + QueueUrl: getConfig().NODE_MICROSERVICES_SQS_URL, MessageGroupId: messageGroupId, MessageDeduplicationId: messageDeduplicationId, MessageBody: JSON.stringify(body), diff --git a/backend/src/serverless/microservices/nodejs/serverless.yml b/backend/src/serverless/microservices/nodejs/serverless.yml index cef6f312aa..1ccebb0f94 100644 --- a/backend/src/serverless/microservices/nodejs/serverless.yml +++ b/backend/src/serverless/microservices/nodejs/serverless.yml @@ -72,6 +72,8 @@ constructs: environment: NODE_ENV: ${env:NODE_ENV} NODE_MICROSERVICES_STATEMACHINE_ARN: ${env:NODE_MICROSERVICES_STATEMACHINE_ARN} + LOCALSTACK_HOSTNAME: ${env:LOCALSTACK_HOSTNAME} + LOCALSTACK_PORT: ${env:LOCALSTACK_PORT} functions: weeklyAnalyticsEmailsCoordinator: diff --git a/start.sh b/start.sh index 8ae75099d3..8928b421d2 100755 --- a/start.sh +++ b/start.sh @@ -45,6 +45,6 @@ echo $IS_DEV if [[ "$IS_DEV" = false ]]; then cd $CLI_HOME/docker && docker-compose -p crowd up --force-recreate else - bash backend/util/install-all.sh + cd $CLI_HOME && bash $CLI_HOME/backend/util/install-all.sh cd $CLI_HOME/docker && docker-compose -f docker-compose.yaml -f docker-compose.dev.yaml -p crowd up --force-recreate fi From b23d641ae600bb3e75caa3c07a6dbf11cffd3271 Mon Sep 17 00:00:00 2001 From: Anil Bostanci Date: Fri, 26 Aug 2022 20:17:37 +0200 Subject: [PATCH 2/5] tests are passing --- .../__tests__/userRepository.test.ts | 120 ++++++++++++++++++ .../database/repositories/userRepository.ts | 11 +- 2 files changed, 128 insertions(+), 3 deletions(-) create mode 100644 backend/src/database/repositories/__tests__/userRepository.test.ts diff --git a/backend/src/database/repositories/__tests__/userRepository.test.ts b/backend/src/database/repositories/__tests__/userRepository.test.ts new file mode 100644 index 0000000000..a18315d05a --- /dev/null +++ b/backend/src/database/repositories/__tests__/userRepository.test.ts @@ -0,0 +1,120 @@ +import UserRepository from '../userRepository' +import SequelizeTestUtils from '../../utils/sequelizeTestUtils' +import Error404 from '../../../errors/Error404' +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/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 6d51b5447a..51c149e760 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -31,11 +31,17 @@ export default class UserRepository { return this._populateRelations(record, options) } - static async findAllUsersOfTenant(tenantId){ + static async findAllUsersOfTenant(tenantId) { const options = await SequelizeRepository.getDefaultIRepositoryOptions() const records = await options.database.user.findAll({ - tenants: tenantId + include: [ + { + model: options.database.tenantUser, + as: 'tenants', + where: { tenantId }, + }, + ], }) if (records.length === 0) { @@ -43,7 +49,6 @@ export default class UserRepository { } return this._populateRelationsForRows(records, options) - } static async create(data, options: IRepositoryOptions) { From a08ac2801d792dfc12062166e5d7cbdef24950f6 Mon Sep 17 00:00:00 2001 From: Anil Bostanci Date: Fri, 26 Aug 2022 20:18:04 +0200 Subject: [PATCH 3/5] formatting --- .../nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts | 4 ++-- .../serverless/microservices/nodejs/nodeMicroserviceSQS.ts | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts index e561981b3e..a8ae175709 100644 --- a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts +++ b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts @@ -88,8 +88,8 @@ async function weeklyAnalyticsEmailsWorker(tenantId: string): Promise Date: Fri, 26 Aug 2022 21:03:50 +0200 Subject: [PATCH 4/5] comments and cleanup --- backend/src/database/repositories/userRepository.ts | 7 ++++++- backend/src/serverless/microservices/nodejs/serverless.yml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/backend/src/database/repositories/userRepository.ts b/backend/src/database/repositories/userRepository.ts index 51c149e760..7079439f68 100644 --- a/backend/src/database/repositories/userRepository.ts +++ b/backend/src/database/repositories/userRepository.ts @@ -31,7 +31,12 @@ export default class UserRepository { return this._populateRelations(record, options) } - static async findAllUsersOfTenant(tenantId) { + /** + * Finds all users of a tenant. + * @param tenantId + * @returns + */ + static async findAllUsersOfTenant(tenantId: string): Promise { const options = await SequelizeRepository.getDefaultIRepositoryOptions() const records = await options.database.user.findAll({ diff --git a/backend/src/serverless/microservices/nodejs/serverless.yml b/backend/src/serverless/microservices/nodejs/serverless.yml index 1ccebb0f94..f6b7fdb920 100644 --- a/backend/src/serverless/microservices/nodejs/serverless.yml +++ b/backend/src/serverless/microservices/nodejs/serverless.yml @@ -85,7 +85,7 @@ functions: NODE_ENV: ${env:NODE_ENV} EDITION: ${env:EDITION} SEGMENT_WRITE_KEY: ${env:SEGMENT_WRITE_KEY} - QUEUE_URL: ${env:NODE_MICROSERVICES_SQS_URL} + NODE_MICROSERVICES_SQS_URL: ${env:NODE_MICROSERVICES_SQS_URL} DATABASE_USERNAME: ${env:DATABASE_USERNAME} DATABASE_DIALECT: ${env:DATABASE_DIALECT} DATABASE_PASSWORD: ${env:DATABASE_PASSWORD} From 8597da7c055b4f97e90a64e85751d8e5fb7b12ac Mon Sep 17 00:00:00 2001 From: Anil Bostanci Date: Fri, 26 Aug 2022 21:06:12 +0200 Subject: [PATCH 5/5] more comments --- .../nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts index a8ae175709..968cf854bc 100644 --- a/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts +++ b/backend/src/serverless/microservices/nodejs/analytics/workers/weeklyAnalyticsEmailsWorker.ts @@ -12,7 +12,8 @@ import { s3 } from '../../../../../services/aws' import UserRepository from '../../../../../database/repositories/userRepository' /** - * Sends weekly analytics of a given tenant to the user email. + * Sends weekly analytics emails of a given tenant + * to all users of the tenant. * Data sent is for the last week. * @param tenantId */