From 411505fd79a6b949a127174716bd2aa61292f561 Mon Sep 17 00:00:00 2001 From: doswalt Date: Tue, 23 Jun 2026 13:46:16 -0400 Subject: [PATCH] parrallel get flag from context --- .../api/repositories/FeatureFlagRepository.ts | 62 +++++++++----- .../FeatureFlagRepository.test.ts | 85 +++++++++++++++++++ 2 files changed, 127 insertions(+), 20 deletions(-) diff --git a/packages/backend/src/api/repositories/FeatureFlagRepository.ts b/packages/backend/src/api/repositories/FeatureFlagRepository.ts index 0f6da24de1..82f348543f 100644 --- a/packages/backend/src/api/repositories/FeatureFlagRepository.ts +++ b/packages/backend/src/api/repositories/FeatureFlagRepository.ts @@ -1,4 +1,4 @@ -import { Repository, EntityManager } from 'typeorm'; +import { Repository, EntityManager, SelectQueryBuilder } from 'typeorm'; import { EntityRepository } from '../../typeorm-typedi-extensions'; import { FeatureFlag } from '../models/FeatureFlag'; import repositoryError from './utils/repositoryError'; @@ -91,26 +91,48 @@ export class FeatureFlagRepository extends Repository { } public async getFlagsFromContext(context: string): Promise { - const result = await this.createQueryBuilder('feature_flag') - .leftJoinAndSelect('feature_flag.featureFlagSegmentInclusion', 'featureFlagSegmentInclusion') - .leftJoinAndSelect('featureFlagSegmentInclusion.segment', 'segmentInclusion') - .leftJoinAndSelect('segmentInclusion.individualForSegment', 'individualForSegment') - .leftJoinAndSelect('segmentInclusion.groupForSegment', 'groupForSegment') - .leftJoinAndSelect('segmentInclusion.subSegments', 'subSegment') - .leftJoinAndSelect('feature_flag.featureFlagSegmentExclusion', 'featureFlagSegmentExclusion') - .leftJoinAndSelect('featureFlagSegmentExclusion.segment', 'segmentExclusion') - .leftJoinAndSelect('segmentExclusion.individualForSegment', 'individualForSegmentExclusion') - .leftJoinAndSelect('segmentExclusion.groupForSegment', 'groupForSegmentExclusion') - .leftJoinAndSelect('segmentExclusion.subSegments', 'subSegmentExclusion') - .where('feature_flag.context @> :searchContext', { searchContext: [context] }) - .andWhere('feature_flag.status = :status', { status: FEATURE_FLAG_STATUS.ENABLED }) - .getMany() - .catch((errorMsg: any) => { - const errorMsgString = repositoryError('FeatureFlagRepository', 'getFlagsFromContext', { context }, errorMsg); - throw errorMsgString; - }); + const addExclusionJoins = (qb: SelectQueryBuilder) => + qb + .leftJoinAndSelect('feature_flag.featureFlagSegmentExclusion', 'featureFlagSegmentExclusion') + .leftJoinAndSelect('featureFlagSegmentExclusion.segment', 'segmentExclusion') + .leftJoinAndSelect('segmentExclusion.individualForSegment', 'individualForSegmentExclusion') + .leftJoinAndSelect('segmentExclusion.groupForSegment', 'groupForSegmentExclusion') + .leftJoinAndSelect('segmentExclusion.subSegments', 'subSegmentExclusion'); - return result; + const addInclusionJoins = (qb: SelectQueryBuilder) => + qb + .leftJoinAndSelect('feature_flag.featureFlagSegmentInclusion', 'featureFlagSegmentInclusion') + .leftJoinAndSelect('featureFlagSegmentInclusion.segment', 'segmentInclusion') + .leftJoinAndSelect('segmentInclusion.individualForSegment', 'individualForSegment') + .leftJoinAndSelect('segmentInclusion.groupForSegment', 'groupForSegment') + .leftJoinAndSelect('segmentInclusion.subSegments', 'subSegment'); + + const addBaseConditions = (qb: SelectQueryBuilder) => + qb + .where('feature_flag.context @> :searchContext', { searchContext: [context] }) + .andWhere('feature_flag.status = :status', { status: FEATURE_FLAG_STATUS.ENABLED }); + + // INCLUDE_ALL flags: inclusion segments are irrelevant — skip all inclusion joins + const includeAllQuery = addBaseConditions(addExclusionJoins(this.createQueryBuilder('feature_flag'))).andWhere( + 'feature_flag.filterMode = :includeAll', + { includeAll: FILTER_MODE.INCLUDE_ALL } + ); + + // EXCLUDE_ALL flags: both inclusion and exclusion segments are needed + const excludeAllQuery = addBaseConditions( + addInclusionJoins(addExclusionJoins(this.createQueryBuilder('feature_flag'))) + ).andWhere('feature_flag.filterMode = :excludeAll', { excludeAll: FILTER_MODE.EXCLUDE_ALL }); + + const [includeAllFlags, excludeAllFlags] = await Promise.all([ + includeAllQuery.getMany().catch((errorMsg: any) => { + throw repositoryError('FeatureFlagRepository', 'getFlagsFromContext', { context }, errorMsg); + }), + excludeAllQuery.getMany().catch((errorMsg: any) => { + throw repositoryError('FeatureFlagRepository', 'getFlagsFromContext', { context }, errorMsg); + }), + ]); + + return [...includeAllFlags, ...excludeAllFlags]; } public async validateUniqueKey(flagDTO: FeatureFlagValidation) { diff --git a/packages/backend/test/unit/repositories/FeatureFlagRepository.test.ts b/packages/backend/test/unit/repositories/FeatureFlagRepository.test.ts index eb91ce6572..5f810ea726 100644 --- a/packages/backend/test/unit/repositories/FeatureFlagRepository.test.ts +++ b/packages/backend/test/unit/repositories/FeatureFlagRepository.test.ts @@ -176,4 +176,89 @@ describe('FeatureFlagRepository Testing', () => { expect(mock.returning).toHaveBeenCalledWith('*'); expect(mock.execute).toHaveBeenCalledTimes(1); }); + + describe('getFlagsFromContext', () => { + const context = 'test-context'; + + beforeEach(() => { + mock.getMany.mockResolvedValue([]); + }); + + it('should run two separate queries in parallel, one per filterMode', async () => { + await repo.getFlagsFromContext(context); + + expect(repo.createQueryBuilder).toHaveBeenCalledTimes(2); + expect(mock.getMany).toHaveBeenCalledTimes(2); + expect(mock.andWhere).toHaveBeenCalledWith('feature_flag.filterMode = :includeAll', { + includeAll: FILTER_MODE.INCLUDE_ALL, + }); + expect(mock.andWhere).toHaveBeenCalledWith('feature_flag.filterMode = :excludeAll', { + excludeAll: FILTER_MODE.EXCLUDE_ALL, + }); + }); + + it('should apply context and status conditions to both queries', async () => { + await repo.getFlagsFromContext(context); + + expect(mock.where).toHaveBeenCalledTimes(2); + expect(mock.where).toHaveBeenCalledWith('feature_flag.context @> :searchContext', { + searchContext: [context], + }); + expect(mock.andWhere).toHaveBeenCalledWith('feature_flag.status = :status', { + status: FEATURE_FLAG_STATUS.ENABLED, + }); + }); + + it('should not join inclusion segment member data for INCLUDE_ALL flags', async () => { + await repo.getFlagsFromContext(context); + + // Collect all first-args passed to leftJoinAndSelect across both queries + const joinedRelations = mock.leftJoinAndSelect.mock.calls.map(([relation]) => relation); + + // Inclusion-side member joins should appear exactly once (only from the EXCLUDE_ALL query) + expect(joinedRelations.filter((r) => r === 'segmentInclusion.individualForSegment')).toHaveLength(1); + expect(joinedRelations.filter((r) => r === 'segmentInclusion.groupForSegment')).toHaveLength(1); + expect(joinedRelations.filter((r) => r === 'segmentInclusion.subSegments')).toHaveLength(1); + + // Exclusion-side member joins should appear twice (once per query) + expect(joinedRelations.filter((r) => r === 'segmentExclusion.individualForSegment')).toHaveLength(2); + expect(joinedRelations.filter((r) => r === 'segmentExclusion.groupForSegment')).toHaveLength(2); + expect(joinedRelations.filter((r) => r === 'segmentExclusion.subSegments')).toHaveLength(2); + }); + + it('should join inclusion segment member data for EXCLUDE_ALL flags', async () => { + await repo.getFlagsFromContext(context); + + expect(mock.leftJoinAndSelect).toHaveBeenCalledWith( + 'feature_flag.featureFlagSegmentInclusion', + 'featureFlagSegmentInclusion' + ); + expect(mock.leftJoinAndSelect).toHaveBeenCalledWith('featureFlagSegmentInclusion.segment', 'segmentInclusion'); + expect(mock.leftJoinAndSelect).toHaveBeenCalledWith( + 'segmentInclusion.individualForSegment', + 'individualForSegment' + ); + expect(mock.leftJoinAndSelect).toHaveBeenCalledWith('segmentInclusion.groupForSegment', 'groupForSegment'); + expect(mock.leftJoinAndSelect).toHaveBeenCalledWith('segmentInclusion.subSegments', 'subSegment'); + }); + + it('should combine results from both queries', async () => { + const includeAllFlag = new FeatureFlag(); + includeAllFlag.id = 'include-all-flag'; + const excludeAllFlag = new FeatureFlag(); + excludeAllFlag.id = 'exclude-all-flag'; + + mock.getMany.mockResolvedValueOnce([includeAllFlag]).mockResolvedValueOnce([excludeAllFlag]); + + const results = await repo.getFlagsFromContext(context); + + expect(results).toEqual([includeAllFlag, excludeAllFlag]); + }); + + it('should throw when either query fails', async () => { + mock.getMany.mockRejectedValueOnce(err); + + await expect(repo.getFlagsFromContext(context)).rejects.toThrow(); + }); + }); });