diff --git a/src/lib/db/feature-toggle-admin-store.ts b/src/lib/db/feature-toggle-admin-store.ts index a50cef0999..c85d604e6b 100644 --- a/src/lib/db/feature-toggle-admin-store.ts +++ b/src/lib/db/feature-toggle-admin-store.ts @@ -2,17 +2,27 @@ import { Knex } from 'knex'; import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { Logger, LogProvider } from '../logger'; -import { FeatureToggle, IFeatureToggleQuery } from '../types/model'; +import { + FeatureToggle, + IFeatureToggleClient, + IFeatureToggleQuery, + IStrategyConfig, + ITag, +} from '../types/model'; import { DEFAULT_ENV } from '../util/constants'; import { PartialDeep } from '../types/partial'; import EventEmitter from 'events'; import FeatureToggleStore from './feature-toggle-store'; +import { ensureStringValue } from '../util/ensureStringValue'; +import { mapValues } from '../util/map-values'; import { Db } from './db'; +import { IFeatureToggleAdminStore } from '../types/stores/feature-toggle-admin-store'; import Raw = Knex.Raw; export interface IGetAllFeatures { featureQuery?: IFeatureToggleQuery; archived: boolean; + includeStrategyIds?: boolean; userId?: number; } @@ -22,9 +32,9 @@ export interface IGetAdminFeatures { userId?: number; } -// This is extracted from the feature-toggle-client-store that was mixing -// client and admin concerns -export default class FeatureToggleAdminStore { +export default class FeatureToggleAdminStore + implements IFeatureToggleAdminStore +{ private db: Db; private logger: Logger; @@ -36,7 +46,7 @@ export default class FeatureToggleAdminStore { this.logger = getLogger('feature-toggle-admin-store.ts'); this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { - store: 'admin-feature-toggle', + store: 'feature-toggle', action, }); } @@ -45,7 +55,7 @@ export default class FeatureToggleAdminStore { featureQuery, archived, userId, - }: IGetAllFeatures): Promise { + }: IGetAllFeatures): Promise<(IFeatureToggleClient & FeatureToggle)[]> { const environment = featureQuery?.environment || DEFAULT_ENV; const stopTimer = this.timer('getFeatureAdmin'); @@ -152,9 +162,28 @@ export default class FeatureToggleAdminStore { stopTimer(); const featureToggles = rows.reduce((acc, r) => { - let feature: PartialDeep = acc[r.name] ?? {}; - + let feature: PartialDeep = + acc[r.name] ?? { + strategies: [], + }; + if (this.isUnseenStrategyRow(feature, r)) { + feature.strategies.push( + FeatureToggleAdminStore.rowToStrategy(r), + ); + } + if (this.isNewTag(feature, r)) { + this.addTag(feature, r); + } + if (featureQuery?.inlineSegmentConstraints && r.segment_id) { + this.addSegmentToStrategy(feature, r); + } else if ( + !featureQuery?.inlineSegmentConstraints && + r.segment_id + ) { + this.addSegmentIdsToStrategy(feature, r); + } feature.impressionData = r.impression_data; + feature.enabled = !!r.enabled; feature.name = r.name; feature.description = r.description; feature.project = r.project; @@ -169,16 +198,90 @@ export default class FeatureToggleAdminStore { return acc; }, {}); - const features: FeatureToggle[] = Object.values(featureToggles); + const features: IFeatureToggleClient[] = Object.values(featureToggles); return features; } + private static rowToStrategy(row: Record): IStrategyConfig { + return { + id: row.strategy_id, + name: row.strategy_name, + constraints: row.constraints || [], + parameters: mapValues(row.parameters || {}, ensureStringValue), + }; + } + + private static rowToTag(row: Record): ITag { + return { + value: row.tag_value, + type: row.tag_type, + }; + } + + private isUnseenStrategyRow( + feature: PartialDeep, + row: Record, + ): boolean { + return ( + row.strategy_id && + !feature.strategies.find((s) => s.id === row.strategy_id) + ); + } + + private addTag( + feature: Record, + row: Record, + ): void { + const tags = feature.tags || []; + const newTag = FeatureToggleAdminStore.rowToTag(row); + feature.tags = [...tags, newTag]; + } + + private 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, + ) + ); + } + + private addSegmentToStrategy( + feature: PartialDeep, + row: Record, + ) { + feature.strategies + .find((s) => s.id === row.strategy_id) + ?.constraints.push(...row.segment_constraints); + } + + private 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); + } + async getAdmin({ featureQuery, userId, archived, - }: IGetAdminFeatures): Promise { + }: IGetAdminFeatures): Promise { return this.getAll({ featureQuery, archived, userId }); } }