Skip to content

Commit

Permalink
feat: export dependent feature toggles (#5007)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus authored Oct 12, 2023
1 parent 7343183 commit 2059706
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { IDependency } from '../../types';
import { IDependency, IFeatureDependency } from '../../types';
import { FeatureDependency } from './dependent-features';

export interface IDependentFeaturesReadModel {
getChildren(parents: string[]): Promise<string[]>;
// given a list of parents and children verifies if some children would be orphaned after deletion
// we're interested in the list of parents, not orphans
getOrphanParents(parentsAndChildren: string[]): Promise<string[]>;
getParents(child: string): Promise<IDependency[]>;
getDependencies(children: string[]): Promise<IFeatureDependency[]>;
getParentOptions(child: string): Promise<string[]>;
hasDependencies(feature: string): Promise<boolean>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Db } from '../../db/db';
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
import { IDependency } from '../../types';
import { IDependency, IFeatureDependency } from '../../types';
import { FeatureDependency } from './dependent-features';

export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
private db: Db;
Expand Down Expand Up @@ -43,6 +44,22 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
}));
}

async getDependencies(children: string[]): Promise<IFeatureDependency[]> {
const rows = await this.db('dependent_features').whereIn(
'child',
children,
);

return rows.map((row) => ({
feature: row.child,
dependency: {
feature: row.parent,
enabled: row.enabled,
variants: row.variants,
},
}));
}

