Skip to content

Commit fee5fcc

Browse files
authored
refactor: aggregate calculations for members and organizations (#3041)
1 parent 8fcc65b commit fee5fcc

37 files changed

Lines changed: 848 additions & 677 deletions
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
drop index concurrently if exists "ix_activityRelations_memberId_segmentId_include";
2+
drop index concurrently if exists "ix_activityRelations_organizationId_segmentId_include";
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
alter table "memberSegmentsAgg"
2+
drop column if exists "updatedAt";
3+
4+
alter table "organizationSegmentsAgg"
5+
drop column if exists "createdAt";
6+
7+
alter table "organizationSegmentsAgg"
8+
drop column if exists "updatedAt";
9+
10+
drop table if exists "systemSettings";
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
drop index concurrently if exists "idx_indexed_entities_type_indexed_at_entity_id";
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-- Indexes to optimize queries for member and organization activity core aggregates
2+
create index concurrently if not exists "ix_activityRelations_memberId_segmentId_include"
3+
on "activityRelations" ("memberId", "segmentId")
4+
include ("platform", "activityId");
5+
6+
create index concurrently if not exists "ix_activityRelations_organizationId_segmentId_include"
7+
on "activityRelations" ("organizationId", "segmentId")
8+
include ("platform", "activityId", "memberId");
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-- Add "updatedAt" column to track the async aggs updates
2+
alter table "memberSegmentsAgg"
3+
add column if not exists "updatedAt" timestamp with time zone not null default now();
4+
5+
-- For existing rows, set the initial value to the createdAt value
6+
update "memberSegmentsAgg" set "updatedAt" = "createdAt";
7+
8+
-- Adding "createdAt" and "updatedAt" to make it consistent with the memberSegmentsAgg table.
9+
alter table "organizationSegmentsAgg"
10+
add column if not exists "createdAt" timestamp with time zone not null default now();
11+
12+
alter table "organizationSegmentsAgg"
13+
add column if not exists "updatedAt" timestamp with time zone not null default now();
14+
15+
-- table store system wide settings since we are moving away from tenants
16+
create table if not exists "systemSettings" (
17+
name varchar(255) not null primary key,
18+
value jsonb not null,
19+
description text,
20+
"createdAt" timestamp with time zone not null default now(),
21+
"updatedAt" timestamp with time zone not null default now()
22+
);
23+
24+
-- system settings for the display aggs last synced at
25+
insert into "systemSettings" (name, value) values ('memberDisplayAggsLastSyncedAt', '{"timestamp": "2025-05-13T00:00:00Z"}');
26+
27+
insert into "systemSettings" (name, value) values ('organizationDisplayAggsLastSyncedAt', '{"timestamp": "2025-05-13T00:00:00Z"}');
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
-- Index to optimize fetching recently indexed entities
2+
create index concurrently if not exists "idx_indexed_entities_type_indexed_at_entity_id"
3+
on "indexed_entities" ("type", "indexed_at", "entity_id");

backend/src/database/repositories/memberRepository.ts

Lines changed: 0 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,12 @@ import {
1414
Error404,
1515
Error409,
1616
RawQueryParser,
17-
distinct,
1817
groupBy,
1918
} from '@crowd/common'
2019
import {
2120
countMembersWithActivities,
2221
getActiveMembers,
2322
getLastActivitiesForMembers,
24-
getMemberAggregates,
2523
setMemberDataToActivities,
2624
} from '@crowd/data-access-layer'
2725
import { findManyLfxMemberships } from '@crowd/data-access-layer/src/lfx_memberships'
@@ -107,17 +105,6 @@ const { Op } = Sequelize
107105

108106
const log: boolean = false
109107

110-
interface ActivityAggregates {
111-
memberId: string
112-
segmentId: string
113-
activityCount: number
114-
activeDaysCount: number
115-
lastActive: string
116-
activityTypes: string[]
117-
activeOn: string[]
118-
averageSentiment: number
119-
}
120-
121108
class MemberRepository {
122109
static async create(data, options: IRepositoryOptions) {
123110
if (!data.username && !data.identities) {
@@ -1238,44 +1225,6 @@ class MemberRepository {
12381225
return segments
12391226
}
12401227

1241-
static async getActivityAggregates(
1242-
memberId: string,
1243-
options: IRepositoryOptions,
1244-
segmentId?: string,
1245-
): Promise<ActivityAggregates> {
1246-
if (segmentId) {
1247-
// we load data for a specific segment (can be leaf, parent or grand parent id)
1248-
const member = (
1249-
await queryMembersAdvanced(optionsQx(options), options.redis, {
1250-
filter: { and: [{ id: { eq: memberId } }] },
1251-
limit: 1,
1252-
offset: 0,
1253-
fields: ['activityCount', 'activityTypes', 'activeOn', 'averageSentiment', 'lastActive'],
1254-
segmentId,
1255-
})
1256-
).rows[0]
1257-
1258-
return {
1259-
activeDaysCount: member?.activeDaysCount || 0,
1260-
activityCount: member?.activityCount || 0,
1261-
activityTypes: member?.activityTypes || [],
1262-
activeOn: member?.activeOn || [],
1263-
averageSentiment: member?.averageSentiment || 0,
1264-
lastActive: member?.lastActive || null,
1265-
memberId,
1266-
segmentId,
1267-
}
1268-
}
1269-
1270-
const results = await getMemberAggregates(options.qdb, memberId)
1271-
1272-
if (results.length > 0) {
1273-
return results[0]
1274-
}
1275-
1276-
return null
1277-
}
1278-
12791228
static async setAffiliations(
12801229
memberId: string,
12811230
data: MemberSegmentAffiliation[],
@@ -2430,147 +2379,6 @@ class MemberRepository {
24302379
)
24312380
}
24322381

2433-
/**
2434-
* Fill a record with the relations and files (if any)
2435-
* @param record Record to get relations and files for
2436-
* @param options IRepository options
2437-
* @param returnPlain If true: return object, otherwise return model
2438-
* @returns The model/object with filled relations and files
2439-
*/
2440-
static async _populateRelations(
2441-
record,
2442-
options: IRepositoryOptions,
2443-
{
2444-
returnPlain,
2445-
segmentId,
2446-
newIdentities,
2447-
withActivityAggregates,
2448-
}: {
2449-
returnPlain: boolean
2450-
segmentId: string
2451-
newIdentities: boolean
2452-
withActivityAggregates: boolean
2453-
},
2454-
) {
2455-
if (!record) {
2456-
return record
2457-
}
2458-
2459-
let output
2460-
2461-
if (returnPlain) {
2462-
output = record.get({ plain: true })
2463-
} else {
2464-
output = record
2465-
}
2466-
2467-
const transaction = SequelizeRepository.getTransaction(options)
2468-
2469-
if (withActivityAggregates) {
2470-
const activityAggregates = await MemberRepository.getActivityAggregates(
2471-
output.id,
2472-
options,
2473-
segmentId,
2474-
)
2475-
2476-
output.activeOn = activityAggregates?.activeOn || []
2477-
output.activityCount = activityAggregates?.activityCount || 0
2478-
output.activityTypes = activityAggregates?.activityTypes || []
2479-
output.activeDaysCount = activityAggregates?.activeDaysCount || 0
2480-
output.averageSentiment = activityAggregates?.averageSentiment || 0
2481-
}
2482-
2483-
output.lastActivity =
2484-
(
2485-
await record.getActivities({
2486-
order: [['timestamp', 'DESC']],
2487-
limit: 1,
2488-
transaction,
2489-
})
2490-
)[0]?.get({ plain: true }) ?? null
2491-
2492-
output.lastActive = output.lastActivity?.timestamp ?? null
2493-
2494-
output.numberOfOpenSourceContributions = output.contributions?.length ?? 0
2495-
2496-
output.tags = await record.getTags({
2497-
transaction,
2498-
order: [['createdAt', 'ASC']],
2499-
joinTableAttributes: [],
2500-
})
2501-
2502-
output.organizations = await record.getOrganizations({
2503-
transaction,
2504-
order: [['createdAt', 'ASC']],
2505-
joinTableAttributes: ['dateStart', 'dateEnd', 'title', 'source'],
2506-
through: {
2507-
where: {
2508-
deletedAt: null,
2509-
},
2510-
},
2511-
})
2512-
MemberRepository.sortOrganizations(output.organizations)
2513-
2514-
output.noMerge = (
2515-
await record.getNoMerge({
2516-
transaction,
2517-
})
2518-
).map((i) => i.id)
2519-
2520-
output.toMerge = (
2521-
await record.getToMerge({
2522-
transaction,
2523-
})
2524-
).map((i) => i.id)
2525-
2526-
const memberIdentities = (await this.getIdentities([record.id], options)).get(record.id)
2527-
2528-
if (newIdentities === true) {
2529-
output.identities = memberIdentities
2530-
output.verifiedEmails = distinct(
2531-
memberIdentities
2532-
.filter((i) => i.verified && i.type === MemberIdentityType.EMAIL)
2533-
.map((i) => i.value),
2534-
)
2535-
output.unverifiedEmails = distinct(
2536-
memberIdentities
2537-
.filter((i) => !i.verified && i.type === MemberIdentityType.EMAIL)
2538-
.map((i) => i.value),
2539-
)
2540-
output.verifiedUsernames = distinct(
2541-
memberIdentities
2542-
.filter((i) => i.verified && i.type === MemberIdentityType.USERNAME)
2543-
.map((i) => i.value),
2544-
)
2545-
output.unverifiedUsernames = distinct(
2546-
memberIdentities
2547-
.filter((i) => !i.verified && i.type === MemberIdentityType.USERNAME)
2548-
.map((i) => i.value),
2549-
)
2550-
output.identityPlatforms = distinct(
2551-
memberIdentities.filter((i) => i.verified).map((i) => i.platform),
2552-
)
2553-
} else {
2554-
output.username = {}
2555-
2556-
for (const identity of memberIdentities.filter(
2557-
(i) => i.type === MemberIdentityType.USERNAME,
2558-
)) {
2559-
if (output.username[identity.platform]) {
2560-
output.username[identity.platform].push(identity.value)
2561-
} else {
2562-
output.username[identity.platform] = [identity.value]
2563-
}
2564-
}
2565-
2566-
output.identities = Object.keys(output.username)
2567-
}
2568-
2569-
output.affiliations = await this.getAffiliations(record.id, options)
2570-
2571-
return output
2572-
}
2573-
25742382
static async updateMemberOrganizations(
25752383
record,
25762384
organizations,

0 commit comments

Comments
 (0)