Skip to content

Commit

Permalink
feat: events for dependencies (#4864)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew committed Sep 29, 2023
1 parent 011aea2 commit fbc571d
Show file tree
Hide file tree
Showing 10 changed files with 143 additions and 34 deletions.
Expand Up @@ -82,6 +82,7 @@ export const useDependentFeaturesApi = (project: string) => {
makeRequest,
setToastData,
formatUnknownError,
project,
];
return {
addDependency: useCallback(addDependency, callbackDeps),
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -161,7 +161,7 @@
"stoppable": "^1.1.0",
"ts-toolbelt": "^9.6.0",
"type-is": "^1.6.18",
"unleash-client": "4.1.1",
"unleash-client": "4.2.0-beta.0",
"uuid": "^9.0.0"
},
"devDependencies": {
Expand Down
Expand Up @@ -4,24 +4,53 @@ import { DependentFeaturesStore } from './dependent-features-store';
import { DependentFeaturesReadModel } from './dependent-features-read-model';
import { FakeDependentFeaturesStore } from './fake-dependent-features-store';
import { FakeDependentFeaturesReadModel } from './fake-dependent-features-read-model';
import EventStore from '../../db/event-store';
import { IUnleashConfig } from '../../types';
import { EventService } from '../../services';
import FeatureTagStore from '../../db/feature-tag-store';
import FakeEventStore from '../../../test/fixtures/fake-event-store';
import FakeFeatureTagStore from '../../../test/fixtures/fake-feature-tag-store';

export const createDependentFeaturesService = (
db: Db,
config: IUnleashConfig,
): DependentFeaturesService => {
const { getLogger, eventBus } = config;
const eventStore = new EventStore(db, getLogger);
const featureTagStore = new FeatureTagStore(db, eventBus, getLogger);
const eventService = new EventService(
{
eventStore,
featureTagStore,
},
config,
);
const dependentFeaturesStore = new DependentFeaturesStore(db);
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
return new DependentFeaturesService(
dependentFeaturesStore,
dependentFeaturesReadModel,
eventService,
);
};

export const createFakeDependentFeaturesService =
(): DependentFeaturesService => {
const dependentFeaturesStore = new FakeDependentFeaturesStore();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
return new DependentFeaturesService(
dependentFeaturesStore,
dependentFeaturesReadModel,
);
};
export const createFakeDependentFeaturesService = (
config: IUnleashConfig,
): DependentFeaturesService => {
const eventStore = new FakeEventStore();
const featureTagStore = new FakeFeatureTagStore();
const eventService = new EventService(
{
eventStore,
featureTagStore,
},
config,
);
const dependentFeaturesStore = new FakeDependentFeaturesStore();
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
return new DependentFeaturesService(
dependentFeaturesStore,
dependentFeaturesReadModel,
eventService,
);
};
Expand Up @@ -21,12 +21,17 @@ import { IAuthRequest } from '../../routes/unleash-types';
import { InvalidOperationError } from '../../error';
import { DependentFeaturesService } from './dependent-features-service';
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
import { extractUsernameFromUser } from '../../util';

interface FeatureParams {
interface ProjectParams {
projectId: string;
}

interface FeatureParams extends ProjectParams {
child: string;
}

interface DeleteDependencyParams {
interface DeleteDependencyParams extends ProjectParams {
child: string;
parent: string;
}
Expand Down Expand Up @@ -167,18 +172,22 @@ export default class DependentFeaturesController extends Controller {
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
res: Response,
): Promise<void> {
const { child } = req.params;
const { child, projectId } = req.params;
const { variants, enabled, feature } = req.body;

if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.startTransaction(async (tx) =>
this.transactionalDependentFeaturesService(
tx,
).upsertFeatureDependency(child, {
variants,
enabled,
feature,
}),
).upsertFeatureDependency(
{ child, projectId },
{
variants,
enabled,
feature,
},
extractUsernameFromUser(req.user),
),
);
res.status(200).end();
} else {
Expand All @@ -192,13 +201,17 @@ export default class DependentFeaturesController extends Controller {
req: IAuthRequest<DeleteDependencyParams, any, any>,
res: Response,
): Promise<void> {
const { child, parent } = req.params;
const { child, parent, projectId } = req.params;

if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.dependentFeaturesService.deleteFeatureDependency({
parent,
child,
});
await this.dependentFeaturesService.deleteFeatureDependency(
{
parent,
child,
},
projectId,
extractUsernameFromUser(req.user),
);
res.status(200).end();
} else {
throw new InvalidOperationError(
Expand All @@ -211,11 +224,13 @@ export default class DependentFeaturesController extends Controller {
req: IAuthRequest<FeatureParams, any, any>,
res: Response,
): Promise<void> {
const { child } = req.params;
const { child, projectId } = req.params;

if (this.config.flagResolver.isEnabled('dependentFeatures')) {
await this.dependentFeaturesService.deleteFeatureDependencies(
child,
projectId,
extractUsernameFromUser(req.user),
);
res.status(200).end();
} else {
Expand Down
40 changes: 38 additions & 2 deletions src/lib/features/dependent-features/dependent-features-service.ts
Expand Up @@ -3,23 +3,29 @@ import { CreateDependentFeatureSchema } from '../../openapi';
import { IDependentFeaturesStore } from './dependent-features-store-type';
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
import { EventService } from '../../services';

export class DependentFeaturesService {
private dependentFeaturesStore: IDependentFeaturesStore;

private dependentFeaturesReadModel: IDependentFeaturesReadModel;

private eventService: EventService;

constructor(
dependentFeaturesStore: IDependentFeaturesStore,
dependentFeaturesReadModel: IDependentFeaturesReadModel,
eventService: EventService,
) {
this.dependentFeaturesStore = dependentFeaturesStore;
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.eventService = eventService;
}

async upsertFeatureDependency(
child: string,
{ child, projectId }: { child: string; projectId: string },
dependentFeature: CreateDependentFeatureSchema,
user: string,
): Promise<void> {
const { enabled, feature: parent, variants } = dependentFeature;

Expand All @@ -46,16 +52,46 @@ export class DependentFeaturesService {
variants,
};
await this.dependentFeaturesStore.upsert(featureDependency);
await this.eventService.storeEvent({
type: 'feature-dependency-added',
project: projectId,
featureName: child,
createdBy: user,
data: {
feature: parent,
enabled: featureDependency.enabled,
...(variants !== undefined && { variants }),
},
});
}

async deleteFeatureDependency(
dependency: FeatureDependencyId,
projectId: string,
user: string,
): Promise<void> {
await this.dependentFeaturesStore.delete(dependency);
await this.eventService.storeEvent({
type: 'feature-dependency-removed',
project: projectId,
featureName: dependency.child,
createdBy: user,
data: { feature: dependency.parent },
});
}

async deleteFeatureDependencies(feature: string): Promise<void> {
async deleteFeatureDependencies(
feature: string,
projectId: string,
user: string,
): Promise<void> {
await this.dependentFeaturesStore.deleteAll(feature);
await this.eventService.storeEvent({
type: 'feature-dependencies-removed',
project: projectId,
featureName: feature,
createdBy: user,
});
}

async getParentOptions(feature: string): Promise<string[]> {
Expand Down
20 changes: 20 additions & 0 deletions src/lib/features/dependent-features/dependent.features.e2e.test.ts
Expand Up @@ -6,9 +6,16 @@ import {
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
import { CreateDependentFeatureSchema } from '../../openapi';
import {
FEATURE_DEPENDENCIES_REMOVED,
FEATURE_DEPENDENCY_ADDED,
FEATURE_DEPENDENCY_REMOVED,
IEventStore,
} from '../../types';

let app: IUnleashTest;
let db: ITestDb;
let eventStore: IEventStore;

beforeAll(async () => {
db = await dbInit('dependent_features', getLogger);
Expand All @@ -24,8 +31,14 @@ beforeAll(async () => {
},
db.rawDatabase,
);
eventStore = db.stores.eventStore;
});

const getRecordedEventTypesForDependencies = async () =>
(await eventStore.getEvents())
.map((event) => event.type)
.filter((type) => type.includes('depend'));

afterAll(async () => {
await app.destroy();
await db.destroy();
Expand Down Expand Up @@ -95,6 +108,13 @@ test('should add and delete feature dependencies', async () => {

await deleteFeatureDependency(child, parent); // single
await deleteFeatureDependencies(child); // all

expect(await getRecordedEventTypesForDependencies()).toStrictEqual([
FEATURE_DEPENDENCIES_REMOVED,
FEATURE_DEPENDENCY_REMOVED,
FEATURE_DEPENDENCY_ADDED,
FEATURE_DEPENDENCY_ADDED,
]);
});

test('should not allow to add a parent dependency to a feature that already has children', async () => {
Expand Down
6 changes: 3 additions & 3 deletions src/lib/services/index.ts
Expand Up @@ -328,10 +328,10 @@ export const createServices = (
const eventAnnouncerService = new EventAnnouncerService(stores, config);

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

return {
accessService,
Expand Down
7 changes: 7 additions & 0 deletions src/lib/types/events.ts
Expand Up @@ -9,6 +9,10 @@ export const APPLICATION_CREATED = 'application-created' as const;
export const FEATURE_CREATED = 'feature-created' as const;
export const FEATURE_DELETED = 'feature-deleted' as const;
export const FEATURE_UPDATED = 'feature-updated' as const;
export const FEATURE_DEPENDENCY_ADDED = 'feature-dependency-added' as const;
export const FEATURE_DEPENDENCY_REMOVED = 'feature-dependency-removed' as const;
export const FEATURE_DEPENDENCIES_REMOVED =
'feature-dependencies-removed' as const;
export const FEATURE_METADATA_UPDATED = 'feature-metadata-updated' as const;
export const FEATURE_VARIANTS_UPDATED = 'feature-variants-updated' as const;
export const FEATURE_ENVIRONMENT_VARIANTS_UPDATED =
Expand Down Expand Up @@ -249,6 +253,9 @@ export const IEventTypes = [
SERVICE_ACCOUNT_DELETED,
SERVICE_ACCOUNT_UPDATED,
FEATURE_POTENTIALLY_STALE_ON,
FEATURE_DEPENDENCY_ADDED,
FEATURE_DEPENDENCY_REMOVED,
FEATURE_DEPENDENCIES_REMOVED,
] as const;
export type IEventType = typeof IEventTypes[number];

Expand Down
3 changes: 2 additions & 1 deletion src/test/e2e/api/client/feature.e2e.test.ts
Expand Up @@ -62,8 +62,9 @@ beforeAll(async () => {
);
// depend on enabled feature with variant
await app.services.dependentFeaturesService.upsertFeatureDependency(
'featureY',
{ child: 'featureY', projectId: 'default' },
{ feature: 'featureX', variants: ['featureXVariant'] },
'test',
);

await app.services.featureToggleServiceV2.archiveToggle(
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Expand Up @@ -7899,10 +7899,10 @@ universalify@^0.1.0:
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==

unleash-client@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-4.1.1.tgz#ad3e90853f98885bbb4746af813514e6d1e7dee9"
integrity sha512-cliJJ82unQauip8/7TQhJbvuHMgBIrM167672uV5RmeD7buluAHm1x0BmYjqsXMpE3MX06m05EzpRz62H90puQ==
unleash-client@4.2.0-beta.0:
version "4.2.0-beta.0"
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-4.2.0-beta.0.tgz#62d4615d1e55255696c09938a12a02224262279c"
integrity sha512-Rhq1ahtXU47FyMZJ1f3Wrjr7rpU5V0noGwfxMj9+79NoksiA9NcmqnP2qeZF0hmE3trLDk8q3hj7NmVIR6RjPA==
dependencies:
ip "^1.1.8"
make-fetch-happen "^10.2.1"
Expand Down

0 comments on commit fbc571d

Please sign in to comment.