1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +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:
Fredrik Strand Oseberg 2023-10-16 12:29:31 +02:00 committed by GitHub
parent 092ba2a625
commit b58d900c2d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 348 additions and 154 deletions

View File

@ -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,

View File

@ -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;

View File

@ -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(

View File

@ -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> {

View File

@ -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(

View File

@ -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;

View 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;
}

View File

@ -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;

View File

@ -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;