mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-14 01:16:17 +02:00
feat: events for dependencies (#4864)
This commit is contained in:
parent
011aea226c
commit
fbc571dffc
@ -82,6 +82,7 @@ export const useDependentFeaturesApi = (project: string) => {
|
|||||||
makeRequest,
|
makeRequest,
|
||||||
setToastData,
|
setToastData,
|
||||||
formatUnknownError,
|
formatUnknownError,
|
||||||
|
project,
|
||||||
];
|
];
|
||||||
return {
|
return {
|
||||||
addDependency: useCallback(addDependency, callbackDeps),
|
addDependency: useCallback(addDependency, callbackDeps),
|
||||||
|
@ -161,7 +161,7 @@
|
|||||||
"stoppable": "^1.1.0",
|
"stoppable": "^1.1.0",
|
||||||
"ts-toolbelt": "^9.6.0",
|
"ts-toolbelt": "^9.6.0",
|
||||||
"type-is": "^1.6.18",
|
"type-is": "^1.6.18",
|
||||||
"unleash-client": "4.1.1",
|
"unleash-client": "4.2.0-beta.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -4,24 +4,53 @@ import { DependentFeaturesStore } from './dependent-features-store';
|
|||||||
import { DependentFeaturesReadModel } from './dependent-features-read-model';
|
import { DependentFeaturesReadModel } from './dependent-features-read-model';
|
||||||
import { FakeDependentFeaturesStore } from './fake-dependent-features-store';
|
import { FakeDependentFeaturesStore } from './fake-dependent-features-store';
|
||||||
import { FakeDependentFeaturesReadModel } from './fake-dependent-features-read-model';
|
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 = (
|
export const createDependentFeaturesService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
|
config: IUnleashConfig,
|
||||||
): DependentFeaturesService => {
|
): 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 dependentFeaturesStore = new DependentFeaturesStore(db);
|
||||||
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
|
const dependentFeaturesReadModel = new DependentFeaturesReadModel(db);
|
||||||
return new DependentFeaturesService(
|
return new DependentFeaturesService(
|
||||||
dependentFeaturesStore,
|
dependentFeaturesStore,
|
||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
|
eventService,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeDependentFeaturesService =
|
export const createFakeDependentFeaturesService = (
|
||||||
(): DependentFeaturesService => {
|
config: IUnleashConfig,
|
||||||
const dependentFeaturesStore = new FakeDependentFeaturesStore();
|
): DependentFeaturesService => {
|
||||||
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
const eventStore = new FakeEventStore();
|
||||||
return new DependentFeaturesService(
|
const featureTagStore = new FakeFeatureTagStore();
|
||||||
dependentFeaturesStore,
|
const eventService = new EventService(
|
||||||
dependentFeaturesReadModel,
|
{
|
||||||
);
|
eventStore,
|
||||||
};
|
featureTagStore,
|
||||||
|
},
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
const dependentFeaturesStore = new FakeDependentFeaturesStore();
|
||||||
|
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
||||||
|
return new DependentFeaturesService(
|
||||||
|
dependentFeaturesStore,
|
||||||
|
dependentFeaturesReadModel,
|
||||||
|
eventService,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -21,12 +21,17 @@ import { IAuthRequest } from '../../routes/unleash-types';
|
|||||||
import { InvalidOperationError } from '../../error';
|
import { InvalidOperationError } from '../../error';
|
||||||
import { DependentFeaturesService } from './dependent-features-service';
|
import { DependentFeaturesService } from './dependent-features-service';
|
||||||
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
import { TransactionCreator, UnleashTransaction } from '../../db/transaction';
|
||||||
|
import { extractUsernameFromUser } from '../../util';
|
||||||
|
|
||||||
interface FeatureParams {
|
interface ProjectParams {
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FeatureParams extends ProjectParams {
|
||||||
child: string;
|
child: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DeleteDependencyParams {
|
interface DeleteDependencyParams extends ProjectParams {
|
||||||
child: string;
|
child: string;
|
||||||
parent: string;
|
parent: string;
|
||||||
}
|
}
|
||||||
@ -167,18 +172,22 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
|
req: IAuthRequest<FeatureParams, any, CreateDependentFeatureSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { child } = req.params;
|
const { child, projectId } = req.params;
|
||||||
const { variants, enabled, feature } = req.body;
|
const { variants, enabled, feature } = req.body;
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
await this.startTransaction(async (tx) =>
|
await this.startTransaction(async (tx) =>
|
||||||
this.transactionalDependentFeaturesService(
|
this.transactionalDependentFeaturesService(
|
||||||
tx,
|
tx,
|
||||||
).upsertFeatureDependency(child, {
|
).upsertFeatureDependency(
|
||||||
variants,
|
{ child, projectId },
|
||||||
enabled,
|
{
|
||||||
feature,
|
variants,
|
||||||
}),
|
enabled,
|
||||||
|
feature,
|
||||||
|
},
|
||||||
|
extractUsernameFromUser(req.user),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
} else {
|
} else {
|
||||||
@ -192,13 +201,17 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
req: IAuthRequest<DeleteDependencyParams, any, any>,
|
req: IAuthRequest<DeleteDependencyParams, any, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { child, parent } = req.params;
|
const { child, parent, projectId } = req.params;
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
await this.dependentFeaturesService.deleteFeatureDependency({
|
await this.dependentFeaturesService.deleteFeatureDependency(
|
||||||
parent,
|
{
|
||||||
child,
|
parent,
|
||||||
});
|
child,
|
||||||
|
},
|
||||||
|
projectId,
|
||||||
|
extractUsernameFromUser(req.user),
|
||||||
|
);
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
} else {
|
} else {
|
||||||
throw new InvalidOperationError(
|
throw new InvalidOperationError(
|
||||||
@ -211,11 +224,13 @@ export default class DependentFeaturesController extends Controller {
|
|||||||
req: IAuthRequest<FeatureParams, any, any>,
|
req: IAuthRequest<FeatureParams, any, any>,
|
||||||
res: Response,
|
res: Response,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { child } = req.params;
|
const { child, projectId } = req.params;
|
||||||
|
|
||||||
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
if (this.config.flagResolver.isEnabled('dependentFeatures')) {
|
||||||
await this.dependentFeaturesService.deleteFeatureDependencies(
|
await this.dependentFeaturesService.deleteFeatureDependencies(
|
||||||
child,
|
child,
|
||||||
|
projectId,
|
||||||
|
extractUsernameFromUser(req.user),
|
||||||
);
|
);
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
} else {
|
} else {
|
||||||
|
@ -3,23 +3,29 @@ import { CreateDependentFeatureSchema } from '../../openapi';
|
|||||||
import { IDependentFeaturesStore } from './dependent-features-store-type';
|
import { IDependentFeaturesStore } from './dependent-features-store-type';
|
||||||
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
|
import { FeatureDependency, FeatureDependencyId } from './dependent-features';
|
||||||
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
import { IDependentFeaturesReadModel } from './dependent-features-read-model-type';
|
||||||
|
import { EventService } from '../../services';
|
||||||
|
|
||||||
export class DependentFeaturesService {
|
export class DependentFeaturesService {
|
||||||
private dependentFeaturesStore: IDependentFeaturesStore;
|
private dependentFeaturesStore: IDependentFeaturesStore;
|
||||||
|
|
||||||
private dependentFeaturesReadModel: IDependentFeaturesReadModel;
|
private dependentFeaturesReadModel: IDependentFeaturesReadModel;
|
||||||
|
|
||||||
|
private eventService: EventService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
dependentFeaturesStore: IDependentFeaturesStore,
|
dependentFeaturesStore: IDependentFeaturesStore,
|
||||||
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
||||||
|
eventService: EventService,
|
||||||
) {
|
) {
|
||||||
this.dependentFeaturesStore = dependentFeaturesStore;
|
this.dependentFeaturesStore = dependentFeaturesStore;
|
||||||
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
||||||
|
this.eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
async upsertFeatureDependency(
|
async upsertFeatureDependency(
|
||||||
child: string,
|
{ child, projectId }: { child: string; projectId: string },
|
||||||
dependentFeature: CreateDependentFeatureSchema,
|
dependentFeature: CreateDependentFeatureSchema,
|
||||||
|
user: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { enabled, feature: parent, variants } = dependentFeature;
|
const { enabled, feature: parent, variants } = dependentFeature;
|
||||||
|
|
||||||
@ -46,16 +52,46 @@ export class DependentFeaturesService {
|
|||||||
variants,
|
variants,
|
||||||
};
|
};
|
||||||
await this.dependentFeaturesStore.upsert(featureDependency);
|
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(
|
async deleteFeatureDependency(
|
||||||
dependency: FeatureDependencyId,
|
dependency: FeatureDependencyId,
|
||||||
|
projectId: string,
|
||||||
|
user: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.dependentFeaturesStore.delete(dependency);
|
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.dependentFeaturesStore.deleteAll(feature);
|
||||||
|
await this.eventService.storeEvent({
|
||||||
|
type: 'feature-dependencies-removed',
|
||||||
|
project: projectId,
|
||||||
|
featureName: feature,
|
||||||
|
createdBy: user,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getParentOptions(feature: string): Promise<string[]> {
|
async getParentOptions(feature: string): Promise<string[]> {
|
||||||
|
@ -6,9 +6,16 @@ import {
|
|||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import { CreateDependentFeatureSchema } from '../../openapi';
|
import { CreateDependentFeatureSchema } from '../../openapi';
|
||||||
|
import {
|
||||||
|
FEATURE_DEPENDENCIES_REMOVED,
|
||||||
|
FEATURE_DEPENDENCY_ADDED,
|
||||||
|
FEATURE_DEPENDENCY_REMOVED,
|
||||||
|
IEventStore,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
let app: IUnleashTest;
|
let app: IUnleashTest;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
|
let eventStore: IEventStore;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('dependent_features', getLogger);
|
db = await dbInit('dependent_features', getLogger);
|
||||||
@ -24,8 +31,14 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
eventStore = db.stores.eventStore;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getRecordedEventTypesForDependencies = async () =>
|
||||||
|
(await eventStore.getEvents())
|
||||||
|
.map((event) => event.type)
|
||||||
|
.filter((type) => type.includes('depend'));
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
await app.destroy();
|
await app.destroy();
|
||||||
await db.destroy();
|
await db.destroy();
|
||||||
@ -95,6 +108,13 @@ test('should add and delete feature dependencies', async () => {
|
|||||||
|
|
||||||
await deleteFeatureDependency(child, parent); // single
|
await deleteFeatureDependency(child, parent); // single
|
||||||
await deleteFeatureDependencies(child); // all
|
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 () => {
|
test('should not allow to add a parent dependency to a feature that already has children', async () => {
|
||||||
|
@ -328,10 +328,10 @@ export const createServices = (
|
|||||||
const eventAnnouncerService = new EventAnnouncerService(stores, config);
|
const eventAnnouncerService = new EventAnnouncerService(stores, config);
|
||||||
|
|
||||||
const dependentFeaturesService = db
|
const dependentFeaturesService = db
|
||||||
? createDependentFeaturesService(db)
|
? createDependentFeaturesService(db, config)
|
||||||
: createFakeDependentFeaturesService();
|
: createFakeDependentFeaturesService(config);
|
||||||
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
|
const transactionalDependentFeaturesService = (txDb: Knex.Transaction) =>
|
||||||
createDependentFeaturesService(txDb);
|
createDependentFeaturesService(txDb, config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
|
@ -9,6 +9,10 @@ export const APPLICATION_CREATED = 'application-created' as const;
|
|||||||
export const FEATURE_CREATED = 'feature-created' as const;
|
export const FEATURE_CREATED = 'feature-created' as const;
|
||||||
export const FEATURE_DELETED = 'feature-deleted' as const;
|
export const FEATURE_DELETED = 'feature-deleted' as const;
|
||||||
export const FEATURE_UPDATED = 'feature-updated' 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_METADATA_UPDATED = 'feature-metadata-updated' as const;
|
||||||
export const FEATURE_VARIANTS_UPDATED = 'feature-variants-updated' as const;
|
export const FEATURE_VARIANTS_UPDATED = 'feature-variants-updated' as const;
|
||||||
export const FEATURE_ENVIRONMENT_VARIANTS_UPDATED =
|
export const FEATURE_ENVIRONMENT_VARIANTS_UPDATED =
|
||||||
@ -249,6 +253,9 @@ export const IEventTypes = [
|
|||||||
SERVICE_ACCOUNT_DELETED,
|
SERVICE_ACCOUNT_DELETED,
|
||||||
SERVICE_ACCOUNT_UPDATED,
|
SERVICE_ACCOUNT_UPDATED,
|
||||||
FEATURE_POTENTIALLY_STALE_ON,
|
FEATURE_POTENTIALLY_STALE_ON,
|
||||||
|
FEATURE_DEPENDENCY_ADDED,
|
||||||
|
FEATURE_DEPENDENCY_REMOVED,
|
||||||
|
FEATURE_DEPENDENCIES_REMOVED,
|
||||||
] as const;
|
] as const;
|
||||||
export type IEventType = typeof IEventTypes[number];
|
export type IEventType = typeof IEventTypes[number];
|
||||||
|
|
||||||
|
@ -62,8 +62,9 @@ beforeAll(async () => {
|
|||||||
);
|
);
|
||||||
// depend on enabled feature with variant
|
// depend on enabled feature with variant
|
||||||
await app.services.dependentFeaturesService.upsertFeatureDependency(
|
await app.services.dependentFeaturesService.upsertFeatureDependency(
|
||||||
'featureY',
|
{ child: 'featureY', projectId: 'default' },
|
||||||
{ feature: 'featureX', variants: ['featureXVariant'] },
|
{ feature: 'featureX', variants: ['featureXVariant'] },
|
||||||
|
'test',
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.services.featureToggleServiceV2.archiveToggle(
|
await app.services.featureToggleServiceV2.archiveToggle(
|
||||||
|
@ -7899,10 +7899,10 @@ universalify@^0.1.0:
|
|||||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66"
|
||||||
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==
|
||||||
|
|
||||||
unleash-client@4.1.1:
|
unleash-client@4.2.0-beta.0:
|
||||||
version "4.1.1"
|
version "4.2.0-beta.0"
|
||||||
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-4.1.1.tgz#ad3e90853f98885bbb4746af813514e6d1e7dee9"
|
resolved "https://registry.yarnpkg.com/unleash-client/-/unleash-client-4.2.0-beta.0.tgz#62d4615d1e55255696c09938a12a02224262279c"
|
||||||
integrity sha512-cliJJ82unQauip8/7TQhJbvuHMgBIrM167672uV5RmeD7buluAHm1x0BmYjqsXMpE3MX06m05EzpRz62H90puQ==
|
integrity sha512-Rhq1ahtXU47FyMZJ1f3Wrjr7rpU5V0noGwfxMj9+79NoksiA9NcmqnP2qeZF0hmE3trLDk8q3hj7NmVIR6RjPA==
|
||||||
dependencies:
|
dependencies:
|
||||||
ip "^1.1.8"
|
ip "^1.1.8"
|
||||||
make-fetch-happen "^10.2.1"
|
make-fetch-happen "^10.2.1"
|
||||||
|
Loading…
Reference in New Issue
Block a user