mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02: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,
|
createFakeChangeRequestAccessService,
|
||||||
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
} from '../change-request-access-service/createChangeRequestAccessReadModel';
|
||||||
import { FeaturesReadModel } from '../feature-toggle/features-read-model';
|
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 = (
|
export const createDependentFeaturesService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
|
@ -8,7 +8,7 @@ import { User } from '../../server-impl';
|
|||||||
import { SKIP_CHANGE_REQUEST } from '../../types';
|
import { SKIP_CHANGE_REQUEST } from '../../types';
|
||||||
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
|
import { IChangeRequestAccessReadModel } from '../change-request-access-service/change-request-access-read-model';
|
||||||
import { extractUsernameFromUser } from '../../util';
|
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 {
|
interface IDependentFeaturesServiceDeps {
|
||||||
dependentFeaturesStore: IDependentFeaturesStore;
|
dependentFeaturesStore: IDependentFeaturesStore;
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
} from 'lib/types/model';
|
} from 'lib/types/model';
|
||||||
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
||||||
import { EnvironmentFeatureNames } from '../feature-toggle-store';
|
import { EnvironmentFeatureNames } from '../feature-toggle-store';
|
||||||
|
import { FeatureConfigurationClient } from '../types/feature-toggle-strategies-store-type';
|
||||||
|
|
||||||
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
||||||
features: FeatureToggle[] = [];
|
features: FeatureToggle[] = [];
|
||||||
@ -159,7 +160,16 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
|
|||||||
userId?: number,
|
userId?: number,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<FeatureToggle[]> {
|
): 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(
|
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 {
|
export class FakeFeaturesReadModel implements IFeaturesReadModel {
|
||||||
featureExists(): Promise<boolean> {
|
featureExists(): Promise<boolean> {
|
@ -27,6 +27,7 @@ import {
|
|||||||
IFeatureOverview,
|
IFeatureOverview,
|
||||||
IFeatureStrategy,
|
IFeatureStrategy,
|
||||||
IFeatureTagStore,
|
IFeatureTagStore,
|
||||||
|
IFeatureToggleClient,
|
||||||
IFeatureToggleClientStore,
|
IFeatureToggleClientStore,
|
||||||
IFeatureToggleQuery,
|
IFeatureToggleQuery,
|
||||||
IFeatureToggleStore,
|
IFeatureToggleStore,
|
||||||
@ -101,6 +102,7 @@ import { IPrivateProjectChecker } from '../private-project/privateProjectChecker
|
|||||||
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
|
||||||
import EventService from '../../services/event-service';
|
import EventService from '../../services/event-service';
|
||||||
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
import { DependentFeaturesService } from '../dependent-features/dependent-features-service';
|
||||||
|
import isEqual from 'lodash.isequal';
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -130,7 +132,6 @@ export type FeatureNameCheckResultWithFeaturePattern =
|
|||||||
const oneOf = (values: string[], match: string) => {
|
const oneOf = (values: string[], match: string) => {
|
||||||
return values.some((value) => value === match);
|
return values.some((value) => value === match);
|
||||||
};
|
};
|
||||||
|
|
||||||
class FeatureToggleService {
|
class FeatureToggleService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
@ -1049,10 +1050,32 @@ class FeatureToggleService {
|
|||||||
async getPlaygroundFeatures(
|
async getPlaygroundFeatures(
|
||||||
query?: IFeatureToggleQuery,
|
query?: IFeatureToggleQuery,
|
||||||
): Promise<FeatureConfigurationClient[]> {
|
): Promise<FeatureConfigurationClient[]> {
|
||||||
const result = await this.clientFeatureToggleStore.getPlayground(
|
// Remove with with feature flag
|
||||||
query || {},
|
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,
|
userId?: number,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
): Promise<FeatureToggle[]> {
|
): Promise<FeatureToggle[]> {
|
||||||
let features = (await this.clientFeatureToggleStore.getAdmin({
|
// Remove with with feature flag
|
||||||
featureQuery: query,
|
const [featuresFromClientStore, featuresFromFeatureToggleStore] =
|
||||||
userId: userId,
|
await Promise.all([
|
||||||
archived: false,
|
(await this.clientFeatureToggleStore.getAdmin({
|
||||||
})) as FeatureToggle[];
|
featureQuery: query,
|
||||||
|
userId: userId,
|
||||||
|
archived: false,
|
||||||
|
})) as FeatureToggle[],
|
||||||
|
await this.featureToggleStore.getFeatureToggleList(
|
||||||
|
query,
|
||||||
|
userId,
|
||||||
|
archived,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('separateAdminClientApi')) {
|
const equal = isEqual(
|
||||||
features = await this.featureToggleStore.getFeatureToggleList(
|
featuresFromClientStore,
|
||||||
query,
|
featuresFromFeatureToggleStore,
|
||||||
userId,
|
);
|
||||||
archived,
|
|
||||||
|
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) {
|
if (this.flagResolver.isEnabled('privateProjects') && userId) {
|
||||||
const projectAccess =
|
const projectAccess =
|
||||||
await this.privateProjectChecker.getUserAccessibleProjects(
|
await this.privateProjectChecker.getUserAccessibleProjects(
|
||||||
|
@ -14,14 +14,15 @@ import { IFeatureToggleStore } from './types/feature-toggle-store-type';
|
|||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../services/client-metrics/last-seen/last-seen-service';
|
||||||
import { NameExistsError } from '../../error';
|
import { NameExistsError } from '../../error';
|
||||||
import { DEFAULT_ENV, ensureStringValue, mapValues } from '../../../lib/util';
|
import { DEFAULT_ENV } from '../../../lib/util';
|
||||||
import {
|
|
||||||
IFeatureToggleClient,
|
|
||||||
IStrategyConfig,
|
|
||||||
ITag,
|
|
||||||
PartialDeep,
|
|
||||||
} from '../../../lib/types';
|
|
||||||
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
|
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[] };
|
export type EnvironmentFeatureNames = { [key: string]: string[] };
|
||||||
|
|
||||||
@ -57,123 +58,6 @@ interface VariantDTO {
|
|||||||
const TABLE = 'features';
|
const TABLE = 'features';
|
||||||
const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments';
|
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 {
|
export default class FeatureToggleStore implements IFeatureToggleStore {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
|
|
||||||
@ -221,13 +105,7 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
.then(this.rowToFeature);
|
.then(this.rowToFeature);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFeatureToggleList(
|
private getBaseFeatureQuery = (archived: boolean, environment: string) => {
|
||||||
featureQuery?: IFeatureToggleQuery,
|
|
||||||
userId?: number,
|
|
||||||
archived: boolean = false,
|
|
||||||
): Promise<FeatureToggle[]> {
|
|
||||||
const environment = featureQuery?.environment || DEFAULT_ENV;
|
|
||||||
|
|
||||||
const builder = new FeatureToggleListBuilder(this.db);
|
const builder = new FeatureToggleListBuilder(this.db);
|
||||||
|
|
||||||
builder
|
builder
|
||||||
@ -236,13 +114,28 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
.withStrategies(environment)
|
.withStrategies(environment)
|
||||||
.withFeatureEnvironments(environment)
|
.withFeatureEnvironments(environment)
|
||||||
.withFeatureStrategySegments()
|
.withFeatureStrategySegments()
|
||||||
.withSegments()
|
.withSegments();
|
||||||
.withDependentFeatureToggles()
|
|
||||||
.withFeatureTags();
|
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) {
|
if (userId) {
|
||||||
builder.withFavorites(userId);
|
builder.withFavorites(userId);
|
||||||
|
|
||||||
builder.addSelectColumn(
|
builder.addSelectColumn(
|
||||||
this.db.raw(
|
this.db.raw(
|
||||||
'favorite_features.feature is not null as favorite',
|
'favorite_features.feature is not null as favorite',
|
||||||
@ -257,6 +150,34 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
|
|||||||
return buildFeatureToggleListFromRows(rows, featureQuery);
|
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(
|
async getAll(
|
||||||
query: {
|
query: {
|
||||||
archived?: boolean;
|
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 { Db } from '../../db/db';
|
||||||
import { IFeaturesReadModel } from './features-read-model-type';
|
import { IFeaturesReadModel } from './types/features-read-model-type';
|
||||||
|
|
||||||
export class FeaturesReadModel implements IFeaturesReadModel {
|
export class FeaturesReadModel implements IFeaturesReadModel {
|
||||||
private db: Db;
|
private db: Db;
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
} from '../../../types/model';
|
} from '../../../types/model';
|
||||||
import { Store } from '../../../types/stores/store';
|
import { Store } from '../../../types/stores/store';
|
||||||
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
import { LastSeenInput } from '../../../services/client-metrics/last-seen/last-seen-service';
|
||||||
|
import { FeatureConfigurationClient } from './feature-toggle-strategies-store-type';
|
||||||
|
|
||||||
export interface IFeatureToggleStoreQuery {
|
export interface IFeatureToggleStoreQuery {
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
@ -36,6 +37,10 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
|
|||||||
userId?: number,
|
userId?: number,
|
||||||
archived?: boolean,
|
archived?: boolean,
|
||||||
): Promise<FeatureToggle[]>;
|
): Promise<FeatureToggle[]>;
|
||||||
|
getPlaygroundFeatures(
|
||||||
|
dependentFeaturesEnabled: boolean,
|
||||||
|
featureQuery?: IFeatureToggleQuery,
|
||||||
|
): Promise<FeatureConfigurationClient[]>;
|
||||||
countByDate(queryModifiers: {
|
countByDate(queryModifiers: {
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
project?: string;
|
project?: string;
|
||||||
|
Loading…
Reference in New Issue
Block a user