async getParentOptions(child: string): Promise<string[]> {
const result = await this.db('features')
.where('features.name', child)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
import { IDependency } from '../../types';
import { IDependency, IFeatureDependency } from '../../types';
import { FeatureDependency } from './dependent-features';

export class FakeDependentFeaturesReadModel
implements IDependentFeaturesReadModel
{
getDependencies(): Promise<IFeatureDependency[]> {
return Promise.resolve([]);
}
getChildren(): Promise<string[]> {
return Promise.resolve([]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import {
createPrivateProjectChecker,
} from '../private-project/createPrivateProjectChecker';
import { DbServiceFactory } from 'lib/db/transaction';
import { DependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model';
import { FakeDependentFeaturesReadModel } from '../dependent-features/fake-dependent-features-read-model';

export const createFakeExportImportTogglesService = (
config: IUnleashConfig,
Expand Down Expand Up @@ -102,6 +104,8 @@ export const createFakeExportImportTogglesService = (
{ getLogger },
eventService,
);
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();

const exportImportService = new ExportImportService(
{
importTogglesStore,
Expand All @@ -123,6 +127,7 @@ export const createFakeExportImportTogglesService = (
strategyService,
tagTypeService,
},
dependentFeaturesReadModel,
);

return exportImportService;
Expand Down Expand Up @@ -213,6 +218,8 @@ export const deferredExportImportTogglesService = (
{ getLogger },
eventService,
);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);

const exportImportService = new ExportImportService(
{
importTogglesStore,
Expand All @@ -234,6 +241,7 @@ export const deferredExportImportTogglesService = (
strategyService,
tagTypeService,
},
dependentFeaturesReadModel,
);

return exportImportService;
Expand Down
58 changes: 43 additions & 15 deletions src/lib/features/export-import-toggles/export-import-service.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { IUnleashConfig } from '../../types/option';
import { Logger } from '../../logger';
import { IStrategy } from '../../types/stores/strategy-store';
import { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
import { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import {
IUnleashConfig,
IContextFieldStore,
IUnleashStores,
ISegmentStore,
IFeatureEnvironmentStore,
ITagTypeStore,
IFeatureTagStore,
FeatureToggleDTO,
IFeatureStrategy,
IFeatureStrategySegment,
IVariant,
} from '../../types/model';
import { Logger } from '../../logger';
import { IFeatureTagStore } from '../../types/stores/feature-tag-store';
import { ITagTypeStore } from '../../types/stores/tag-type-store';
import { IStrategy } from '../../types/stores/strategy-store';
import { IFeatureToggleStore } from '../feature-toggle/types/feature-toggle-store-type';
import { IFeatureStrategiesStore } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { IFeatureEnvironmentStore } from '../../types/stores/feature-environment-store';
import { IContextFieldStore, IUnleashStores } from '../../types/stores';
import { ISegmentStore } from '../../types/stores/segment-store';
import { ExportQuerySchema } from '../../openapi/spec/export-query-schema';
} from '../../types';
import { ExportQuerySchema, ImportTogglesSchema } from '../../openapi';
import {
FEATURES_EXPORTED,
FEATURES_IMPORTED,
Expand All @@ -27,7 +28,6 @@ import {
FeatureStrategySchema,
ImportTogglesValidateSchema,
} from '../../openapi';
import { ImportTogglesSchema } from '../../openapi/spec/import-toggles-schema';
import User from '../../types/user';
import { BadDataError } from '../../error';
import { extractUsernameFromUser } from '../../util';
Expand All @@ -49,6 +49,8 @@ import { ImportPermissionsService, Mode } from './import-permissions-service';
import { ImportValidationMessages } from './import-validation-messages';
import { findDuplicates } from '../../util/findDuplicates';
import { FeatureNameCheckResultWithFeaturePattern } from '../feature-toggle/feature-toggle-service';
import { IDependentFeaturesReadModel } from '../dependent-features/dependent-features-read-model-type';
import groupBy from 'lodash.groupby';

export type IImportService = {
validate(
Expand Down Expand Up @@ -105,6 +107,8 @@ export default class ExportImportService

private importPermissionsService: ImportPermissionsService;

private dependentFeaturesReadModel: IDependentFeaturesReadModel;

constructor(
stores: Pick<
IUnleashStores,
Expand Down Expand Up @@ -139,6 +143,7 @@ export default class ExportImportService
| 'tagTypeService'
| 'featureTagService'
>,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
) {
this.toggleStore = stores.featureToggleStore;
this.importTogglesStore = stores.importTogglesStore;
Expand All @@ -162,6 +167,7 @@ export default class ExportImportService
this.tagTypeService,
this.contextService,
);
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.logger = getLogger('services/state-service.js');
}

Expand Down Expand Up @@ -332,7 +338,10 @@ export default class ExportImportService
if (tag.tagType) {
await this.featureTagService.addTag(
tag.featureName,
{ type: tag.tagType, value: tag.tagValue },
{
type: tag.tagType,
value: tag.tagValue,
},
extractUsernameFromUser(user),
);
}
Expand Down Expand Up @@ -657,6 +666,7 @@ export default class ExportImportService
featureTags,
segments,
tagTypes,
featureDependencies,
] = await Promise.all([
this.toggleStore.getAllByNames(featureNames),
await this.featureEnvironmentStore.getAllByFeatures(
Expand All @@ -672,6 +682,7 @@ export default class ExportImportService
this.featureTagStore.getAllByFeatures(featureNames),
this.segmentStore.getAll(),
this.tagTypeStore.getAll(),
this.dependentFeaturesReadModel.getDependencies(featureNames),
]);
this.addSegmentsToStrategies(featureStrategies, strategySegments);
const filteredContextFields = contextFields
Expand Down Expand Up @@ -708,6 +719,19 @@ export default class ExportImportService
const filteredTagTypes = tagTypes.filter((tagType) =>
featureTags.map((tag) => tag.tagType).includes(tagType.name),
);

const groupedFeatureDependencies = groupBy(
featureDependencies,
'feature',
);

const mappedFeatureDependencies = Object.entries(
groupedFeatureDependencies,
).map(([feature, dependencies]) => ({
feature,
dependencies: dependencies.map((d) => d.dependency),
}));

const result = {
features: features.map((item) => {
const { createdAt, archivedAt, lastSeenAt, ...rest } = item;
Expand Down Expand Up @@ -741,9 +765,13 @@ export default class ExportImportService
featureTags,
segments: filteredSegments.map((item) => {
const { id, name } = item;
return { id, name };
return {
id,
name,
};
}),
tagTypes: filteredTagTypes,
dependencies: mappedFeatureDependencies,
};
await this.eventService.storeEvent({
type: FEATURES_EXPORTED,
Expand Down
Loading

0 comments on commit 2059706

Please sign in to comment.