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..7aca1d55bc 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, }) @@ -450,6 +456,10 @@ class IntegrationRepository { 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, ) @@ -461,11 +471,19 @@ 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 + 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..cd2f5c5c4d 100644 --- a/backend/src/middlewares/segmentMiddleware.ts +++ b/backend/src/middlewares/segmentMiddleware.ts @@ -1,44 +1,26 @@ 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 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) - 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 } + const segmentIds = querySegments.length > 0 ? querySegments : bodySegments + + if (segmentIds.length > 0) { + options.currentSegments = await segmentRepository.findInIds(segmentIds) } else { - segments = await segmentRepository.querySubprojects({ limit: 1, offset: 0 }) + 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 +29,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..9ac5708835 100644 --- a/backend/src/services/activityService.ts +++ b/backend/src/services/activityService.ts @@ -1,12 +1,15 @@ 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' import ActivityRepository from '../database/repositories/activityRepository' -import SegmentRepository from '../database/repositories/segmentRepository' import { UsernameIdentities, mapUsernameToIdentities, @@ -90,35 +93,22 @@ export default class ActivityService extends LoggerBase { } } - async findActivityTypes(segments?: string[]) { - const segmentService = new SegmentService(this.options) - - let subprojects: SegmentData[] + async findActivityTypes() { + 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 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 +118,25 @@ 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 activitiyTypes = SegmentRepository.getActivityTypes(this.options) + const currentSegments = SequelizeRepository.getSegmentIds(this.options) + + const subprojects = await getSegmentSubprojects(qx, currentSegments) + + if (subprojects.length === 0) { + return { + count: 0, + rows: [], + limit, + offset, + } + } + + const activitiyTypes = SegmentService.getTenantActivityTypes(subprojects) const page = await queryActivities( { - segmentIds, + segmentIds: subprojects.map((s) => s.id), filter, orderBy, limit, diff --git a/backend/src/services/segmentService.ts b/backend/src/services/segmentService.ts index 527a1f7e14..b70ae5b951 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,35 @@ 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 async getTenantActivityTypes(subprojects: any) { - if (!subprojects) { + static getTenantActivityTypes( + subprojects?: Array | null, + ): ActivityTypeSettings { + if (!subprojects?.length) { 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 subprojects.reduce( + (acc: ActivityTypeSettings, subproject) => { + const activityTypes = buildSegmentActivityTypes(subproject as SegmentRawData) + + 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..a3cef8a2e5 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,65 @@ 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 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") + or (sl.level = 'grandparent' and s."grandparentSlug" = sl.slug) + where status = 'active' + and s."tenantId" = $(tenantId); + `, + { + 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