mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: feature flag playground features in new store (#5013)
Have playground use the method in the feature-toggle-service instead of asking the client-feature-toggle-store
This commit is contained in:
		
							parent
							
								
									092ba2a625
								
							
						
					
					
						commit
						b58d900c2d
					
				| @ -15,7 +15,7 @@ import { | ||||
|     createFakeChangeRequestAccessService, | ||||
| } from '../change-request-access-service/createChangeRequestAccessReadModel'; | ||||
| import { FeaturesReadModel } from '../feature-toggle/features-read-model'; | ||||
| import { FakeFeaturesReadModel } from '../feature-toggle/fake-features-read-model'; | ||||
| import { FakeFeaturesReadModel } from '../feature-toggle/fakes/fake-features-read-model'; | ||||
| 
 | ||||
| export const createDependentFeaturesService = ( | ||||
|     db: Db, | ||||
|  | ||||
| @ -8,7 +8,7 @@ import { User } from '../../server-impl'; | ||||
| import { SKIP_CHANGE_REQUEST } from '../../types'; | ||||
| import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model'; | ||||
| import { extractUsernameFromUser } from '../../util'; | ||||
| import { IFeaturesReadModel } from '../feature-toggle/features-read-model-type'; | ||||
| import { IFeaturesReadModel } from '../feature-toggle/types/features-read-model-type'; | ||||
| 
 | ||||
| interface IDependentFeaturesServiceDeps { | ||||
|     dependentFeaturesStore: IDependentFeaturesStore; | ||||
|  | ||||
| @ -12,6 +12,7 @@ import { | ||||
| } from 'lib/types/model'; | ||||
| import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; | ||||
| import { EnvironmentFeatureNames } from '../feature-toggle-store'; | ||||
| import { FeatureConfigurationClient } from '../types/feature-toggle-strategies-store-type'; | ||||
| 
 | ||||
| export default class FakeFeatureToggleStore implements IFeatureToggleStore { | ||||
|     features: FeatureToggle[] = []; | ||||
| @ -159,7 +160,16 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore { | ||||
|         userId?: number, | ||||
|         archived: boolean = false, | ||||
|     ): Promise<FeatureToggle[]> { | ||||
|         return this.features.filter((f) => f.archived !== archived); | ||||
|         return this.features.filter((feature) => feature.archived !== archived); | ||||
|     } | ||||
| 
 | ||||
|     async getPlaygroundFeatures( | ||||
|         dependentFeaturesEnabled: boolean, | ||||
|         query?: IFeatureToggleQuery, | ||||
|     ): Promise<FeatureConfigurationClient[]> { | ||||
|         return this.features.filter( | ||||
|             (feature) => feature, | ||||
|         ) as FeatureConfigurationClient[]; | ||||
|     } | ||||
| 
 | ||||
|     async update( | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| import { IFeaturesReadModel } from './features-read-model-type'; | ||||
| import { IFeaturesReadModel } from '../types/features-read-model-type'; | ||||
| 
 | ||||
| export class FakeFeaturesReadModel implements IFeaturesReadModel { | ||||
|     featureExists(): Promise<boolean> { | ||||
| @ -27,6 +27,7 @@ import { | ||||
|     IFeatureOverview, | ||||
|     IFeatureStrategy, | ||||
|     IFeatureTagStore, | ||||
|     IFeatureToggleClient, | ||||
|     IFeatureToggleClientStore, | ||||
|     IFeatureToggleQuery, | ||||
|     IFeatureToggleStore, | ||||
| @ -101,6 +102,7 @@ import { IPrivateProjectChecker } from '../private-project/privateProjectChecker | ||||
| import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type'; | ||||
| import EventService from '../../services/event-service'; | ||||
| import { DependentFeaturesService } from '../dependent-features/dependent-features-service'; | ||||
| import isEqual from 'lodash.isequal'; | ||||
| 
 | ||||
| interface IFeatureContext { | ||||
|     featureName: string; | ||||
| @ -130,7 +132,6 @@ export type FeatureNameCheckResultWithFeaturePattern = | ||||
| const oneOf = (values: string[], match: string) => { | ||||
|     return values.some((value) => value === match); | ||||
| }; | ||||
| 
 | ||||
| class FeatureToggleService { | ||||
|     private logger: Logger; | ||||
| 
 | ||||
| @ -1049,10 +1050,32 @@ class FeatureToggleService { | ||||
|     async getPlaygroundFeatures( | ||||
|         query?: IFeatureToggleQuery, | ||||
|     ): Promise<FeatureConfigurationClient[]> { | ||||
|         const result = await this.clientFeatureToggleStore.getPlayground( | ||||
|             query || {}, | ||||
|         // Remove with with feature flag
 | ||||
|         const [featuresFromClientStore, featuresFromFeatureToggleStore] = | ||||
|             await Promise.all([ | ||||
|                 await this.clientFeatureToggleStore.getPlayground(query || {}), | ||||
|                 await this.featureToggleStore.getPlaygroundFeatures( | ||||
|                     this.flagResolver.isEnabled('dependentFeatures'), | ||||
|                     query, | ||||
|                 ), | ||||
|             ]); | ||||
| 
 | ||||
|         const equal = isEqual( | ||||
|             featuresFromClientStore, | ||||
|             featuresFromFeatureToggleStore, | ||||
|         ); | ||||
|         return result; | ||||
| 
 | ||||
|         if (!equal) { | ||||
|             this.logger.warn( | ||||
|                 'features from client-feature-toggle-store is not equal to features from feature-toggle-store', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const features = this.flagResolver.isEnabled('useLastSeenRefactor') | ||||
|             ? featuresFromFeatureToggleStore | ||||
|             : featuresFromClientStore; | ||||
| 
 | ||||
|         return features as FeatureConfigurationClient[]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -1068,20 +1091,36 @@ class FeatureToggleService { | ||||
|         userId?: number, | ||||
|         archived: boolean = false, | ||||
|     ): Promise<FeatureToggle[]> { | ||||
|         let features = (await this.clientFeatureToggleStore.getAdmin({ | ||||
|             featureQuery: query, | ||||
|             userId: userId, | ||||
|             archived: false, | ||||
|         })) as FeatureToggle[]; | ||||
|         // Remove with with feature flag
 | ||||
|         const [featuresFromClientStore, featuresFromFeatureToggleStore] = | ||||
|             await Promise.all([ | ||||
|                 (await this.clientFeatureToggleStore.getAdmin({ | ||||
|                     featureQuery: query, | ||||
|                     userId: userId, | ||||
|                     archived: false, | ||||
|                 })) as FeatureToggle[], | ||||
|                 await this.featureToggleStore.getFeatureToggleList( | ||||
|                     query, | ||||
|                     userId, | ||||
|                     archived, | ||||
|                 ), | ||||
|             ]); | ||||
| 
 | ||||
|         if (this.flagResolver.isEnabled('separateAdminClientApi')) { | ||||
|             features = await this.featureToggleStore.getFeatureToggleList( | ||||
|                 query, | ||||
|                 userId, | ||||
|                 archived, | ||||
|         const equal = isEqual( | ||||
|             featuresFromClientStore, | ||||
|             featuresFromFeatureToggleStore, | ||||
|         ); | ||||
| 
 | ||||
|         if (!equal) { | ||||
|             this.logger.warn( | ||||
|                 'features from client-feature-toggle-store is not equal to features from feature-toggle-store diff', | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const features = this.flagResolver.isEnabled('useLastSeenRefactor') | ||||
|             ? featuresFromFeatureToggleStore | ||||
|             : featuresFromClientStore; | ||||
| 
 | ||||
|         if (this.flagResolver.isEnabled('privateProjects') && userId) { | ||||
|             const projectAccess = | ||||
|                 await this.privateProjectChecker.getUserAccessibleProjects( | ||||
|  | ||||
| @ -14,14 +14,15 @@ import { IFeatureToggleStore } from './types/feature-toggle-store-type'; | ||||
| import { Db } from '../../db/db'; | ||||
| import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service'; | ||||
| import { NameExistsError } from '../../error'; | ||||
| import { DEFAULT_ENV, ensureStringValue, mapValues } from '../../../lib/util'; | ||||
| import { | ||||
|     IFeatureToggleClient, | ||||
|     IStrategyConfig, | ||||
|     ITag, | ||||
|     PartialDeep, | ||||
| } from '../../../lib/types'; | ||||
| 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'; | ||||
| 
 | ||||
| export type EnvironmentFeatureNames = { [key: string]: string[] }; | ||||
| 
 | ||||
| @ -57,123 +58,6 @@ interface VariantDTO { | ||||
| const TABLE = 'features'; | ||||
| const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments'; | ||||
| 
 | ||||
| const isUnseenStrategyRow = ( | ||||
|     feature: PartialDeep<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ): boolean => { | ||||
|     return ( | ||||
|         row.strategy_id && | ||||
|         !feature.strategies?.find((s) => s?.id === row.strategy_id) | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const isNewTag = ( | ||||
|     feature: PartialDeep<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ): 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<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ) => { | ||||
|     feature.strategies | ||||
|         ?.find((s) => s?.id === row.strategy_id) | ||||
|         ?.constraints?.push(...row.segment_constraints); | ||||
| }; | ||||
| 
 | ||||
| const addSegmentIdsToStrategy = ( | ||||
|     feature: PartialDeep<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ) => { | ||||
|     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<string, any>): 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<string, any>, | ||||
|     row: Record<string, any>, | ||||
| ): void => { | ||||
|     const tags = feature.tags || []; | ||||
|     const newTag = rowToTag(row); | ||||
|     feature.tags = [...tags, newTag]; | ||||
| }; | ||||
| 
 | ||||
| const rowToTag = (row: Record<string, any>): ITag => { | ||||
|     return { | ||||
|         value: row.tag_value, | ||||
|         type: row.tag_type, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| const buildFeatureToggleListFromRows = ( | ||||
|     rows: any[], | ||||
|     featureQuery?: IFeatureToggleQuery, | ||||
| ): FeatureToggle[] => { | ||||
|     const result = rows.reduce((acc, r) => { | ||||
|         const feature: PartialDeep<IFeatureToggleClient> = 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; | ||||
| 
 | ||||
| @ -221,13 +105,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore { | ||||
|             .then(this.rowToFeature); | ||||
|     } | ||||
| 
 | ||||
|     async getFeatureToggleList( | ||||
|         featureQuery?: IFeatureToggleQuery, | ||||
|         userId?: number, | ||||
|         archived: boolean = false, | ||||
|     ): Promise<FeatureToggle[]> { | ||||
|         const environment = featureQuery?.environment || DEFAULT_ENV; | ||||
| 
 | ||||
|     private getBaseFeatureQuery = (archived: boolean, environment: string) => { | ||||
|         const builder = new FeatureToggleListBuilder(this.db); | ||||
| 
 | ||||
|         builder | ||||
| @ -236,13 +114,28 @@ export default class FeatureToggleStore implements IFeatureToggleStore { | ||||
|             .withStrategies(environment) | ||||
|             .withFeatureEnvironments(environment) | ||||
|             .withFeatureStrategySegments() | ||||
|             .withSegments() | ||||
|             .withDependentFeatureToggles() | ||||
|             .withFeatureTags(); | ||||
|             .withSegments(); | ||||
| 
 | ||||
|         return builder; | ||||
|     }; | ||||
| 
 | ||||
|     async getFeatureToggleList( | ||||
|         featureQuery?: IFeatureToggleQuery, | ||||
|         userId?: number, | ||||
|         archived: boolean = false, | ||||
|     ): Promise<FeatureToggle[]> { | ||||
|         const environment = featureQuery?.environment || DEFAULT_ENV; | ||||
| 
 | ||||
|         const builder = this.getBaseFeatureQuery( | ||||
|             archived, | ||||
|             environment, | ||||
|         ).withFeatureTags(); | ||||
| 
 | ||||
|         builder.addSelectColumn('ft.tag_value as tag_value'); | ||||
|         builder.addSelectColumn('ft.tag_type as tag_type'); | ||||
| 
 | ||||
|         if (userId) { | ||||
|             builder.withFavorites(userId); | ||||
| 
 | ||||
|             builder.addSelectColumn( | ||||
|                 this.db.raw( | ||||
|                     'favorite_features.feature is not null as favorite', | ||||
| @ -257,6 +150,34 @@ export default class FeatureToggleStore implements IFeatureToggleStore { | ||||
|         return buildFeatureToggleListFromRows(rows, featureQuery); | ||||
|     } | ||||
| 
 | ||||
|     async getPlaygroundFeatures( | ||||
|         dependentFeaturesEnabled: boolean, | ||||
|         featureQuery: IFeatureToggleQuery, | ||||
|     ): Promise<FeatureConfigurationClient[]> { | ||||
|         const environment = featureQuery?.environment || DEFAULT_ENV; | ||||
| 
 | ||||
|         const archived = false; | ||||
|         const builder = this.getBaseFeatureQuery(archived, environment); | ||||
| 
 | ||||
|         if (dependentFeaturesEnabled) { | ||||
|             builder.withDependentFeatureToggles(); | ||||
| 
 | ||||
|             builder.addSelectColumn('df.parent as parent'); | ||||
|             builder.addSelectColumn('df.variants as parent_variants'); | ||||
|             builder.addSelectColumn('df.enabled as parent_enabled'); | ||||
|         } | ||||
| 
 | ||||
|         const rows = await builder.internalQuery.select( | ||||
|             builder.getSelectColumns(), | ||||
|         ); | ||||
| 
 | ||||
|         return buildPlaygroundFeaturesFromRows( | ||||
|             rows, | ||||
|             dependentFeaturesEnabled, | ||||
|             featureQuery, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     async getAll( | ||||
|         query: { | ||||
|             archived?: boolean; | ||||
|  | ||||
							
								
								
									
										219
									
								
								src/lib/features/feature-toggle/feature-toggle-utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										219
									
								
								src/lib/features/feature-toggle/feature-toggle-utils.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,219 @@ | ||||
| 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<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ): boolean => { | ||||
|     return ( | ||||
|         row.strategy_id && | ||||
|         !feature.strategies?.find((s) => s?.id === row.strategy_id) | ||||
|     ); | ||||
| }; | ||||
| 
 | ||||
| const isNewTag = ( | ||||
|     feature: PartialDeep<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ): 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<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ) => { | ||||
|     feature.strategies | ||||
|         ?.find((s) => s?.id === row.strategy_id) | ||||
|         ?.constraints?.push(...row.segment_constraints); | ||||
| }; | ||||
| 
 | ||||
| const addSegmentIdsToStrategy = ( | ||||
|     feature: PartialDeep<IFeatureToggleClient>, | ||||
|     row: Record<string, any>, | ||||
| ) => { | ||||
|     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<string, any>): 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<string, any>, | ||||
|     row: Record<string, any>, | ||||
| ): void => { | ||||
|     const tags = feature.tags || []; | ||||
|     const newTag = rowToTag(row); | ||||
|     feature.tags = [...tags, newTag]; | ||||
| }; | ||||
| 
 | ||||
| const rowToTag = (row: Record<string, any>): 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<IFeatureToggleClient> = 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<IFeatureToggleClient> = 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; | ||||
| } | ||||
| @ -1,5 +1,5 @@ | ||||
| import { Db } from '../../db/db'; | ||||
| import { IFeaturesReadModel } from './features-read-model-type'; | ||||
| import { IFeaturesReadModel } from './types/features-read-model-type'; | ||||
| 
 | ||||
| export class FeaturesReadModel implements IFeaturesReadModel { | ||||
|     private db: Db; | ||||
|  | ||||
| @ -6,6 +6,7 @@ import { | ||||
| } from '../../../types/model'; | ||||
| import { Store } from '../../../types/stores/store'; | ||||
| import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service'; | ||||
| import { FeatureConfigurationClient } from './feature-toggle-strategies-store-type'; | ||||
| 
 | ||||
| export interface IFeatureToggleStoreQuery { | ||||
|     archived: boolean; | ||||
| @ -36,6 +37,10 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> { | ||||
|         userId?: number, | ||||
|         archived?: boolean, | ||||
|     ): Promise<FeatureToggle[]>; | ||||
|     getPlaygroundFeatures( | ||||
|         dependentFeaturesEnabled: boolean, | ||||
|         featureQuery?: IFeatureToggleQuery, | ||||
|     ): Promise<FeatureConfigurationClient[]>; | ||||
|     countByDate(queryModifiers: { | ||||
|         archived?: boolean; | ||||
|         project?: string; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user