From 05e0acb7cf9973a2d45167ae534a662e601b268e Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Fri, 19 Jun 2026 16:55:10 +0530 Subject: [PATCH 1/5] refactor: move segment subproject expansion out of middleware into DAL Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/api/activity/activityChannels.ts | 2 +- backend/src/api/activity/activityTypes.ts | 3 +- .../repositories/integrationRepository.ts | 19 ++- .../database/repositories/memberRepository.ts | 56 +++++---- .../repositories/organizationRepository.ts | 48 +++++--- .../repositories/segmentRepository.ts | 53 -------- .../repositories/settingsRepository.ts | 2 +- backend/src/middlewares/segmentMiddleware.ts | 116 +++--------------- backend/src/services/activityService.ts | 44 +++---- backend/src/services/segmentService.ts | 50 ++++---- frontend/src/shared/axios/auth-axios.js | 3 +- .../data-access-layer/src/segments/index.ts | 59 +++++++++ 12 files changed, 201 insertions(+), 254 deletions(-) diff --git a/backend/src/api/activity/activityChannels.ts b/backend/src/api/activity/activityChannels.ts index 87986cc32a..f1b0c33208 100644 --- a/backend/src/api/activity/activityChannels.ts +++ b/backend/src/api/activity/activityChannels.ts @@ -18,7 +18,7 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.activityRead) - const payload = await new ActivityService(req).findActivityChannels(req.query.segments) + const payload = await new ActivityService(req).findActivityChannels() await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/api/activity/activityTypes.ts b/backend/src/api/activity/activityTypes.ts index 151a0dc6d0..9f8b2b64f0 100644 --- a/backend/src/api/activity/activityTypes.ts +++ b/backend/src/api/activity/activityTypes.ts @@ -4,7 +4,8 @@ import PermissionChecker from '../../services/user/permissionChecker' export default async (req, res) => { new PermissionChecker(req).validateHas(Permissions.values.activityRead) - const payload = await new ActivityService(req).findActivityTypes(req.query.segments) + + const payload = await new ActivityService(req).findActivityTypes() await req.responseHandler.success(req, res, payload) } diff --git a/backend/src/database/repositories/integrationRepository.ts b/backend/src/database/repositories/integrationRepository.ts index 388e3b4503..601dd3f8c6 100644 --- a/backend/src/database/repositories/integrationRepository.ts +++ b/backend/src/database/repositories/integrationRepository.ts @@ -13,6 +13,7 @@ import { } from '@crowd/data-access-layer/src/integrations' import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { getReposGroupedByOrgForIntegrations } from '@crowd/data-access-layer/src/repositories' +import { getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' import { IntegrationRunState, PlatformType } from '@crowd/types' import SequelizeFilterUtils from '../utils/sequelizeFilterUtils' @@ -66,10 +67,15 @@ class IntegrationRepository { const transaction = SequelizeRepository.getTransaction(options) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + let record = await options.database.integration.findOne({ where: { id, - segmentId: SequelizeRepository.getSegmentIds(options), + segmentId: subprojectIds, }, transaction, }) @@ -445,11 +451,17 @@ class IntegrationRepository { } } + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + const parser = new QueryParser( { nestedFields: { sentiment: 'sentiment.sentiment', }, + withSegments: false, }, options, ) @@ -461,11 +473,14 @@ class IntegrationRepository { offset, }) + const segmentWhere = { segmentId: subprojectIds } + const where = parsed.where ? { [Op.and]: [parsed.where, segmentWhere] } : segmentWhere + let { rows, count, // eslint-disable-line prefer-const } = await options.database.integration.findAndCountAll({ - ...(parsed.where ? { where: parsed.where } : {}), + where, ...(parsed.having ? { having: parsed.having } : {}), order: parsed.order, limit: limit ? parsed.limit : undefined, diff --git a/backend/src/database/repositories/memberRepository.ts b/backend/src/database/repositories/memberRepository.ts index 6346a9e56e..4cb8be8e8f 100644 --- a/backend/src/database/repositories/memberRepository.ts +++ b/backend/src/database/repositories/memberRepository.ts @@ -51,7 +51,7 @@ import { } from '@crowd/data-access-layer/src/members/segments' import { IDbMemberData } from '@crowd/data-access-layer/src/members/types' import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor' -import { fetchManySegments } from '@crowd/data-access-layer/src/segments' +import { fetchManySegments, getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' import { ActivityDisplayService } from '@crowd/integrations' import { ALL_PLATFORM_TYPES, @@ -148,6 +148,9 @@ class MemberRepository { ) const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) if (data.identities) { for (const i of data.identities as IMemberIdentity[]) { @@ -182,11 +185,7 @@ class MemberRepository { } } - await includeMemberToSegments( - qx, - record.id, - options.currentSegments.map((s) => s.id), - ) + await includeMemberToSegments(qx, record.id, subprojectIds) const memberService = new CommonMemberService(optionsQx(options), options.temporal, options.log) @@ -194,7 +193,7 @@ class MemberRepository { record.id, data.organizations, true, - options.currentSegments.map((s) => s.id), + subprojectIds, options, ) @@ -234,10 +233,15 @@ class MemberRepository { const bulkDeleteMemberSegments = `DELETE FROM "memberSegments" WHERE "memberId" in (:memberIds) and "segmentId" in (:segmentIds);` + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + await seq.query(bulkDeleteMemberSegments, { replacements: { memberIds, - segmentIds: SequelizeRepository.getSegmentIds(options), + segmentIds: subprojectIds, }, type: QueryTypes.DELETE, transaction, @@ -294,13 +298,12 @@ class MemberRepository { const HIGH_CONFIDENCE_LOWER_BOUND = 0.9 const MEDIUM_CONFIDENCE_LOWER_BOUND = 0.7 + const qx = SequelizeRepository.getQueryExecutor(options) const currentSegments = SequelizeRepository.getSegmentIds(options) - const segmentIds = ( - await new SegmentRepository(options).getSegmentSubprojects(currentSegments) - ).map((s) => s.id) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) - if (segmentIds.length === 0) { + if (subprojectIds.length === 0) { return args.countOnly ? { count: '0' } : { @@ -361,7 +364,7 @@ class MemberRepository { similarityFilter, displayNameFilter, { - segmentIds, + segmentIds: subprojectIds, displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, memberId: args?.filter?.memberId, }, @@ -403,7 +406,7 @@ class MemberRepository { `, { replacements: { - segmentIds, + segmentIds: subprojectIds, limit: args.limit, offset: args.offset, displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, @@ -509,7 +512,7 @@ class MemberRepository { similarityFilter, displayNameFilter, { - segmentIds, + segmentIds: subprojectIds, memberId: args?.filter?.memberId, displayName: args?.filter?.displayName ? `${args.filter.displayName}%` : undefined, }, @@ -888,13 +891,19 @@ class MemberRepository { !manualChange, // no need to track for audit if it's not a manual change ) + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds( + qx, + SequelizeRepository.getSegmentIds(options), + ) + const memberService = new CommonMemberService(optionsQx(options), options.temporal, options.log) await memberService.updateMemberOrganizations( record.id, data.organizations, data.organizationsReplace, - options.currentSegments.map((s) => s.id), + subprojectIds, options, ) @@ -915,11 +924,7 @@ class MemberRepository { } if (options.currentSegments && options.currentSegments.length > 0) { - await includeMemberToSegments( - optionsQx(options), - record.id, - options.currentSegments.map((s) => s.id), - ) + await includeMemberToSegments(qx, record.id, subprojectIds) } // Before upserting identities, check if they already exist @@ -998,8 +1003,6 @@ class MemberRepository { } } - const qx = SequelizeRepository.getQueryExecutor(options) - if (data.identitiesToCreate && data.identitiesToCreate.length > 0) { for (const i of data.identitiesToCreate) { await createMemberIdentity(qx, { @@ -1801,6 +1804,11 @@ class MemberRepository { const where = { [Op.and]: whereAnd } + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + const records = await options.database.member.findAll({ attributes: ['id', 'displayName', 'attributes'], where, @@ -1816,7 +1824,7 @@ class MemberRepository { model: options.database.segment, as: 'segments', where: { - id: SequelizeRepository.getSegmentIds(options), + id: subprojectIds, }, }, ], diff --git a/backend/src/database/repositories/organizationRepository.ts b/backend/src/database/repositories/organizationRepository.ts index c4c948c5c2..b9f5fc2e5c 100644 --- a/backend/src/database/repositories/organizationRepository.ts +++ b/backend/src/database/repositories/organizationRepository.ts @@ -34,7 +34,7 @@ import { } from '@crowd/data-access-layer/src/organizations' import { findAttribute } from '@crowd/data-access-layer/src/organizations/attributesConfig' import { optionsQx } from '@crowd/data-access-layer/src/queryExecutor' -import { findSegmentById } from '@crowd/data-access-layer/src/segments' +import { findSegmentById, getSegmentSubprojectIds } from '@crowd/data-access-layer/src/segments' import { IMemberRenderFriendlyRole, IMemberRoleWithOrganization, @@ -163,11 +163,12 @@ class OrganizationRepository { await OrganizationRepository.setIdentities(record.id, data.identities, options) } - await addOrgsToSegments( - optionsQx(options), - options.currentSegments.map((s) => s.id), - [record.id], - ) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + await addOrgsToSegments(qx, subprojectIds, [record.id]) return this.findById(record.id, options) } @@ -182,10 +183,15 @@ class OrganizationRepository { const bulkDeleteOrganizationSegments = `DELETE FROM "organizationSegments" WHERE "organizationId" in (:organizationIds) and "segmentId" in (:segmentIds);` + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const qx = SequelizeRepository.getQueryExecutor(options) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + await seq.query(bulkDeleteOrganizationSegments, { replacements: { organizationIds, - segmentIds: SequelizeRepository.getSegmentIds(options), + segmentIds: subprojectIds, }, type: QueryTypes.DELETE, transaction, @@ -430,11 +436,12 @@ class OrganizationRepository { } if (data.segments) { - await addOrgsToSegments( - optionsQx(options), - options.currentSegments.map((s) => s.id), - [record.id], - ) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + + await addOrgsToSegments(qx, subprojectIds, [record.id]) } await captureApiChange( @@ -842,10 +849,10 @@ class OrganizationRepository { const HIGH_CONFIDENCE_LOWER_BOUND = 0.9 const MEDIUM_CONFIDENCE_LOWER_BOUND = 0.7 + const qx = SequelizeRepository.getQueryExecutor(options) const currentSegments = SequelizeRepository.getSegmentIds(options) - const segmentIds = ( - await new SegmentRepository(options).getSegmentSubprojects(currentSegments) - ).map((s) => s.id) + + const segmentIds = await getSegmentSubprojectIds(qx, currentSegments) let similarityFilter = '' const similarityConditions = [] @@ -1619,7 +1626,10 @@ class OrganizationRepository { static async findAllAutocomplete(query, limit, options: IRepositoryOptions) { const tenant = SequelizeRepository.getCurrentTenant(options) - const segmentIds = SequelizeRepository.getSegmentIds(options) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) const records = await options.database.sequelize.query( ` @@ -1643,7 +1653,7 @@ class OrganizationRepository { replacements: { limit: limit ? Number(limit) : 20, tenantId: tenant.id, - segmentIds, + segmentIds: subprojectIds, queryLike: `%${query}%`, queryExact: query, uuid: validator.isUUID(query) ? query : null, @@ -1758,9 +1768,7 @@ class OrganizationRepository { const qx = SequelizeRepository.getQueryExecutor(options) const activityTypes = SegmentRepository.getActivityTypes(options) - const subprojectIds = ( - await new SegmentRepository(options).getSegmentSubprojects(currentSegments) - ).map((s) => s.id) + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) const result = await queryActivities( { diff --git a/backend/src/database/repositories/segmentRepository.ts b/backend/src/database/repositories/segmentRepository.ts index 647a0fa8b6..d5edda0e5b 100644 --- a/backend/src/database/repositories/segmentRepository.ts +++ b/backend/src/database/repositories/segmentRepository.ts @@ -295,59 +295,6 @@ class SegmentRepository extends RepositoryBase< }, {}) } - async getSegmentSubprojects(segments: string[]) { - if (segments.length === 0) return [] - - const transaction = this.transaction - - const records = await this.options.database.sequelize.query( - ` - with input_segment AS ( - select - id, - slug, - "parentSlug", - "grandparentSlug" - from segments - where id in (:segmentIds) - and "tenantId" = :tenantId - ), - segment_level AS ( - select - case - when "parentSlug" is not null and "grandparentSlug" is not null - then 'child' - when "parentSlug" is not null and "grandparentSlug" is null - then 'parent' - when "parentSlug" is null and "grandparentSlug" is null - then 'grandparent' - end as level, - id, - slug, - "parentSlug", - "grandparentSlug" - from input_segment - ) - select s.* - from segments s - join segment_level sl on (sl.level = 'child' and s.id = sl.id) - or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" is not null) - or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) - where status = 'active'; - `, - { - replacements: { - tenantId: this.options.currentTenant.id, - segmentIds: segments, - }, - type: QueryTypes.SELECT, - transaction, - }, - ) - - return records - } - async fetchTenantActivityChannels(segmentIds: string[]) { const transaction = this.transaction diff --git a/backend/src/database/repositories/settingsRepository.ts b/backend/src/database/repositories/settingsRepository.ts index 3866e7e0af..65465cf060 100644 --- a/backend/src/database/repositories/settingsRepository.ts +++ b/backend/src/database/repositories/settingsRepository.ts @@ -74,7 +74,7 @@ export default class SettingsRepository { return record } - const activityTypes = await SegmentService.getTenantActivityTypes(options.currentSegments) + const activityTypes = SegmentService.getTenantActivityTypes(options.currentSegments) const settings = record.get({ plain: true }) diff --git a/backend/src/middlewares/segmentMiddleware.ts b/backend/src/middlewares/segmentMiddleware.ts index 2d1385bc6a..8357fc845e 100644 --- a/backend/src/middlewares/segmentMiddleware.ts +++ b/backend/src/middlewares/segmentMiddleware.ts @@ -1,44 +1,29 @@ import { NextFunction, Request, Response } from 'express' -import { - buildSegmentActivityTypes, - isSegmentSubproject, -} from '@crowd/data-access-layer/src/segments' -import { getServiceChildLogger } from '@crowd/logging' - import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions' import SegmentRepository from '../database/repositories/segmentRepository' -const log = getServiceChildLogger('segmentMiddleware') - +/** Resolves active segment(s) from the request and sets `req.currentSegments` for downstream handlers. */ export async function segmentMiddleware(req: Request, _res: Response, next: NextFunction) { try { - let segments: any = null - const segmentRepository = new SegmentRepository(req as unknown as IRepositoryOptions) + const options = req as unknown as IRepositoryOptions + const segmentRepository = new SegmentRepository(options) - // Note: req.params is NOT available here. This middleware is registered via app.use(), - // which runs before Express matches a specific route and populates req.params. - // Any check on req.params (e.g. req.params.segmentId) would always be undefined. - // Route handlers that need a specific segment by ID (e.g. GET /segment/:segmentId) - // read req.params directly and ignore req.currentSegments entirely — so the - // resolution below is harmless for those endpoints. - const querySegments = toStringArray(req.query.segments) - const bodySegments = toStringArray((req.body as Record)?.segments) + // Query parameters take precedence; request body acts as the fallback. + const rawSegments = req.query.segments ?? (req.body as Record)?.segments - if (querySegments.length > 0) { - segments = { - rows: await resolveToLeafSegments(segmentRepository, querySegments, req), - } - } else if (bodySegments.length > 0) { - const resolvedRows = await resolveToLeafSegments(segmentRepository, bodySegments, req) - segments = { rows: resolvedRows } + // Express can parse segments as a single string or an array (?segments=a vs ?segments=a&segments=b). + // Normalize into a clean, flat array of non-empty strings. + const segmentIds = toStringArray(rawSegments) + + if (segmentIds.length > 0) { + options.currentSegments = await segmentRepository.findInIds(segmentIds) } else { - segments = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) + // No segmentIds in the request — use the first subproject as the default + const { rows } = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) + options.currentSegments = rows } - const options = req as unknown as IRepositoryOptions - options.currentSegments = segments.rows - next() } catch (error) { next(error) @@ -47,77 +32,14 @@ export async function segmentMiddleware(req: Request, _res: Response, next: Next /** * Safely extracts a string[] from an unknown query/body value. - * Rejects ParsedQs objects (e.g. ?segments[key]=val) that would cause type confusion. */ function toStringArray(value: unknown): string[] { if (value === undefined || value === null) return [] - const items = Array.isArray(value) ? value : [value] - return items.filter((v): v is string => typeof v === 'string') -} - -/** - * Resolves segment IDs to their leaf sub-projects. - * - * If all provided IDs are already sub-projects (leaf level), returns them as-is - * without any extra DB call — fully backward-compatible with the current behavior. - * - * If any ID is a project or project group (non-leaf), expands it to all its - * active sub-projects and applies populateSegmentRelations to match the shape - * that downstream services expect from req.currentSegments. - */ -async function resolveToLeafSegments( - segmentRepository: SegmentRepository, - segmentIds: string[], - req: Request, -) { - const fetched = await segmentRepository.findInIds(segmentIds) - - const nonLeaf = fetched.filter((s) => !isSegmentSubproject(s)) - - const segmentLevel = (s: any) => { - if (s.grandparentSlug) return 'subproject' - if (s.parentSlug) return 'project' - return 'projectGroup' - } - - const nullActivityTypes = (record: any) => ({ ...record, activityTypes: null }) - if (nonLeaf.length === 0) { - // All inputs are already leaf subprojects. findInIds() already called populateSegmentRelations - // on each record, which includes a cloneDeep(DEFAULT_ACTIVITY_TYPE_SETTINGS) per segment. - // Keep activityTypes on the first record only; null the rest to release those clones. - // getSegmentActivityTypes merges with lodash.merge which skips null values, so the first - // record's activityTypes (default + its custom types) is sufficient for display purposes. - const [first, ...rest] = fetched - log.debug( - { - api: `${req.method} ${req.path}`, - usedInDbQueries: fetched.map((s) => ({ id: s.id, name: s.name, level: segmentLevel(s) })), - }, - `All segments are already leaf — used as-is in DB queries`, - ) - return first ? [first, ...rest.map(nullActivityTypes)] : [] - } - - const leafRecords = await segmentRepository.getSegmentSubprojects(segmentIds) - - log.debug( - { - api: `${req.method} ${req.path}`, - input_segments: nonLeaf.map((s) => ({ id: s.id, name: s.name, level: segmentLevel(s) })), - resolved_count: leafRecords.length, - }, - 'Non-leaf segments resolved to leaf sub-projects', - ) - - if (leafRecords.length === 0) return [] + const items = Array.isArray(value) ? value : [value] - // getSegmentSubprojects returns raw DB rows (no populateSegmentRelations/cloneDeep). - // Build activityTypes from the first leaf only (one cloneDeep of DEFAULT_ACTIVITY_TYPE_SETTINGS). - // null the rest — getSegmentActivityTypes merges all and lodash.merge skips null sources. - const [first, ...rest] = leafRecords - return [ - { ...first, activityTypes: buildSegmentActivityTypes(first) }, - ...rest.map(nullActivityTypes), - ] + return items + .filter((item): item is string => typeof item === 'string') + .map((item) => item.trim()) + .filter(Boolean) } diff --git a/backend/src/services/activityService.ts b/backend/src/services/activityService.ts index d312ed0cc0..f0b90c7c93 100644 --- a/backend/src/services/activityService.ts +++ b/backend/src/services/activityService.ts @@ -1,6 +1,10 @@ import { queryActivities } from '@crowd/data-access-layer' +import { + getSegmentSubprojectIds, + getSegmentSubprojects, +} from '@crowd/data-access-layer/src/segments' import { LoggerBase } from '@crowd/logging' -import { IMemberIdentity, IntegrationResultType, SegmentData } from '@crowd/types' +import { IMemberIdentity, IntegrationResultType } from '@crowd/types' import SequelizeRepository from '@/database/repositories/sequelizeRepository' import { getDataSinkWorkerEmitter } from '@/serverless/utils/queueService' @@ -90,35 +94,22 @@ export default class ActivityService extends LoggerBase { } } - async findActivityTypes(segments?: string[]) { - const segmentService = new SegmentService(this.options) + async findActivityTypes() { + const qx = SequelizeRepository.getQueryExecutor(this.options) + const currentSegments = SequelizeRepository.getSegmentIds(this.options) - let subprojects: SegmentData[] - - if (!segments || segments.length === 0) { - subprojects = await segmentService.getTenantSubprojects() - } else { - subprojects = await segmentService.getSegmentSubprojects(segments) - } + const subprojects = await getSegmentSubprojects(qx, currentSegments) return SegmentService.getTenantActivityTypes(subprojects) } - async findActivityChannels(segments?: string[]) { - const segmentService = new SegmentService(this.options) - - let subprojects: SegmentData[] + async findActivityChannels() { + const qx = SequelizeRepository.getQueryExecutor(this.options) + const currentSegments = SequelizeRepository.getSegmentIds(this.options) - if (!segments || segments.length === 0) { - subprojects = await segmentService.getTenantSubprojects() - } else { - subprojects = await segmentService.getSegmentSubprojects(segments) - } + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) - return SegmentService.getTenantActivityChannels( - subprojects.map((s) => s.id), - this.options, - ) + return SegmentService.getTenantActivityChannels(subprojectIds, this.options) } async query(data) { @@ -128,13 +119,16 @@ export default class ActivityService extends LoggerBase { const offset = data.offset const countOnly = data.countOnly ?? false - const segmentIds = SequelizeRepository.getSegmentIds(this.options) const qx = SequelizeRepository.getQueryExecutor(this.options) + const currentSegments = SequelizeRepository.getSegmentIds(this.options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + const activitiyTypes = SegmentRepository.getActivityTypes(this.options) const page = await queryActivities( { - segmentIds, + segmentIds: subprojectIds, filter, orderBy, limit, diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 527a1f7e14..6b189844aa 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -11,6 +11,7 @@ import { applyOrganizationAffiliationPolicyToMembers } from '@crowd/data-access- import { deleteMemberSegmentAffiliations } from '@crowd/data-access-layer/src/member_segment_affiliations' import { buildSegmentActivityTypes, + getSegmentSubprojects, isSegmentSubproject, } from '@crowd/data-access-layer/src/segments' import { LoggerBase } from '@crowd/logging' @@ -21,6 +22,7 @@ import { SegmentCriteria, SegmentData, SegmentLevel, + SegmentRawData, SegmentUpdateData, } from '@crowd/types' @@ -547,37 +549,29 @@ export default class SegmentService extends LoggerBase { await segmentRepository.addActivityChannel(segment.id, data.platform, data.channel) } - async getSegmentSubprojects(segments: string[]) { - const segmentRepository = new SegmentRepository(this.options) - const subprojects = await segmentRepository.getSegmentSubprojects(segments) - return subprojects + async getSegmentSubprojects(segments: string[]): Promise { + const qx = SequelizeRepository.getQueryExecutor(this.options) + return getSegmentSubprojects(qx, segments) } - async getTenantSubprojects() { - const segmentRepository = new SegmentRepository(this.options) - - const { rows } = await segmentRepository.querySubprojects({}) - return rows - } + static getTenantActivityTypes(subprojects: any): ActivityTypeSettings { + return subprojects.reduce( + (acc: ActivityTypeSettings, subproject) => { + const activityTypes = buildSegmentActivityTypes(subproject) - static async getTenantActivityTypes(subprojects: any) { - if (!subprojects) { - return { custom: {}, default: {} } - } - return subprojects.reduce((acc: any, subproject) => { - const activityTypes = buildSegmentActivityTypes(subproject) - - return { - custom: { - ...acc.custom, - ...activityTypes.custom, - }, - default: { - ...acc.default, - ...activityTypes.default, - }, - } - }, {}) + return { + custom: { + ...acc.custom, + ...activityTypes.custom, + }, + default: { + ...acc.default, + ...activityTypes.default, + }, + } + }, + { custom: {}, default: {} } as ActivityTypeSettings, + ) } static async getTenantActivityChannels(segments: string[], options: any) { diff --git a/frontend/src/shared/axios/auth-axios.js b/frontend/src/shared/axios/auth-axios.js index f164516b87..1ea192f4f8 100644 --- a/frontend/src/shared/axios/auth-axios.js +++ b/frontend/src/shared/axios/auth-axios.js @@ -46,8 +46,7 @@ authAxios.interceptors.request.use( segments = options.data.segments; } else if (hasSegmentsQueryParams) { segments = options.params.segments; - // If neither body or query params have segments, use the selected project group id. - // The backend segment middleware resolves it to the correct leaf sub-projects. + // Default to the selected project group when no segments are specified. } else if (selectedProjectGroup.value?.id) { segments = [selectedProjectGroup.value.id]; } diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index 0d1ce81acc..3acfbde604 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -1,6 +1,7 @@ import lodash from 'lodash' import cloneDeep from 'lodash.clonedeep' +import { DEFAULT_TENANT_ID } from '@crowd/common' import { DEFAULT_ACTIVITY_TYPE_SETTINGS } from '@crowd/integrations' import { ActivityTypeSettings, PlatformType, SegmentData, SegmentRawData } from '@crowd/types' @@ -156,6 +157,64 @@ export function isSegmentSubproject(segment: SegmentData | SegmentRawData): bool return segment.slug != null && segment.parentSlug != null && segment.grandparentSlug != null } +export async function getSegmentSubprojects( + qx: QueryExecutor, + segmentIds: string[], +): Promise { + if (segmentIds.length === 0) { + return [] + } + + return qx.select( + ` + with input_segment AS ( + select + id, + slug, + "parentSlug", + "grandparentSlug" + from segments + where id = ANY($(segmentIds)::UUID[]) + and "tenantId" = $(tenantId) + ), + segment_level AS ( + select + case + when "parentSlug" is not null and "grandparentSlug" is not null + then 'child' + when "parentSlug" is not null and "grandparentSlug" is null + then 'parent' + when "parentSlug" is null and "grandparentSlug" is null + then 'grandparent' + end as level, + id, + slug, + "parentSlug", + "grandparentSlug" + from input_segment + ) + select s.* + from segments s + join segment_level sl on (sl.level = 'child' and s.id = sl.id) + or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" is not null) + or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) + where status = 'active'; + `, + { + tenantId: DEFAULT_TENANT_ID, + segmentIds, + }, + ) +} + +export async function getSegmentSubprojectIds( + qx: QueryExecutor, + segmentIds: string[], +): Promise { + const subprojects = await getSegmentSubprojects(qx, segmentIds) + return subprojects.map((s) => s.id) +} + export function buildSegmentActivityTypes(segment: SegmentRawData): ActivityTypeSettings { const activityTypes = {} as ActivityTypeSettings From 45be6a3c7e27ef2fb3ed0bbc2c08264c069fab73 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:00:33 +0530 Subject: [PATCH 2/5] fix: make prettier and linter happy Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- .../database/repositories/integrationRepository.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/backend/src/database/repositories/integrationRepository.ts b/backend/src/database/repositories/integrationRepository.ts index 601dd3f8c6..7aca1d55bc 100644 --- a/backend/src/database/repositories/integrationRepository.ts +++ b/backend/src/database/repositories/integrationRepository.ts @@ -451,16 +451,14 @@ class IntegrationRepository { } } - const qx = SequelizeRepository.getQueryExecutor(options) - const currentSegments = SequelizeRepository.getSegmentIds(options) - - const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) - const parser = new QueryParser( { nestedFields: { sentiment: 'sentiment.sentiment', }, + // QueryParser filters on req.currentSegments directly (e.g., projectGroupId). + // Since integrations are stored per subproject, segment filtering is applied manually below + // after expanding to subprojectIds. withSegments: false, }, options, @@ -473,6 +471,11 @@ class IntegrationRepository { offset, }) + const qx = SequelizeRepository.getQueryExecutor(options) + const currentSegments = SequelizeRepository.getSegmentIds(options) + + const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + const segmentWhere = { segmentId: subprojectIds } const where = parsed.where ? { [Op.and]: [parsed.where, segmentWhere] } : segmentWhere From 29cabbbcf73d09b39ed70f226e78b6e8cad16f9b Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Fri, 19 Jun 2026 17:35:31 +0530 Subject: [PATCH 3/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/middlewares/segmentMiddleware.ts | 9 +++------ backend/src/services/activityService.ts | 9 +++++++++ backend/src/services/segmentService.ts | 6 ++++-- services/libs/data-access-layer/src/segments/index.ts | 3 ++- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/backend/src/middlewares/segmentMiddleware.ts b/backend/src/middlewares/segmentMiddleware.ts index 8357fc845e..416ab21d9b 100644 --- a/backend/src/middlewares/segmentMiddleware.ts +++ b/backend/src/middlewares/segmentMiddleware.ts @@ -9,17 +9,14 @@ export async function segmentMiddleware(req: Request, _res: Response, next: Next const options = req as unknown as IRepositoryOptions const segmentRepository = new SegmentRepository(options) - // Query parameters take precedence; request body acts as the fallback. - const rawSegments = req.query.segments ?? (req.body as Record)?.segments + const querySegments = toStringArray(req.query.segments) + const bodySegments = toStringArray((req.body as Record)?.segments) - // Express can parse segments as a single string or an array (?segments=a vs ?segments=a&segments=b). - // Normalize into a clean, flat array of non-empty strings. - const segmentIds = toStringArray(rawSegments) + const segmentIds = querySegments.length > 0 ? querySegments : bodySegments if (segmentIds.length > 0) { options.currentSegments = await segmentRepository.findInIds(segmentIds) } else { - // No segmentIds in the request — use the first subproject as the default const { rows } = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) options.currentSegments = rows } diff --git a/backend/src/services/activityService.ts b/backend/src/services/activityService.ts index f0b90c7c93..a178184f97 100644 --- a/backend/src/services/activityService.ts +++ b/backend/src/services/activityService.ts @@ -124,6 +124,15 @@ export default class ActivityService extends LoggerBase { const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + if (subprojectIds.length === 0) { + return { + count: 0, + rows: [], + limit, + offset, + } + } + const activitiyTypes = SegmentRepository.getActivityTypes(this.options) const page = await queryActivities( diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 6b189844aa..668348b4f8 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -554,10 +554,12 @@ export default class SegmentService extends LoggerBase { return getSegmentSubprojects(qx, segments) } - static getTenantActivityTypes(subprojects: any): ActivityTypeSettings { + static getTenantActivityTypes( + subprojects: Array, + ): ActivityTypeSettings { return subprojects.reduce( (acc: ActivityTypeSettings, subproject) => { - const activityTypes = buildSegmentActivityTypes(subproject) + const activityTypes = buildSegmentActivityTypes(subproject as SegmentRawData) return { custom: { diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index 3acfbde604..116ac9676a 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -198,7 +198,8 @@ export async function getSegmentSubprojects( join segment_level sl on (sl.level = 'child' and s.id = sl.id) or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" is not null) or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) - where status = 'active'; + where status = 'active' + and s."tenantId" = $(tenantId); `, { tenantId: DEFAULT_TENANT_ID, From 74e61b2a4b79a922218bb824662d1612c93740c3 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Fri, 19 Jun 2026 19:19:43 +0530 Subject: [PATCH 4/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/middlewares/segmentMiddleware.ts | 2 +- services/libs/data-access-layer/src/segments/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/middlewares/segmentMiddleware.ts b/backend/src/middlewares/segmentMiddleware.ts index 416ab21d9b..cd2f5c5c4d 100644 --- a/backend/src/middlewares/segmentMiddleware.ts +++ b/backend/src/middlewares/segmentMiddleware.ts @@ -3,7 +3,7 @@ import { NextFunction, Request, Response } from 'express' import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions' import SegmentRepository from '../database/repositories/segmentRepository' -/** Resolves active segment(s) from the request and sets `req.currentSegments` for downstream handlers. */ +/** Resolves segment(s) from the request and sets `req.currentSegments` for downstream handlers. */ export async function segmentMiddleware(req: Request, _res: Response, next: NextFunction) { try { const options = req as unknown as IRepositoryOptions diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index 116ac9676a..faf257ed9d 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -196,7 +196,7 @@ export async function getSegmentSubprojects( select s.* from segments s join segment_level sl on (sl.level = 'child' and s.id = sl.id) - or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" is not null) + or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" = sl."parentSlug") or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) where status = 'active' and s."tenantId" = $(tenantId); From d8e87e3af75b026a45582c7050a242ec512ba585 Mon Sep 17 00:00:00 2001 From: Yeganathan S <63534555+skwowet@users.noreply.github.com> Date: Mon, 22 Jun 2026 13:49:44 +0530 Subject: [PATCH 5/5] fix: resolve pr review comments Signed-off-by: Yeganathan S <63534555+skwowet@users.noreply.github.com> --- backend/src/services/activityService.ts | 9 ++++----- backend/src/services/segmentService.ts | 6 +++++- services/libs/data-access-layer/src/segments/index.ts | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/backend/src/services/activityService.ts b/backend/src/services/activityService.ts index a178184f97..9ac5708835 100644 --- a/backend/src/services/activityService.ts +++ b/backend/src/services/activityService.ts @@ -10,7 +10,6 @@ import SequelizeRepository from '@/database/repositories/sequelizeRepository' import { getDataSinkWorkerEmitter } from '@/serverless/utils/queueService' import ActivityRepository from '../database/repositories/activityRepository' -import SegmentRepository from '../database/repositories/segmentRepository' import { UsernameIdentities, mapUsernameToIdentities, @@ -122,9 +121,9 @@ export default class ActivityService extends LoggerBase { const qx = SequelizeRepository.getQueryExecutor(this.options) const currentSegments = SequelizeRepository.getSegmentIds(this.options) - const subprojectIds = await getSegmentSubprojectIds(qx, currentSegments) + const subprojects = await getSegmentSubprojects(qx, currentSegments) - if (subprojectIds.length === 0) { + if (subprojects.length === 0) { return { count: 0, rows: [], @@ -133,11 +132,11 @@ export default class ActivityService extends LoggerBase { } } - const activitiyTypes = SegmentRepository.getActivityTypes(this.options) + const activitiyTypes = SegmentService.getTenantActivityTypes(subprojects) const page = await queryActivities( { - segmentIds: subprojectIds, + segmentIds: subprojects.map((s) => s.id), filter, orderBy, limit, diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 668348b4f8..b70ae5b951 100644 --- a/backend/src/services/segmentService.ts +++ b/backend/src/services/segmentService.ts @@ -555,8 +555,12 @@ export default class SegmentService extends LoggerBase { } static getTenantActivityTypes( - subprojects: Array, + subprojects?: Array | null, ): ActivityTypeSettings { + if (!subprojects?.length) { + return { custom: {}, default: {} } + } + return subprojects.reduce( (acc: ActivityTypeSettings, subproject) => { const activityTypes = buildSegmentActivityTypes(subproject as SegmentRawData) diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index faf257ed9d..a3cef8a2e5 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -193,7 +193,7 @@ export async function getSegmentSubprojects( "grandparentSlug" from input_segment ) - select s.* + select distinct s.* from segments s join segment_level sl on (sl.level = 'child' and s.id = sl.id) or (sl.level = 'parent' and s."parentSlug" = sl.slug and s."grandparentSlug" = sl."parentSlug")