diff --git a/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts b/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts new file mode 100644 index 000000000000..354b049d8861 --- /dev/null +++ b/src/lib/features/feature-toggle/converters/feature-toggle-row-converter.ts @@ -0,0 +1,191 @@ +import { + PartialDeep, + IFeatureToggleClient, + IStrategyConfig, + FeatureToggle, + IFeatureToggleQuery, + ITag, +} from '../../../types'; + +import { mapValues, ensureStringValue } from '../../../util'; +import { FeatureConfigurationClient } from '../types/feature-toggle-strategies-store-type'; + +export class FeatureToggleRowConverter { + isUnseenStrategyRow = ( + feature: PartialDeep, + row: Record, + ): boolean => { + return ( + row.strategy_id && + !feature.strategies?.find( + (strategy) => strategy?.id === row.strategy_id, + ) + ); + }; + + isNewTag = ( + feature: PartialDeep, + row: Record, + ): boolean => { + return ( + row.tag_type && + row.tag_value && + !feature.tags?.some( + (tag) => + tag?.type === row.tag_type && tag?.value === row.tag_value, + ) + ); + }; + + addSegmentToStrategy = ( + feature: PartialDeep, + row: Record, + ) => { + feature.strategies + ?.find((strategy) => strategy?.id === row.strategy_id) + ?.constraints?.push(...row.segment_constraints); + }; + + addSegmentIdsToStrategy = ( + feature: PartialDeep, + row: Record, + ) => { + const strategy = feature.strategies?.find( + (strategy) => strategy?.id === row.strategy_id, + ); + if (!strategy) { + return; + } + if (!strategy.segments) { + strategy.segments = []; + } + strategy.segments.push(row.segment_id); + }; + + rowToStrategy = (row: Record): IStrategyConfig => { + const strategy: IStrategyConfig = { + id: row.strategy_id, + name: row.strategy_name, + title: row.strategy_title, + constraints: row.constraints || [], + parameters: mapValues(row.parameters || {}, ensureStringValue), + sortOrder: row.sort_order, + }; + strategy.variants = row.strategy_variants || []; + return strategy; + }; + + addTag = (feature: Record, row: Record): void => { + const tags = feature.tags || []; + const newTag = this.rowToTag(row); + feature.tags = [...tags, newTag]; + }; + + rowToTag = (row: Record): ITag => { + return { + value: row.tag_value, + type: row.tag_type, + }; + }; + + formatToggles = (result: IFeatureToggleQuery) => + Object.values(result).map(({ strategies, ...rest }) => ({ + ...rest, + strategies: strategies + ?.sort((strategy1, strategy2) => { + if ( + typeof strategy1.sortOrder === 'number' && + typeof strategy2.sortOrder === 'number' + ) { + return strategy1.sortOrder - strategy2.sortOrder; + } + return 0; + }) + .map(({ title, sortOrder, ...strategy }) => ({ + ...strategy, + ...(title ? { title } : {}), + })), + })); + + createBaseFeature = ( + row: any, + feature: PartialDeep, + featureQuery?: IFeatureToggleQuery, + ) => { + feature.impressionData = row.impression_data; + feature.enabled = !!row.enabled; + feature.name = row.name; + feature.description = row.description; + feature.project = row.project; + feature.stale = row.stale; + feature.type = row.type; + feature.lastSeenAt = row.last_seen_at; + feature.variants = row.variants || []; + feature.project = row.project; + + if (this.isUnseenStrategyRow(feature, row) && !row.strategy_disabled) { + feature.strategies?.push(this.rowToStrategy(row)); + } + if (this.isNewTag(feature, row)) { + this.addTag(feature, row); + } + if (featureQuery?.inlineSegmentConstraints && row.segment_id) { + this.addSegmentToStrategy(feature, row); + } else if (!featureQuery?.inlineSegmentConstraints && row.segment_id) { + this.addSegmentIdsToStrategy(feature, row); + } + + return feature; + }; + + buildFeatureToggleListFromRows = ( + rows: any[], + featureQuery?: IFeatureToggleQuery, + ): FeatureToggle[] => { + const result = rows.reduce((acc, r) => { + let feature: PartialDeep = acc[r.name] ?? { + strategies: [], + }; + + feature = this.createBaseFeature(r, feature, featureQuery); + + feature.createdAt = r.created_at; + feature.favorite = r.favorite; + + acc[r.name] = feature; + return acc; + }, {}); + + return this.formatToggles(result); + }; + + buildPlaygroundFeaturesFromRows = ( + rows: any[], + dependentFeaturesEnabled: boolean, + featureQuery?: IFeatureToggleQuery, + ): FeatureConfigurationClient[] => { + const result = rows.reduce((acc, r) => { + let feature: PartialDeep = acc[r.name] ?? { + strategies: [], + }; + + feature = this.createBaseFeature(r, feature, featureQuery); + + if (r.parent && dependentFeaturesEnabled) { + feature.dependencies = feature.dependencies || []; + feature.dependencies.push({ + feature: r.parent, + enabled: r.parent_enabled, + ...(r.parent_enabled + ? { variants: r.parent_variants } + : {}), + }); + } + + acc[r.name] = feature; + return acc; + }, {}); + + return this.formatToggles(result); + }; +} diff --git a/src/lib/features/feature-toggle/feature-toggle-store.ts b/src/lib/features/feature-toggle/feature-toggle-store.ts index 4b80522001c9..97da22b60336 100644 --- a/src/lib/features/feature-toggle/feature-toggle-store.ts +++ b/src/lib/features/feature-toggle/feature-toggle-store.ts @@ -17,12 +17,9 @@ import { NameExistsError } from '../../error'; import { DEFAULT_ENV } from '../../../lib/util'; import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder'; -import { - buildFeatureToggleListFromRows, - buildPlaygroundFeaturesFromRows, -} from './feature-toggle-utils'; import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type'; import { IFlagResolver } from '../../../lib/types'; +import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter'; export type EnvironmentFeatureNames = { [key: string]: string[] }; @@ -65,9 +62,12 @@ export default class FeatureToggleStore implements IFeatureToggleStore { private timer: Function; + private featureToggleRowConverter: FeatureToggleRowConverter; + constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('feature-toggle-store.ts'); + this.featureToggleRowConverter = new FeatureToggleRowConverter(); this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'feature-toggle', @@ -147,7 +147,10 @@ export default class FeatureToggleStore implements IFeatureToggleStore { builder.getSelectColumns(), ); - return buildFeatureToggleListFromRows(rows, featureQuery); + return this.featureToggleRowConverter.buildFeatureToggleListFromRows( + rows, + featureQuery, + ); } async getPlaygroundFeatures( @@ -171,7 +174,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { builder.getSelectColumns(), ); - return buildPlaygroundFeaturesFromRows( + return this.featureToggleRowConverter.buildPlaygroundFeaturesFromRows( rows, dependentFeaturesEnabled, featureQuery, diff --git a/src/lib/features/feature-toggle/feature-toggle-utils.ts b/src/lib/features/feature-toggle/feature-toggle-utils.ts deleted file mode 100644 index df603090dcdf..000000000000 --- a/src/lib/features/feature-toggle/feature-toggle-utils.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { - PartialDeep, - IFeatureToggleClient, - IStrategyConfig, - ITag, - IFeatureToggleQuery, - FeatureToggle, -} from '../../types'; -import { mapValues, ensureStringValue } from '../../util'; -import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type'; - -const isUnseenStrategyRow = ( - feature: PartialDeep, - row: Record, -): boolean => { - return ( - row.strategy_id && - !feature.strategies?.find((s) => s?.id === row.strategy_id) - ); -}; - -const isNewTag = ( - feature: PartialDeep, - row: Record, -): boolean => { - return ( - row.tag_type && - row.tag_value && - !feature.tags?.some( - (tag) => tag?.type === row.tag_type && tag?.value === row.tag_value, - ) - ); -}; - -const addSegmentToStrategy = ( - feature: PartialDeep, - row: Record, -) => { - feature.strategies - ?.find((s) => s?.id === row.strategy_id) - ?.constraints?.push(...row.segment_constraints); -}; - -const addSegmentIdsToStrategy = ( - feature: PartialDeep, - row: Record, -) => { - const strategy = feature.strategies?.find((s) => s?.id === row.strategy_id); - if (!strategy) { - return; - } - if (!strategy.segments) { - strategy.segments = []; - } - strategy.segments.push(row.segment_id); -}; - -const rowToStrategy = (row: Record): IStrategyConfig => { - const strategy: IStrategyConfig = { - id: row.strategy_id, - name: row.strategy_name, - title: row.strategy_title, - constraints: row.constraints || [], - parameters: mapValues(row.parameters || {}, ensureStringValue), - sortOrder: row.sort_order, - }; - strategy.variants = row.strategy_variants || []; - return strategy; -}; - -const addTag = ( - feature: Record, - row: Record, -): void => { - const tags = feature.tags || []; - const newTag = rowToTag(row); - feature.tags = [...tags, newTag]; -}; - -const rowToTag = (row: Record): ITag => { - return { - value: row.tag_value, - type: row.tag_type, - }; -}; - -export const buildFeatureToggleListFromRows = ( - rows: any[], - featureQuery?: IFeatureToggleQuery, -): FeatureToggle[] => { - let 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.createdAt = r.created_at; - feature.favorite = r.favorite; - - feature.lastSeenAt = r.last_seen_at; - - acc[r.name] = feature; - return acc; - }, {}); - - result = Object.values(result).map(({ strategies, ...rest }) => ({ - ...rest, - strategies: strategies - ?.sort((strategy1, strategy2) => { - if ( - typeof strategy1.sortOrder === 'number' && - typeof strategy2.sortOrder === 'number' - ) { - return strategy1.sortOrder - strategy2.sortOrder; - } - return 0; - }) - .map(({ title, sortOrder, ...strategy }) => ({ - ...strategy, - ...(title ? { title } : {}), - })), - })); - - return result; -}; - -export const buildPlaygroundFeaturesFromRows = ( - rows: any[], - dependentFeaturesEnabled: boolean, - featureQuery?: IFeatureToggleQuery, -): FeatureConfigurationClient[] => { - let 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.lastSeenAt = r.last_seen_at; - - if (r.parent && dependentFeaturesEnabled) { - feature.dependencies = feature.dependencies || []; - feature.dependencies.push({ - feature: r.parent, - enabled: r.parent_enabled, - ...(r.parent_enabled ? { variants: r.parent_variants } : {}), - }); - } - - acc[r.name] = feature; - return acc; - }, {}); - - result = Object.values(result).map(({ strategies, ...rest }) => ({ - ...rest, - strategies: strategies - ?.sort((strategy1, strategy2) => { - if ( - typeof strategy1.sortOrder === 'number' && - typeof strategy2.sortOrder === 'number' - ) { - return strategy1.sortOrder - strategy2.sortOrder; - } - return 0; - }) - .map(({ title, sortOrder, ...strategy }) => ({ - ...strategy, - ...(title ? { title } : {}), - })), - })); - - return result; -}; - -interface Difference { - index: (string | number)[]; - reason: string; - valueA: any; - valueB: any; -}