Skip to content

Commit

Permalink
Contact findById activity aggregates (#1818)
Browse files Browse the repository at this point in the history
  • Loading branch information
epipav authored Nov 6, 2023
1 parent d0b4570 commit 01dc552
Show file tree
Hide file tree
Showing 4 changed files with 71 additions and 53 deletions.
6 changes: 3 additions & 3 deletions backend/src/api/cubejs/cubeJsAuth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import CubeJsService from '../../services/cubejs/cubeJsService'
import SequelizeRepository from '../../database/repositories/sequelizeRepository'
//import PermissionChecker from '../../services/user/permissionChecker'
//import Permissions from '../../security/permissions'
// import PermissionChecker from '../../services/user/permissionChecker'
// import Permissions from '../../security/permissions'

export default async (req, res) => {
//new PermissionChecker(req).validateHas(Permissions.values.memberRead)
// new PermissionChecker(req).validateHas(Permissions.values.memberRead)
const segments = SequelizeRepository.getSegmentIds(req)
const payload = await CubeJsService.generateJwtToken(req.params.tenantId, segments)
await req.responseHandler.success(req, res, payload)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ describe('MemberRepository tests', () => {
segments: mockIRepositoryOptions.currentSegments,
createdById: mockIRepositoryOptions.currentUser.id,
updatedById: mockIRepositoryOptions.currentUser.id,
activities: [],
activeOn: [],
activityTypes: [],
reach: { total: -1 },
Expand Down Expand Up @@ -257,7 +256,6 @@ describe('MemberRepository tests', () => {
segments: mockIRepositoryOptions.currentSegments,
createdById: mockIRepositoryOptions.currentUser.id,
updatedById: mockIRepositoryOptions.currentUser.id,
activities: [],
activeOn: [],
activityTypes: [],
reach: { total: -1 },
Expand Down Expand Up @@ -492,7 +490,6 @@ describe('MemberRepository tests', () => {
segments: mockIRepositoryOptions.currentSegments,
createdById: mockIRepositoryOptions.currentUser.id,
updatedById: mockIRepositoryOptions.currentUser.id,
activities: [],
activeOn: [],
activityTypes: [],
reach: { total: -1 },
Expand Down Expand Up @@ -3000,7 +2997,6 @@ describe('MemberRepository tests', () => {
segments: mockIRepositoryOptions.currentSegments,
createdById: mockIRepositoryOptions.currentUser.id,
updatedById: mockIRepositoryOptions.currentUser.id,
activities: [],
reach: { total: -1 },
notes: [],
tasks: [],
Expand Down Expand Up @@ -3184,7 +3180,6 @@ describe('MemberRepository tests', () => {
segments: mockIRepositoryOptions.currentSegments,
createdById: mockIRepositoryOptions.currentUser.id,
updatedById: mockIRepositoryOptions.currentUser.id,
activities: [],
reach: { total: -1 },
notes: [],
tasks: [],
Expand Down Expand Up @@ -3295,7 +3290,6 @@ describe('MemberRepository tests', () => {
updatedById: mockIRepositoryOptions.currentUser.id,
activeOn: [],
activityTypes: [],
activities: [],
reach: { total: -1 },
joinedAt: new Date(member1.joinedAt),
organizations: [
Expand Down
110 changes: 68 additions & 42 deletions backend/src/database/repositories/memberRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,17 @@ const { Op } = Sequelize

const log: boolean = false

interface ActivityAggregates {
memberId: string
segmentId: string
activityCount: number
activeDaysCount: number
lastActive: string
activityTypes: string[]
activeOn: string[]
averageSentiment: number
}

class MemberRepository {
static async create(data, options: IRepositoryOptions, doPopulateRelations = true) {
if (!data.username) {
Expand Down Expand Up @@ -857,6 +868,48 @@ class MemberRepository {
return segments
}

static async getActivityAggregates(
memberId: string,
options: IRepositoryOptions,
): Promise<ActivityAggregates> {
const transaction = SequelizeRepository.getTransaction(options)
const seq = SequelizeRepository.getSequelize(options)
const currentTenant = SequelizeRepository.getCurrentTenant(options)

const query = `
SELECT
a."memberId",
a."segmentId",
count(a.id)::integer AS "activityCount",
max(a.timestamp) AS "lastActive",
array_agg(DISTINCT concat(a.platform, ':', a.type)) FILTER (WHERE a.platform IS NOT NULL) AS "activityTypes",
array_agg(DISTINCT a.platform) FILTER (WHERE a.platform IS NOT NULL) AS "activeOn",
count(DISTINCT a."timestamp"::date)::integer AS "activeDaysCount",
round(avg(
CASE WHEN (a.sentiment ->> 'sentiment'::text) IS NOT NULL THEN
(a.sentiment ->> 'sentiment'::text)::double precision
ELSE
NULL::double precision
END
)::numeric, 2):: float AS "averageSentiment"
FROM activities a
WHERE a."memberId" = :memberId
and a."tenantId" = :tenantId
GROUP BY a."memberId", a."segmentId"
`

const data: ActivityAggregates[] = await seq.query(query, {
replacements: {
memberId,
tenantId: currentTenant.id,
},
type: QueryTypes.SELECT,
transaction,
})

return data?.[0] || null
}

static async setAffiliations(
memberId: string,
data: MemberSegmentAffiliation[],
Expand Down Expand Up @@ -3141,54 +3194,27 @@ class MemberRepository {

const transaction = SequelizeRepository.getTransaction(options)

output.activities = await record.getActivities({
order: [['timestamp', 'DESC']],
limit: 20,
transaction,
})
const activityAggregates = await MemberRepository.getActivityAggregates(output.id, options)

output.lastActivity = output.activities[0]?.get({ plain: true }) ?? null
output.activeOn = activityAggregates?.activeOn || []
output.activityCount = activityAggregates?.activityCount || 0
output.activityTypes = activityAggregates?.activityTypes || []
output.activeDaysCount = activityAggregates?.activeDaysCount || 0
output.averageSentiment = activityAggregates?.averageSentiment || 0

output.lastActive = output.activities[0]?.timestamp ?? null

output.activeOn = [...new Set(output.activities.map((i) => i.platform))]
output.lastActivity =
(
await record.getActivities({
order: [['timestamp', 'DESC']],
limit: 1,
transaction,
})
)[0]?.get({ plain: true }) ?? null

output.activityCount = output.activities.length
output.lastActive = output.lastActivity?.timestamp ?? null

output.numberOfOpenSourceContributions = output.contributions?.length ?? 0

output.activityTypes = [...new Set(output.activities.map((i) => `${i.platform}:${i.type}`))]
output.activeDaysCount =
output.activities.reduce((acc, activity) => {
if (!acc.datetimeHashmap) {
acc.datetimeHashmap = {}
acc.count = 0
}
// strip hours from timestamp
const date = activity.timestamp.toISOString().split('T')[0]

if (!acc.datetimeHashmap[date]) {
acc.count += 1
acc.datetimeHashmap[date] = true
}

return acc
}, {}).count ?? 0

output.averageSentiment =
output.activityCount > 0
? Math.round(
(output.activities.reduce((acc, i) => {
if (i.sentiment && 'sentiment' in i.sentiment) {
acc += i.sentiment.sentiment
}
return acc
}, 0) /
output.activities.filter((i) => i.sentiment && 'sentiment' in i.sentiment).length) *
100,
) / 100
: 0

output.tags = await record.getTags({
transaction,
order: [['createdAt', 'ASC']],
Expand Down
2 changes: 0 additions & 2 deletions backend/src/services/__tests__/memberService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1948,7 +1948,6 @@ describe('MemberService tests', () => {
)

// Sequelize returns associations as array of models, we need to get plain objects
mergedMember.activities = mergedMember.activities.map((i) => i.get({ plain: true }))
mergedMember.tags = mergedMember.tags.map((i) => i.get({ plain: true }))
mergedMember.organizations = mergedMember.organizations.map((i) =>
SequelizeTestUtils.objectWithoutKey(i.get({ plain: true }), ['memberOrganizations']),
Expand Down Expand Up @@ -2040,7 +2039,6 @@ describe('MemberService tests', () => {
contributions: null,
displayName: 'Anil',
identities: [PlatformType.GITHUB, PlatformType.DISCORD],
activities: [activityCreated],
attributes: {
...member1.attributes,
...member2.attributes,
Expand Down

0 comments on commit 01dc552

Please # to comment.