Skip to content

Commit

Permalink
feat: copy feature with parent (#4918)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Oct 4, 2023
1 parent 5141d9d commit 2574144
Show file tree
Hide file tree
Showing 13 changed files with 140 additions and 346 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const AddDependencyDialogue = ({
secondaryButtonText='Cancel'
>
<Box>
You feature will be evaluated only when the selected parent
Your feature will be evaluated only when the selected parent
feature is enabled in the same environment.
<br />
<br />
Expand Down
26 changes: 26 additions & 0 deletions src/lib/features/dependent-features/dependent-features-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,32 @@ export class DependentFeaturesService {
this.eventService = eventService;
}

async cloneDependencies(
{
featureName,
newFeatureName,
projectId,
}: { featureName: string; newFeatureName: string; projectId: string },
user: string,
) {
const parents = await this.dependentFeaturesReadModel.getParents(
featureName,
);
await Promise.all(
parents.map((parent) =>
this.upsertFeatureDependency(
{ child: newFeatureName, projectId },
{
feature: parent.feature,
enabled: parent.enabled,
variants: parent.variants,
},
user,
),
),
);
}

async upsertFeatureDependency(
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
Expand Down
9 changes: 9 additions & 0 deletions src/lib/features/feature-toggle/createFeatureToggleService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ import { DependentFeaturesReadModel } from '../dependent-features/dependent-feat
import { FakeDependentFeaturesReadModel } from '../dependent-features/fake-dependent-features-read-model';
import FeatureTagStore from '../../db/feature-tag-store';
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';
import {
createDependentFeaturesService,
createFakeDependentFeaturesService,
} from '../dependent-features/createDependentFeaturesService';

export const createFeatureToggleService = (
db: Db,
Expand Down Expand Up @@ -115,6 +119,8 @@ export const createFeatureToggleService = (

const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);

const dependentFeaturesService = createDependentFeaturesService(db, config);

const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
Expand All @@ -133,6 +139,7 @@ export const createFeatureToggleService = (
changeRequestAccessReadModel,
privateProjectChecker,
dependentFeaturesReadModel,
dependentFeaturesService,
);
return featureToggleService;
};
Expand Down Expand Up @@ -173,6 +180,7 @@ export const createFakeFeatureToggleService = (
const changeRequestAccessReadModel = createFakeChangeRequestAccessService();
const fakePrivateProjectChecker = createFakePrivateProjectChecker();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
const dependentFeaturesService = createFakeDependentFeaturesService(config);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
Expand All @@ -191,6 +199,7 @@ export const createFakeFeatureToggleService = (
changeRequestAccessReadModel,
fakePrivateProjectChecker,
dependentFeaturesReadModel,
dependentFeaturesService,
);
return featureToggleService;
};
2 changes: 2 additions & 0 deletions src/lib/services/feature-service-potentially-stale.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IPrivateProjectChecker } from '../features/private-project/privateProje
import { IDependentFeaturesReadModel } from '../features/dependent-features/dependent-features-read-model-type';
import EventService from './event-service';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';

test('Should only store events for potentially stale on', async () => {
expect.assertions(2);
Expand Down Expand Up @@ -64,6 +65,7 @@ test('Should only store events for potentially stale on', async () => {
{} as IChangeRequestAccessReadModel,
{} as IPrivateProjectChecker,
{} as IDependentFeaturesReadModel,
{} as DependentFeaturesService,
);

await featureToggleService.updatePotentiallyStaleFeatures();
Expand Down
17 changes: 16 additions & 1 deletion src/lib/services/feature-toggle-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import { checkFeatureFlagNamesAgainstPattern } from '../features/feature-naming-
import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType';
import { IDependentFeaturesReadModel } from '../features/dependent-features/dependent-features-read-model-type';
import EventService from './event-service';
import { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';

interface IFeatureContext {
featureName: string;
Expand Down Expand Up @@ -162,6 +163,8 @@ class FeatureToggleService {

private dependentFeaturesReadModel: IDependentFeaturesReadModel;

private dependentFeaturesService: DependentFeaturesService;

constructor(
{
featureStrategiesStore,
Expand Down Expand Up @@ -193,6 +196,7 @@ class FeatureToggleService {
changeRequestAccessReadModel: IChangeRequestAccessReadModel,
privateProjectChecker: IPrivateProjectChecker,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
dependentFeaturesService: DependentFeaturesService,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
Expand All @@ -210,6 +214,7 @@ class FeatureToggleService {
this.changeRequestAccessReadModel = changeRequestAccessReadModel;
this.privateProjectChecker = privateProjectChecker;
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.dependentFeaturesService = dependentFeaturesService;
}

async validateFeaturesContext(
Expand Down Expand Up @@ -1255,7 +1260,17 @@ class FeatureToggleService {
}),
);

await Promise.all([...strategyTasks, ...variantTasks]);
const cloneDependencies =
this.dependentFeaturesService.cloneDependencies(
{ featureName, newFeatureName, projectId },
userName,
);

await Promise.all([
...strategyTasks,
...variantTasks,
cloneDependencies,
]);
return created;
}

Expand Down
14 changes: 8 additions & 6 deletions src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ export const createServices = (
config,
privateProjectChecker,
);

const dependentFeaturesService = db
? createDependentFeaturesService(db, config)
: createFakeDependentFeaturesService(config);
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
createDependentFeaturesService(txDb, config);

const featureToggleServiceV2 = new FeatureToggleService(
stores,
config,
Expand All @@ -242,6 +249,7 @@ export const createServices = (
changeRequestAccessReadModel,
privateProjectChecker,
dependentFeaturesReadModel,
dependentFeaturesService,
);
const environmentService = new EnvironmentService(stores, config);
const featureTagService = new FeatureTagService(
Expand Down Expand Up @@ -327,12 +335,6 @@ export const createServices = (

const eventAnnouncerService = new EventAnnouncerService(stores, config);

const dependentFeaturesService = db
? createDependentFeaturesService(db, config)
: createFakeDependentFeaturesService(config);
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
createDependentFeaturesService(txDb, config);

return {
accessService,
accountService,
Expand Down
23 changes: 23 additions & 0 deletions src/test/e2e/api/admin/project/features.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,29 @@ test('Should not allow to archive/delete feature with children', async () => {
);
});

test('should clone feature with parent dependencies', async () => {
const parent = uuidv4();
const child = uuidv4();
const childClone = uuidv4();
await app.createFeature(parent, 'default');
await app.createFeature(child, 'default');
await app.addDependency(child, parent);

await app.request
.post(`/api/admin/projects/default/features/${child}/clone`)
.send({ name: childClone, replaceGroupId: false })
.expect(201);

const { body: clonedFeature } = await app.getProjectFeatures(
'default',
child,
);
expect(clonedFeature).toMatchObject({
children: [],
dependencies: [{ feature: parent, enabled: true, variants: [] }],
});
});

test('Can get features for project', async () => {
await app.request
.post('/api/admin/projects/default/features')
Expand Down
61 changes: 9 additions & 52 deletions src/test/e2e/services/access-service.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,25 +13,19 @@ import {
IUnleashStores,
IUserAccessOverview,
} from '../../../lib/types';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import ProjectService from '../../../lib/services/project-service';
import { createTestConfig } from '../../config/test-config';
import { DEFAULT_PROJECT } from '../../../lib/types/project';
import { ALL_PROJECTS } from '../../../lib/util/constants';
import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service';
import { EventService, FavoritesService } from '../../../lib/services';
import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model';
import { createPrivateProjectChecker } from '../../../lib/features/private-project/createPrivateProjectChecker';
import { DependentFeaturesReadModel } from '../../../lib/features/dependent-features/dependent-features-read-model';
import {
createAccessService,
createFeatureToggleService,
createProjectService,
} from '../../../lib/features';

let db: ITestDb;
let stores: IUnleashStores;
let accessService: AccessService;
let eventService: EventService;
let groupService: GroupService;
let featureToggleService;
let favoritesService;
let projectService;
let editorUser;
let editorRole;
Expand Down Expand Up @@ -238,51 +232,14 @@ beforeAll(async () => {
// @ts-ignore
experimental: { environments: { enabled: true } },
});
eventService = new EventService(stores, config);
groupService = new GroupService(stores, { getLogger }, eventService);
accessService = new AccessService(stores, config, groupService);
accessService = createAccessService(db.rawDatabase, config);
const roles = await accessService.getRootRoles();
editorRole = roles.find((r) => r.name === RoleName.EDITOR);
adminRole = roles.find((r) => r.name === RoleName.ADMIN);
readRole = roles.find((r) => r.name === RoleName.VIEWER);
const changeRequestAccessReadModel = new ChangeRequestAccessReadModel(
db.rawDatabase,
accessService,
);
const privateProjectChecker = createPrivateProjectChecker(
db.rawDatabase,
config,
);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(
db.rawDatabase,
);
featureToggleService = new FeatureToggleService(
stores,
config,
new SegmentService(
stores,
changeRequestAccessReadModel,
config,
eventService,
privateProjectChecker,
),
accessService,
eventService,
changeRequestAccessReadModel,
privateProjectChecker,
dependentFeaturesReadModel,
);
favoritesService = new FavoritesService(stores, config, eventService);
projectService = new ProjectService(
stores,
config,
accessService,
featureToggleService,
groupService,
favoritesService,
eventService,
privateProjectChecker,
);

featureToggleService = createFeatureToggleService(db.rawDatabase, config);
projectService = createProjectService(db.rawDatabase, config);

editorUser = await createUser(editorRole.id);

Expand Down
52 changes: 3 additions & 49 deletions src/test/e2e/services/api-token-service.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,13 @@ import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token';
import { DEFAULT_ENV } from '../../../lib/util/constants';
import { addDays, subDays } from 'date-fns';
import ProjectService from '../../../lib/services/project-service';
import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { AccessService } from '../../../lib/services/access-service';
import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service';
import { EventService, FavoritesService } from '../../../lib/services';
import { ChangeRequestAccessReadModel } from '../../../lib/features/change-request-access-service/sql-change-request-access-read-model';
import { createPrivateProjectChecker } from '../../../lib/features/private-project/createPrivateProjectChecker';
import { DependentFeaturesReadModel } from '../../../lib/features/dependent-features/dependent-features-read-model';
import { EventService } from '../../../lib/services';
import { createProjectService } from '../../../lib/features';

let db;
let stores;
let apiTokenService: ApiTokenService;
let projectService: ProjectService;
let favoritesService: FavoritesService;

beforeAll(async () => {
const config = createTestConfig({
Expand All @@ -28,35 +21,6 @@ beforeAll(async () => {
db = await dbInit('api_token_service_serial', getLogger);
stores = db.stores;
const eventService = new EventService(stores, config);
const groupService = new GroupService(stores, config, eventService);
const accessService = new AccessService(stores, config, groupService);
const changeRequestAccessReadModel = new ChangeRequestAccessReadModel(
db.rawDatabase,
accessService,
);
const privateProjectChecker = createPrivateProjectChecker(
db.rawDatabase,
config,
);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(
db.rawDatabase,
);
const featureToggleService = new FeatureToggleService(
stores,
config,
new SegmentService(
stores,
changeRequestAccessReadModel,
config,
eventService,
privateProjectChecker,
),
accessService,
eventService,
changeRequestAccessReadModel,
privateProjectChecker,
dependentFeaturesReadModel,
);
const project = {
id: 'test-project',
name: 'Test Project',
Expand All @@ -68,17 +32,7 @@ beforeAll(async () => {
name: 'Some Name',
email: '[email protected]',
});
favoritesService = new FavoritesService(stores, config, eventService);
projectService = new ProjectService(
stores,
config,
accessService,
featureToggleService,
groupService,
favoritesService,
eventService,
privateProjectChecker,
);
projectService = createProjectService(db.rawDatabase, config);

await projectService.createProject(project, user);

Expand Down
Loading

0 comments on commit 2574144

Please sign in to comment.