From e876e6438d794fff348348b45fdb86db0dbd8e14 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Wed, 9 Apr 2025 08:54:19 +0200 Subject: [PATCH] feat: total count respect lifecycle filter (#9724) --- .../feature-search/feature-search-store.ts | 91 +++++++++---------- .../feature-search/feature.search.e2e.test.ts | 15 ++- 2 files changed, 56 insertions(+), 50 deletions(-) diff --git a/src/lib/features/feature-search/feature-search-store.ts b/src/lib/features/feature-search/feature-search-store.ts index 96344df9a3..4d0459b1ab 100644 --- a/src/lib/features/feature-search/feature-search-store.ts +++ b/src/lib/features/feature-search/feature-search-store.ts @@ -79,24 +79,6 @@ class FeatureSearchStore implements IFeatureSearchStore { }; } - private getLatestLifecycleStageQuery() { - return this.db('feature_lifecycles') - .select( - 'feature as stage_feature', - 'stage as latest_stage', - 'status as stage_status', - 'created_at as entered_stage_at', - ) - .distinctOn('stage_feature') - .orderBy([ - 'stage_feature', - { - column: 'entered_stage_at', - order: 'desc', - }, - ]); - } - async searchFeatures( { userId, @@ -147,6 +129,9 @@ class FeatureSearchStore implements IFeatureSearchStore { 'users.username as user_username', 'users.email as user_email', 'users.image_url as user_image_url', + 'lifecycle.latest_stage', + 'lifecycle.stage_status', + 'lifecycle.entered_stage_at', ] as (string | Raw | Knex.QueryBuilder)[]; const lastSeenQuery = 'last_seen_at_metrics.last_seen_at'; @@ -245,19 +230,48 @@ class FeatureSearchStore implements IFeatureSearchStore { 'users', 'users.id', 'features.created_by_user_id', + ) + .leftJoin('last_seen_at_metrics', function () { + this.on( + 'last_seen_at_metrics.environment', + '=', + 'environments.name', + ).andOn( + 'last_seen_at_metrics.feature_name', + '=', + 'features.name', + ); + }) + .leftJoin( + this.db + .select( + 'feature as stage_feature', + 'stage as latest_stage', + 'status as stage_status', + 'created_at as entered_stage_at', + ) + .from('feature_lifecycles') + .distinctOn('feature') + .orderBy([ + 'feature', + { column: 'created_at', order: 'desc' }, + ]) + .as('lifecycle'), + 'features.name', + 'lifecycle.stage_feature', ); - query.leftJoin('last_seen_at_metrics', function () { - this.on( - 'last_seen_at_metrics.environment', - '=', - 'environments.name', - ).andOn( - 'last_seen_at_metrics.feature_name', - '=', - 'features.name', - ); - }); + if (this.flagResolver.isEnabled('flagsOverviewSearch')) { + const parsedLifecycle = lifecycle + ? parseSearchOperatorValue( + 'lifecycle.latest_stage', + lifecycle, + ) + : null; + if (parsedLifecycle) { + applyGenericQueryParams(query, [parsedLifecycle]); + } + } const rankingSql = this.buildRankingSql( favoritesFirst, @@ -270,7 +284,6 @@ class FeatureSearchStore implements IFeatureSearchStore { .select(selectColumns) .denseRank('rank', this.db.raw(rankingSql)); }) - .with('lifecycle', this.getLatestLifecycleStageQuery()) .with( 'final_ranks', this.db.raw( @@ -321,26 +334,8 @@ class FeatureSearchStore implements IFeatureSearchStore { .joinRaw('CROSS JOIN total_features') .whereBetween('final_rank', [offset + 1, offset + limit]) .orderBy('final_rank'); - finalQuery - .select( - 'lifecycle.latest_stage', - 'lifecycle.stage_status', - 'lifecycle.entered_stage_at', - ) - .leftJoin( - 'lifecycle', - 'ranked_features.feature_name', - 'lifecycle.stage_feature', - ); if (this.flagResolver.isEnabled('flagsOverviewSearch')) { - const parsedLifecycle = lifecycle - ? parseSearchOperatorValue('lifecycle.latest_stage', lifecycle) - : null; - if (parsedLifecycle) { - applyGenericQueryParams(finalQuery, [parsedLifecycle]); - } - finalQuery .leftJoin( this.db('change_request_events AS cre') diff --git a/src/lib/features/feature-search/feature.search.e2e.test.ts b/src/lib/features/feature-search/feature.search.e2e.test.ts index f226da0b8c..d8194e0fc4 100644 --- a/src/lib/features/feature-search/feature.search.e2e.test.ts +++ b/src/lib/features/feature-search/feature.search.e2e.test.ts @@ -1111,6 +1111,10 @@ test('should return environment usage metrics and lifecycle', async () => { name: 'my_feature_b', createdAt: '2023-01-29T15:21:39.975Z', }); + await app.createFeature({ + name: 'my_feature_c', + createdAt: '2023-01-29T15:21:39.975Z', + }); await stores.clientMetricsStoreV2.batchInsertMetrics([ { @@ -1142,6 +1146,9 @@ test('should return environment usage metrics and lifecycle', async () => { await stores.featureLifecycleStore.insert([ { feature: 'my_feature_b', stage: 'initial' }, ]); + await stores.featureLifecycleStore.insert([ + { feature: 'my_feature_c', stage: 'initial' }, + ]); await stores.featureLifecycleStore.insert([ { feature: 'my_feature_b', stage: 'completed', status: 'discarded' }, ]); @@ -1150,6 +1157,7 @@ test('should return environment usage metrics and lifecycle', async () => { query: 'my_feature_b', }); expect(noExplicitLifecycle).toMatchObject({ + total: 1, features: [ { name: 'my_feature_b', @@ -1180,14 +1188,17 @@ test('should return environment usage metrics and lifecycle', async () => { query: 'my_feature_b', lifecycle: 'IS:initial', }); - expect(noFeaturesWithOtherLifecycle).toMatchObject({ features: [] }); + expect(noFeaturesWithOtherLifecycle).toMatchObject({ + total: 0, + features: [], + }); const { body: featureWithMatchingLifecycle } = await searchFeaturesWithLifecycle({ - query: 'my_feature_b', lifecycle: 'IS:completed', }); expect(featureWithMatchingLifecycle).toMatchObject({ + total: 1, features: [{ name: 'my_feature_b' }], }); });