From b6d945befce825519df4daaaeec9f60be02ece13 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 13 Oct 2023 12:29:14 +0200 Subject: [PATCH] refactor: feature toggle list query (#5022) Cean up huge query method in feature-toggle-store --- .../feature-toggle/feature-toggle-store.ts | 169 +++++++----------- .../feature-toggle-list-builder.ts | 129 +++++++++++++ 2 files changed, 190 insertions(+), 108 deletions(-) create mode 100644 src/lib/features/feature-toggle/query-builders/feature-toggle-list-builder.ts diff --git a/src/lib/features/feature-toggle/feature-toggle-store.ts b/src/lib/features/feature-toggle/feature-toggle-store.ts index c8cd967a3bee..2b25ccccf5af 100644 --- a/src/lib/features/feature-toggle/feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-store.ts @@ -21,6 +21,7 @@ import { ITag, PartialDeep, } from '../../../lib/types'; +import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder'; export type EnvironmentFeatureNames = { [key: string]: string[] }; @@ -131,6 +132,48 @@ const rowToTag = (row: Record): ITag => { }; }; +const buildFeatureToggleListFromRows = ( + rows: any[], + featureQuery?: IFeatureToggleQuery, +): FeatureToggle[] => { + const result = rows.reduce((acc, r) => { + const feature: PartialDeep = acc[r.name] ?? { + strategies: [], + }; + if (isUnseenStrategyRow(feature, r) && !r.strategy_disabled) { + feature.strategies?.push(rowToStrategy(r)); + } + if (isNewTag(feature, r)) { + addTag(feature, r); + } + if (featureQuery?.inlineSegmentConstraints && r.segment_id) { + addSegmentToStrategy(feature, r); + } else if (!featureQuery?.inlineSegmentConstraints && r.segment_id) { + addSegmentIdsToStrategy(feature, r); + } + + feature.impressionData = r.impression_data; + feature.enabled = !!r.enabled; + feature.name = r.name; + feature.description = r.description; + feature.project = r.project; + feature.stale = r.stale; + feature.type = r.type; + feature.lastSeenAt = r.last_seen_at; + feature.variants = r.variants || []; + feature.project = r.project; + + feature.favorite = r.favorite; + feature.lastSeenAt = r.last_seen_at; + feature.createdAt = r.created_at; + + acc[r.name] = feature; + return acc; + }, {}); + + return Object.values(result); +}; + export default class FeatureToggleStore implements IFeatureToggleStore { private db: Db; @@ -183,125 +226,35 @@ export default class FeatureToggleStore implements IFeatureToggleStore { userId?: number, archived: boolean = false, ): Promise { - // Handle the admin case first - // Handle the playground case - const environment = featureQuery?.environment || DEFAULT_ENV; - const selectColumns = [ - 'features.name as name', - 'features.description as description', - 'features.type as type', - 'features.project as project', - 'features.stale as stale', - 'features.impression_data as impression_data', - 'features.last_seen_at as last_seen_at', - 'features.created_at as created_at', - 'fe.variants as variants', - 'fe.enabled as enabled', - 'fe.environment as environment', - 'fs.id as strategy_id', - 'fs.strategy_name as strategy_name', - 'fs.title as strategy_title', - 'fs.disabled as strategy_disabled', - 'fs.parameters as parameters', - 'fs.constraints as constraints', - 'fs.sort_order as sort_order', - 'fs.variants as strategy_variants', - 'segments.id as segment_id', - 'segments.constraints as segment_constraints', - ] as (string | Knex.Raw)[]; - - let query = this.db('features') - .modify(FeatureToggleStore.filterByArchived, archived) - .leftJoin( - this.db('feature_strategies') - .select('*') - .where({ environment }) - .as('fs'), - 'fs.feature_name', - 'features.name', - ) - .leftJoin( - this.db('feature_environments') - .select( - 'feature_name', - 'enabled', - 'environment', - 'variants', - 'last_seen_at', - ) - .where({ environment }) - .as('fe'), - 'fe.feature_name', - 'features.name', - ) - .leftJoin( - 'feature_strategy_segment as fss', - `fss.feature_strategy_id`, - `fs.id`, - ) - .leftJoin('segments', `segments.id`, `fss.segment_id`) - .leftJoin('dependent_features as df', 'df.child', 'features.name') - .leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); + const builder = new FeatureToggleListBuilder(this.db); + + builder + .query('features') + .withArchived(archived) + .withStrategies(environment) + .withFeatureEnvironments(environment) + .withFeatureStrategySegments() + .withSegments() + .withDependentFeatureToggles() + .withFeatureTags(); if (userId) { - query = query.leftJoin(`favorite_features`, function () { - this.on('favorite_features.feature', 'features.name').andOnVal( - 'favorite_features.user_id', - '=', - userId, - ); - }); - selectColumns.push( + builder.withFavorites(userId); + + builder.addSelectColumn( this.db.raw( 'favorite_features.feature is not null as favorite', ), ); } - query = query.select(selectColumns); - const rows = await query; - - const featureToggles = rows.reduce((acc, r) => { - const feature: PartialDeep = acc[r.name] ?? { - strategies: [], - }; - if (isUnseenStrategyRow(feature, r) && !r.strategy_disabled) { - feature.strategies?.push(rowToStrategy(r)); - } - if (isNewTag(feature, r)) { - addTag(feature, r); - } - if (featureQuery?.inlineSegmentConstraints && r.segment_id) { - addSegmentToStrategy(feature, r); - } else if ( - !featureQuery?.inlineSegmentConstraints && - r.segment_id - ) { - addSegmentIdsToStrategy(feature, r); - } + const rows = await builder.internalQuery.select( + builder.getSelectColumns(), + ); - feature.impressionData = r.impression_data; - feature.enabled = !!r.enabled; - feature.name = r.name; - feature.description = r.description; - feature.project = r.project; - feature.stale = r.stale; - feature.type = r.type; - feature.lastSeenAt = r.last_seen_at; - feature.variants = r.variants || []; - feature.project = r.project; - - feature.favorite = r.favorite; - feature.lastSeenAt = r.last_seen_at; - feature.createdAt = r.created_at; - - acc[r.name] = feature; - return acc; - }, {}); - - return Object.values(featureToggles); + return buildFeatureToggleListFromRows(rows, featureQuery); } async getAll( diff --git a/src/lib/features/feature-toggle/query-builders/feature-toggle-list-builder.ts b/src/lib/features/feature-toggle/query-builders/feature-toggle-list-builder.ts new file mode 100644 index 000000000000..99682205b1fa --- /dev/null +++ b/src/lib/features/feature-toggle/query-builders/feature-toggle-list-builder.ts @@ -0,0 +1,129 @@ +import { Knex } from "knex"; +import FeatureToggleStore from "../feature-toggle-store"; + +export class FeatureToggleListBuilder { + private db: Knex; + + public internalQuery: Knex.QueryBuilder; + + private selectColumns: (string | Knex.Raw)[]; + + constructor(db) { + this.db = db; + this.selectColumns = [ + 'features.name as name', + 'features.description as description', + 'features.type as type', + 'features.project as project', + 'features.stale as stale', + 'features.impression_data as impression_data', + 'features.last_seen_at as last_seen_at', + 'features.created_at as created_at', + 'fe.variants as variants', + 'fe.enabled as enabled', + 'fe.environment as environment', + 'fs.id as strategy_id', + 'fs.strategy_name as strategy_name', + 'fs.title as strategy_title', + 'fs.disabled as strategy_disabled', + 'fs.parameters as parameters', + 'fs.constraints as constraints', + 'fs.sort_order as sort_order', + 'fs.variants as strategy_variants', + 'segments.id as segment_id', + 'segments.constraints as segment_constraints', + ] as (string | Knex.Raw)[]; + } + + getSelectColumns = () => { + return this.selectColumns; + } + + query = (table: string) => { + this.internalQuery = this.db(table); + + return this; + } + + addSelectColumn = (column: string | Knex.Raw) => { + this.selectColumns.push(column); + } + + withArchived = (includeArchived: boolean) => { + this.internalQuery.modify(FeatureToggleStore.filterByArchived, includeArchived) + + return this; + } + + withStrategies = (filter: string) => { + this.internalQuery.leftJoin( + this.db('feature_strategies') + .select('*') + .where({ environment: filter }) + .as('fs'), + 'fs.feature_name', + 'features.name', + ) + + return this; + } + + withFeatureEnvironments = (filter: string) => { + this.internalQuery.leftJoin( + this.db('feature_environments') + .select( + 'feature_name', + 'enabled', + 'environment', + 'variants', + 'last_seen_at', + ) + .where({ environment: filter }) + .as('fe'), + 'fe.feature_name', + 'features.name', + ) + + return this; + } + + withFeatureStrategySegments = () => { + this.internalQuery.leftJoin( + 'feature_strategy_segment as fss', + `fss.feature_strategy_id`, + `fs.id`, + ) + + return this; + } + + withSegments = () => { + this.internalQuery.leftJoin('segments', `segments.id`, `fss.segment_id`) + + return this; + } + + withDependentFeatureToggles = () => { + this.internalQuery.leftJoin('dependent_features as df', 'df.child', 'features.name') + + return this; + } + + withFeatureTags = () => { + this.internalQuery.leftJoin('feature_tag as ft', 'ft.feature_name', 'features.name'); + + return this; + } + + withFavorites = (userId: number) => { + this.internalQuery.leftJoin(`favorite_features`, function () { + this.on('favorite_features.feature', 'features.name').andOnVal( + 'favorite_features.user_id', + '=', + userId, + ); + }); + + return this; + } +} \ No newline at end of file