From 8ff4595e77aea4307f42a0f375f0419606f775c5 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Wed, 8 Mar 2023 15:27:01 +0100 Subject: [PATCH] refactor: separate client store and legacy admin store --- src/lib/db/feature-toggle-client-store.ts | 68 +---- .../db/feature-toggle-legacy-admin-store.ts | 283 ++++++++++++++++++ src/lib/db/index.ts | 6 + src/lib/services/feature-toggle-service.ts | 8 +- src/lib/types/stores.ts | 2 + .../stores/feature-toggle-client-store.ts | 4 - .../feature-toggle-legacy-admin-store.ts | 7 + .../fake-feature-toggle-client-store.ts | 3 +- 8 files changed, 310 insertions(+), 71 deletions(-) create mode 100644 src/lib/db/feature-toggle-legacy-admin-store.ts create mode 100644 src/lib/types/stores/feature-toggle-legacy-admin-store.ts diff --git a/src/lib/db/feature-toggle-client-store.ts b/src/lib/db/feature-toggle-client-store.ts index 97c4c8e52c..0bf505423d 100644 --- a/src/lib/db/feature-toggle-client-store.ts +++ b/src/lib/db/feature-toggle-client-store.ts @@ -8,33 +8,19 @@ import { IStrategyConfig, ITag, } from '../types/model'; -import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; 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 Raw = Knex.Raw; import { Db } from './db'; - -export interface FeaturesTable { - name: string; - description: string; - type: string; - stale: boolean; - variants: string; - project: string; - last_seen_at?: Date; - created_at?: Date; -} +import Raw = Knex.Raw; export interface IGetAllFeatures { featureQuery?: IFeatureToggleQuery; archived: boolean; - isAdmin: boolean; includeStrategyIds?: boolean; - userId?: number; } export interface IGetAdminFeatures { @@ -43,9 +29,7 @@ export interface IGetAdminFeatures { userId?: number; } -export default class FeatureToggleClientStore - implements IFeatureToggleClientStore -{ +export default class FeatureToggleClientStore { private db: Db; private logger: Logger; @@ -65,9 +49,7 @@ export default class FeatureToggleClientStore private async getAll({ featureQuery, archived, - isAdmin, includeStrategyIds, - userId, }: IGetAllFeatures): Promise { const environment = featureQuery?.environment || DEFAULT_ENV; const stopTimer = this.timer('getFeatureAdmin'); @@ -122,34 +104,6 @@ export default class FeatureToggleClientStore ) .leftJoin('segments', `segments.id`, `fss.segment_id`); - if (isAdmin) { - query = query.leftJoin( - 'feature_tag as ft', - 'ft.feature_name', - 'features.name', - ); - selectColumns = [ - ...selectColumns, - 'ft.tag_value as tag_value', - 'ft.tag_type as tag_type', - ]; - - if (userId) { - query = query.leftJoin(`favorite_features`, function () { - this.on( - 'favorite_features.feature', - 'features.name', - ).andOnVal('favorite_features.user_id', '=', userId); - }); - selectColumns = [ - ...selectColumns, - this.db.raw( - 'favorite_features.feature is not null as favorite', - ), - ]; - } - } - query = query.select(selectColumns); if (featureQuery) { @@ -204,18 +158,13 @@ export default class FeatureToggleClientStore feature.type = r.type; feature.variants = r.variants || []; feature.project = r.project; - if (isAdmin) { - feature.favorite = r.favorite; - feature.lastSeenAt = r.last_seen_at; - feature.createdAt = r.created_at; - } acc[r.name] = feature; return acc; }, {}); const features: IFeatureToggleClient[] = Object.values(featureToggles); - if (!isAdmin && !includeStrategyIds) { + if (!includeStrategyIds) { // We should not send strategy IDs from the client API, // as this breaks old versions of the Go SDK (at least). FeatureToggleClientStore.removeIdsFromStrategies(features); @@ -313,18 +262,7 @@ export default class FeatureToggleClientStore return this.getAll({ featureQuery, archived: false, - isAdmin: false, includeStrategyIds, }); } - - async getAdmin({ - featureQuery, - userId, - archived, - }: IGetAdminFeatures): Promise { - return this.getAll({ featureQuery, archived, isAdmin: true, userId }); - } } - -module.exports = FeatureToggleClientStore; diff --git a/src/lib/db/feature-toggle-legacy-admin-store.ts b/src/lib/db/feature-toggle-legacy-admin-store.ts new file mode 100644 index 0000000000..fe15e274a3 --- /dev/null +++ b/src/lib/db/feature-toggle-legacy-admin-store.ts @@ -0,0 +1,283 @@ +import { Knex } from 'knex'; +import metricsHelper from '../util/metrics-helper'; +import { DB_TIME } from '../metric-events'; +import { Logger, LogProvider } from '../logger'; +import { + 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 Raw = Knex.Raw; + +export interface IGetAllFeatures { + featureQuery?: IFeatureToggleQuery; + archived: boolean; + userId?: number; +} + +export interface IGetAdminFeatures { + featureQuery?: IFeatureToggleQuery; + archived?: boolean; + userId?: number; +} + +// This is extracted from the feature-toggle-client-store that was mixing +// client and admin concerns +export default class FeatureToggleLegacyAdminStore { + private db: Db; + + private logger: Logger; + + private timer: Function; + + constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('feature-toggle-legacy-admin-store.ts'); + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'admin-feature-toggle', + action, + }); + } + + private async getAll({ + featureQuery, + archived, + userId, + }: IGetAllFeatures): Promise { + const environment = featureQuery?.environment || DEFAULT_ENV; + const stopTimer = this.timer('getFeatureAdmin'); + + let 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', + 'fe.variants as variants', + 'features.created_at as created_at', + 'features.last_seen_at as last_seen_at', + 'fe.enabled as enabled', + 'fe.environment as environment', + 'fs.id as strategy_id', + 'fs.strategy_name as strategy_name', + 'fs.parameters as parameters', + 'fs.constraints as constraints', + 'segments.id as segment_id', + 'segments.constraints as segment_constraints', + ] as (string | 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', + ) + .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`); + + query = query.leftJoin( + 'feature_tag as ft', + 'ft.feature_name', + 'features.name', + ); + selectColumns = [ + ...selectColumns, + 'ft.tag_value as tag_value', + 'ft.tag_type as tag_type', + ]; + + if (userId) { + query = query.leftJoin(`favorite_features`, function () { + this.on('favorite_features.feature', 'features.name').andOnVal( + 'favorite_features.user_id', + '=', + userId, + ); + }); + selectColumns = [ + ...selectColumns, + this.db.raw( + 'favorite_features.feature is not null as favorite', + ), + ]; + } + + query = query.select(selectColumns); + + if (featureQuery) { + if (featureQuery.tag) { + const tagQuery = this.db + .from('feature_tag') + .select('feature_name') + .whereIn(['tag_type', 'tag_value'], featureQuery.tag); + query = query.whereIn('features.name', tagQuery); + } + if (featureQuery.project) { + query = query.whereIn('project', featureQuery.project); + } + if (featureQuery.namePrefix) { + query = query.where( + 'features.name', + 'like', + `${featureQuery.namePrefix}%`, + ); + } + } + + const rows = await query; + stopTimer(); + + const featureToggles = rows.reduce((acc, r) => { + let feature: PartialDeep = acc[r.name] ?? { + strategies: [], + }; + if (this.isUnseenStrategyRow(feature, r)) { + feature.strategies.push( + FeatureToggleLegacyAdminStore.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; + feature.stale = r.stale; + feature.type = r.type; + 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; + }, {}); + + 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 = FeatureToggleLegacyAdminStore.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 { + return this.getAll({ featureQuery, archived, userId }); + } +} diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 0baefaad43..f2fcfa5531 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -37,6 +37,7 @@ import { AccountStore } from './account-store'; import ProjectStatsStore from './project-stats-store'; import { Db } from './db'; import { ImportTogglesStore } from '../features/export-import-toggles/import-toggles-store'; +import FeatureToggleLegacyAdminStore from './feature-toggle-legacy-admin-store'; export const createStores = ( config: IUnleashConfig, @@ -86,6 +87,11 @@ export const createStores = ( eventBus, getLogger, ), + featureToggleLegacyAdminStore: new FeatureToggleLegacyAdminStore( + db, + eventBus, + getLogger, + ), environmentStore: new EnvironmentStore(db, eventBus, getLogger), featureTagStore: new FeatureTagStore(db, eventBus, getLogger), featureEnvironmentStore: new FeatureEnvironmentStore( diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index 19ab473746..562b9b2b8e 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -82,6 +82,7 @@ import { } from '../types/permissions'; import NoAccessError from '../error/no-access-error'; import { IFeatureProjectUserParams } from '../routes/admin-api/project/project-features'; +import { IFeatureToggleLegacyAdminStore } from '../types/stores/feature-toggle-legacy-admin-store'; interface IFeatureContext { featureName: string; @@ -113,6 +114,8 @@ class FeatureToggleService { private featureToggleClientStore: IFeatureToggleClientStore; + private featureToggleLegacyAdminStore: IFeatureToggleLegacyAdminStore; + private tagStore: IFeatureTagStore; private featureEnvironmentStore: IFeatureEnvironmentStore; @@ -134,6 +137,7 @@ class FeatureToggleService { featureStrategiesStore, featureToggleStore, featureToggleClientStore, + featureToggleLegacyAdminStore, projectStore, eventStore, featureTagStore, @@ -144,6 +148,7 @@ class FeatureToggleService { | 'featureStrategiesStore' | 'featureToggleStore' | 'featureToggleClientStore' + | 'featureToggleLegacyAdminStore' | 'projectStore' | 'eventStore' | 'featureTagStore' @@ -161,6 +166,7 @@ class FeatureToggleService { this.featureStrategiesStore = featureStrategiesStore; this.featureToggleStore = featureToggleStore; this.featureToggleClientStore = featureToggleClientStore; + this.featureToggleLegacyAdminStore = featureToggleLegacyAdminStore; this.tagStore = featureTagStore; this.projectStore = projectStore; this.eventStore = eventStore; @@ -704,7 +710,7 @@ class FeatureToggleService { userId?: number, archived: boolean = false, ): Promise { - return this.featureToggleClientStore.getAdmin({ + return this.featureToggleLegacyAdminStore.getAdmin({ featureQuery: query, userId, archived, diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 547e4e0921..1d71076e1d 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -33,6 +33,7 @@ import { IFavoriteProjectsStore } from './stores/favorite-projects'; import { IAccountStore } from './stores/account-store'; import { IProjectStatsStore } from './stores/project-stats-store-type'; import { IImportTogglesStore } from '../features/export-import-toggles/import-toggles-store-type'; +import { IFeatureToggleLegacyAdminStore } from './stores/feature-toggle-legacy-admin-store'; export interface IUnleashStores { accessStore: IAccessStore; @@ -50,6 +51,7 @@ export interface IUnleashStores { featureTagStore: IFeatureTagStore; featureToggleStore: IFeatureToggleStore; featureToggleClientStore: IFeatureToggleClientStore; + featureToggleLegacyAdminStore: IFeatureToggleLegacyAdminStore; featureTypeStore: IFeatureTypeStore; groupStore: IGroupStore; projectStore: IProjectStore; diff --git a/src/lib/types/stores/feature-toggle-client-store.ts b/src/lib/types/stores/feature-toggle-client-store.ts index 2902ca9d83..6b52895962 100644 --- a/src/lib/types/stores/feature-toggle-client-store.ts +++ b/src/lib/types/stores/feature-toggle-client-store.ts @@ -1,12 +1,8 @@ import { IFeatureToggleClient, IFeatureToggleQuery } from '../model'; -import { IGetAdminFeatures } from '../../db/feature-toggle-client-store'; export interface IFeatureToggleClientStore { getClient( featureQuery: Partial, includeStrategyIds?: boolean, ): Promise; - - // @Deprecated - getAdmin(params: IGetAdminFeatures): Promise; } diff --git a/src/lib/types/stores/feature-toggle-legacy-admin-store.ts b/src/lib/types/stores/feature-toggle-legacy-admin-store.ts new file mode 100644 index 0000000000..4127da81b5 --- /dev/null +++ b/src/lib/types/stores/feature-toggle-legacy-admin-store.ts @@ -0,0 +1,7 @@ +import { IFeatureToggleClient } from '../model'; +import { IGetAdminFeatures } from '../../db/feature-toggle-client-store'; + +export interface IFeatureToggleLegacyAdminStore { + // @Deprecated + getAdmin(params: IGetAdminFeatures): Promise; +} diff --git a/src/test/fixtures/fake-feature-toggle-client-store.ts b/src/test/fixtures/fake-feature-toggle-client-store.ts index d765b8bcfd..a21ae314c0 100644 --- a/src/test/fixtures/fake-feature-toggle-client-store.ts +++ b/src/test/fixtures/fake-feature-toggle-client-store.ts @@ -5,9 +5,10 @@ import { } from '../../lib/types/model'; import { IFeatureToggleClientStore } from '../../lib/types/stores/feature-toggle-client-store'; import { IGetAdminFeatures } from '../../lib/db/feature-toggle-client-store'; +import { IFeatureToggleLegacyAdminStore } from '../../lib/types/stores/feature-toggle-legacy-admin-store'; export default class FakeFeatureToggleClientStore - implements IFeatureToggleClientStore + implements IFeatureToggleClientStore, IFeatureToggleLegacyAdminStore { featureToggles: FeatureToggle[] = [];