11import { NextFunction , Request , Response } from 'express'
22
3- import { isSegmentSubproject } from '@crowd/data-access-layer/src/segments'
3+ import {
4+ buildSegmentActivityTypes ,
5+ isSegmentSubproject ,
6+ } from '@crowd/data-access-layer/src/segments'
47import { getServiceChildLogger } from '@crowd/logging'
58
69import { IRepositoryOptions } from '../database/repositories/IRepositoryOptions'
@@ -77,22 +80,23 @@ async function resolveToLeafSegments(
7780 return 'projectGroup'
7881 }
7982
80- // nullActivityTypes: strips the eagerly deep-cloned activityTypes from each record.
81- // findInIds() and populateSegmentRelations() call cloneDeep(DEFAULT_ACTIVITY_TYPE_SETTINGS)
82- // for every leaf segment. With 10K+ segments per request (e.g. merge endpoint) this
83- // allocates gigabytes and causes OOM. Downstream code (getActivityTypes) was already
84- // receiving null here before this PR, so null is the safe value to return.
8583 const nullActivityTypes = ( record : any ) => ( { ...record , activityTypes : null } )
8684
8785 if ( nonLeaf . length === 0 ) {
86+ // All inputs are already leaf subprojects. findInIds() already called populateSegmentRelations
87+ // on each record, which includes a cloneDeep(DEFAULT_ACTIVITY_TYPE_SETTINGS) per segment.
88+ // Keep activityTypes on the first record only; null the rest to release those clones.
89+ // getSegmentActivityTypes merges with lodash.merge which skips null values, so the first
90+ // record's activityTypes (default + its custom types) is sufficient for display purposes.
91+ const [ first , ...rest ] = fetched
8892 log . debug (
8993 {
9094 api : `${ req . method } ${ req . path } ` ,
9195 usedInDbQueries : fetched . map ( ( s ) => ( { id : s . id , name : s . name , level : segmentLevel ( s ) } ) ) ,
9296 } ,
9397 `All segments are already leaf — used as-is in DB queries` ,
9498 )
95- return fetched . map ( nullActivityTypes )
99+ return first ? [ first , ... rest . map ( nullActivityTypes ) ] : [ ]
96100 }
97101
98102 const leafRecords = await segmentRepository . getSegmentSubprojects ( segmentIds )
@@ -101,11 +105,19 @@ async function resolveToLeafSegments(
101105 {
102106 api : `${ req . method } ${ req . path } ` ,
103107 input_segments : nonLeaf . map ( ( s ) => ( { id : s . id , name : s . name , level : segmentLevel ( s ) } ) ) ,
104- resolved_leaf_segments : leafRecords . map ( ( s : any ) => ( { id : s . id , name : ( s as any ) . name } ) ) ,
105108 resolved_count : leafRecords . length ,
106109 } ,
107110 'Non-leaf segments resolved to leaf sub-projects' ,
108111 )
109112
110- return leafRecords . map ( nullActivityTypes )
113+ if ( leafRecords . length === 0 ) return [ ]
114+
115+ // getSegmentSubprojects returns raw DB rows (no populateSegmentRelations/cloneDeep).
116+ // Build activityTypes from the first leaf only (one cloneDeep of DEFAULT_ACTIVITY_TYPE_SETTINGS).
117+ // null the rest — getSegmentActivityTypes merges all and lodash.merge skips null sources.
118+ const [ first , ...rest ] = leafRecords
119+ return [
120+ { ...first , activityTypes : buildSegmentActivityTypes ( first ) } ,
121+ ...rest . map ( nullActivityTypes ) ,
122+ ]
111123}
0 commit comments