1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-09 00:18:00 +01:00

feat: project environment added and removed events (#5459)

This commit is contained in:
Mateusz Kwasniewski 2023-11-28 12:58:30 +01:00 committed by GitHub
parent 9ed2c37b25
commit 2965daa195
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 76 additions and 13 deletions

View File

@ -18,11 +18,13 @@ import {
insertLastSeenAt, insertLastSeenAt,
insertFeatureEnvironmentsLastSeen, insertFeatureEnvironmentsLastSeen,
} from '../../../../test/e2e/helpers/test-helper'; } from '../../../../test/e2e/helpers/test-helper';
import { EventService } from '../../../services';
let stores: IUnleashStores; let stores: IUnleashStores;
let db; let db;
let service: FeatureToggleService; let service: FeatureToggleService;
let segmentService: ISegmentService; let segmentService: ISegmentService;
let eventService: EventService;
let environmentService: EnvironmentService; let environmentService: EnvironmentService;
let unleashConfig; let unleashConfig;
@ -48,6 +50,8 @@ beforeAll(async () => {
segmentService = createSegmentService(db.rawDatabase, config); segmentService = createSegmentService(db.rawDatabase, config);
service = createFeatureToggleService(db.rawDatabase, config); service = createFeatureToggleService(db.rawDatabase, config);
eventService = new EventService(stores, config);
}); });
afterAll(async () => { afterAll(async () => {
@ -253,13 +257,17 @@ test('adding and removing an environment preserves variants when variants per en
); );
//force the variantEnvironments flag off so that we can test legacy behavior //force the variantEnvironments flag off so that we can test legacy behavior
environmentService = new EnvironmentService(stores, { environmentService = new EnvironmentService(
stores,
{
...unleashConfig, ...unleashConfig,
flagResolver: { flagResolver: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
isEnabled: (toggleName: string) => false, isEnabled: (toggleName: string) => false,
}, },
}); },
eventService,
);
await environmentService.addEnvironmentToProject(prodEnv, 'default'); await environmentService.addEnvironmentToProject(prodEnv, 'default');
await environmentService.removeEnvironmentFromProject(prodEnv, 'default'); await environmentService.removeEnvironmentFromProject(prodEnv, 'default');

View File

@ -18,6 +18,8 @@ import {
ProjectEnvironmentSchema, ProjectEnvironmentSchema,
} from '../../../openapi'; } from '../../../openapi';
import { OpenApiService, ProjectService } from '../../../services'; import { OpenApiService, ProjectService } from '../../../services';
import { extractUsername } from '../../../util';
import { IAuthRequest } from '../../unleash-types';
const PREFIX = '/:projectId/environments'; const PREFIX = '/:projectId/environments';
@ -124,7 +126,7 @@ export default class EnvironmentsController extends Controller {
} }
async addEnvironmentToProject( async addEnvironmentToProject(
req: Request< req: IAuthRequest<
Omit<IProjectEnvironmentParams, 'environment'>, Omit<IProjectEnvironmentParams, 'environment'>,
void, void,
ProjectEnvironmentSchema ProjectEnvironmentSchema
@ -138,13 +140,14 @@ export default class EnvironmentsController extends Controller {
await this.environmentService.addEnvironmentToProject( await this.environmentService.addEnvironmentToProject(
environment, environment,
projectId, projectId,
extractUsername(req),
); );
res.status(200).end(); res.status(200).end();
} }
async removeEnvironmentFromProject( async removeEnvironmentFromProject(
req: Request<IProjectEnvironmentParams>, req: IAuthRequest<IProjectEnvironmentParams>,
res: Response<void>, res: Response<void>,
): Promise<void> { ): Promise<void> {
const { projectId, environment } = req.params; const { projectId, environment } = req.params;
@ -152,6 +155,7 @@ export default class EnvironmentsController extends Controller {
await this.environmentService.removeEnvironmentFromProject( await this.environmentService.removeEnvironmentFromProject(
environment, environment,
projectId, projectId,
extractUsername(req),
); );
res.status(200).end(); res.status(200).end();

View File

@ -7,6 +7,8 @@ import {
ISortOrder, ISortOrder,
IUnleashConfig, IUnleashConfig,
IUnleashStores, IUnleashStores,
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
} from '../types'; } from '../types';
import { Logger } from '../logger'; import { Logger } from '../logger';
import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../error'; import { BadDataError, UNIQUE_CONSTRAINT_VIOLATION } from '../error';
@ -17,6 +19,7 @@ import { IProjectStore } from 'lib/types/stores/project-store';
import MinimumOneEnvironmentError from '../error/minimum-one-environment-error'; import MinimumOneEnvironmentError from '../error/minimum-one-environment-error';
import { IFlagResolver } from 'lib/types/experimental'; import { IFlagResolver } from 'lib/types/experimental';
import { CreateFeatureStrategySchema } from '../openapi'; import { CreateFeatureStrategySchema } from '../openapi';
import EventService from './event-service';
export default class EnvironmentService { export default class EnvironmentService {
private logger: Logger; private logger: Logger;
@ -29,6 +32,8 @@ export default class EnvironmentService {
private featureEnvironmentStore: IFeatureEnvironmentStore; private featureEnvironmentStore: IFeatureEnvironmentStore;
private eventService: EventService;
private flagResolver: IFlagResolver; private flagResolver: IFlagResolver;
constructor( constructor(
@ -48,12 +53,14 @@ export default class EnvironmentService {
getLogger, getLogger,
flagResolver, flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>, }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
eventService: EventService,
) { ) {
this.logger = getLogger('services/environment-service.ts'); this.logger = getLogger('services/environment-service.ts');
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
this.featureStrategiesStore = featureStrategiesStore; this.featureStrategiesStore = featureStrategiesStore;
this.featureEnvironmentStore = featureEnvironmentStore; this.featureEnvironmentStore = featureEnvironmentStore;
this.projectStore = projectStore; this.projectStore = projectStore;
this.eventService = eventService;
this.flagResolver = flagResolver; this.flagResolver = flagResolver;
} }
@ -92,6 +99,7 @@ export default class EnvironmentService {
async addEnvironmentToProject( async addEnvironmentToProject(
environment: string, environment: string,
projectId: string, projectId: string,
username = 'unknown',
): Promise<void> { ): Promise<void> {
try { try {
await this.featureEnvironmentStore.connectProject( await this.featureEnvironmentStore.connectProject(
@ -102,6 +110,12 @@ export default class EnvironmentService {
environment, environment,
projectId, projectId,
); );
await this.eventService.storeEvent({
type: PROJECT_ENVIRONMENT_ADDED,
project: projectId,
environment,
createdBy: username,
});
} catch (e) { } catch (e) {
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) { if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
throw new NameExistsError( throw new NameExistsError(
@ -213,6 +227,7 @@ export default class EnvironmentService {
async removeEnvironmentFromProject( async removeEnvironmentFromProject(
environment: string, environment: string,
projectId: string, projectId: string,
username = 'unknown',
): Promise<void> { ): Promise<void> {
const projectEnvs = const projectEnvs =
await this.projectStore.getEnvironmentsForProject(projectId); await this.projectStore.getEnvironmentsForProject(projectId);
@ -222,6 +237,12 @@ export default class EnvironmentService {
environment, environment,
projectId, projectId,
); );
await this.eventService.storeEvent({
type: PROJECT_ENVIRONMENT_REMOVED,
project: projectId,
environment,
createdBy: username,
});
return; return;
} }
throw new MinimumOneEnvironmentError( throw new MinimumOneEnvironmentError(

View File

@ -222,7 +222,11 @@ export const createServices = (
dependentFeaturesReadModel, dependentFeaturesReadModel,
dependentFeaturesService, dependentFeaturesService,
); );
const environmentService = new EnvironmentService(stores, config); const environmentService = new EnvironmentService(
stores,
config,
eventService,
);
const featureTagService = new FeatureTagService( const featureTagService = new FeatureTagService(
stores, stores,
config, config,

View File

@ -108,6 +108,9 @@ export const GROUP_DELETED = 'group-deleted' as const;
export const SETTING_CREATED = 'setting-created' as const; export const SETTING_CREATED = 'setting-created' as const;
export const SETTING_UPDATED = 'setting-updated' as const; export const SETTING_UPDATED = 'setting-updated' as const;
export const SETTING_DELETED = 'setting-deleted' as const; export const SETTING_DELETED = 'setting-deleted' as const;
export const PROJECT_ENVIRONMENT_ADDED = 'project-environment-added' as const;
export const PROJECT_ENVIRONMENT_REMOVED =
'project-environment-removed' as const;
export const CLIENT_METRICS = 'client-metrics' as const; export const CLIENT_METRICS = 'client-metrics' as const;
export const CLIENT_REGISTER = 'client-register' as const; export const CLIENT_REGISTER = 'client-register' as const;
@ -292,6 +295,8 @@ export const IEventTypes = [
BANNER_CREATED, BANNER_CREATED,
BANNER_UPDATED, BANNER_UPDATED,
BANNER_DELETED, BANNER_DELETED,
PROJECT_ENVIRONMENT_ADDED,
PROJECT_ENVIRONMENT_REMOVED,
] as const; ] as const;
export type IEventType = (typeof IEventTypes)[number]; export type IEventType = (typeof IEventTypes)[number];

View File

@ -4,16 +4,19 @@ import dbInit from '../helpers/database-init';
import NotFoundError from '../../../lib/error/notfound-error'; import NotFoundError from '../../../lib/error/notfound-error';
import { IUnleashStores } from '../../../lib/types'; import { IUnleashStores } from '../../../lib/types';
import NameExistsError from '../../../lib/error/name-exists-error'; import NameExistsError from '../../../lib/error/name-exists-error';
import { EventService } from '../../../lib/services';
let stores: IUnleashStores; let stores: IUnleashStores;
let db; let db;
let service: EnvironmentService; let service: EnvironmentService;
let eventService: EventService;
beforeAll(async () => { beforeAll(async () => {
const config = createTestConfig(); const config = createTestConfig();
db = await dbInit('environment_service_serial', config.getLogger); db = await dbInit('environment_service_serial', config.getLogger);
stores = db.stores; stores = db.stores;
service = new EnvironmentService(stores, config); eventService = new EventService(stores, config);
service = new EnvironmentService(stores, config, eventService);
}); });
afterAll(async () => { afterAll(async () => {
await db.destroy(); await db.destroy();
@ -50,7 +53,7 @@ test('Can connect environment to project', async () => {
description: '', description: '',
stale: false, stale: false,
}); });
await service.addEnvironmentToProject('test-connection', 'default'); await service.addEnvironmentToProject('test-connection', 'default', 'user');
const overview = await stores.featureStrategiesStore.getFeatureOverview({ const overview = await stores.featureStrategiesStore.getFeatureOverview({
projectId: 'default', projectId: 'default',
}); });
@ -68,6 +71,13 @@ test('Can connect environment to project', async () => {
}, },
]); ]);
}); });
const { events } = await eventService.getEvents();
expect(events[0]).toMatchObject({
type: 'project-environment-added',
project: 'default',
environment: 'test-connection',
createdBy: 'user',
});
}); });
test('Can remove environment from project', async () => { test('Can remove environment from project', async () => {
@ -98,7 +108,11 @@ test('Can remove environment from project', async () => {
}, },
]); ]);
}); });
await service.removeEnvironmentFromProject('removal-test', 'default'); await service.removeEnvironmentFromProject(
'removal-test',
'default',
'user',
);
overview = await stores.featureStrategiesStore.getFeatureOverview({ overview = await stores.featureStrategiesStore.getFeatureOverview({
projectId: 'default', projectId: 'default',
}); });
@ -106,6 +120,13 @@ test('Can remove environment from project', async () => {
overview.forEach((o) => { overview.forEach((o) => {
expect(o.environments).toEqual([]); expect(o.environments).toEqual([]);
}); });
const { events } = await eventService.getEvents();
expect(events[0]).toMatchObject({
type: 'project-environment-removed',
project: 'default',
environment: 'removal-test',
createdBy: 'user',
});
}); });
test('Adding same environment twice should throw a NameExistsError', async () => { test('Adding same environment twice should throw a NameExistsError', async () => {

View File

@ -64,7 +64,7 @@ beforeAll(async () => {
featureToggleService = createFeatureToggleService(db.rawDatabase, config); featureToggleService = createFeatureToggleService(db.rawDatabase, config);
environmentService = new EnvironmentService(stores, config); environmentService = new EnvironmentService(stores, config, eventService);
projectService = createProjectService(db.rawDatabase, config); projectService = createProjectService(db.rawDatabase, config);
}); });