1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: feature collaborators added to API behind a flag (#7627)

This commit is contained in:
Mateusz Kwasniewski 2024-07-19 14:34:22 +02:00 committed by GitHub
parent 203b700e27
commit 71b3a2ae0a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 82 additions and 13 deletions

View File

@ -55,6 +55,8 @@ import { createEventsService } from '../events/createEventsService';
import { EventEmitter } from 'stream';
import { FeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model';
import { FakeFeatureLifecycleReadModel } from '../feature-lifecycle/fake-feature-lifecycle-read-model';
import { FakeFeatureCollaboratorsReadModel } from './fake-feature-collaborators-read-model';
import { FeatureCollaboratorsReadModel } from './feature-collaborators-read-model';
export const createFeatureToggleService = (
db: Db,
@ -131,6 +133,8 @@ export const createFeatureToggleService = (
const dependentFeaturesService = createDependentFeaturesService(config)(db);
const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
@ -151,6 +155,7 @@ export const createFeatureToggleService = (
dependentFeaturesReadModel,
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
);
return featureToggleService;
};
@ -192,6 +197,8 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
const dependentFeaturesService = createFakeDependentFeaturesService(config);
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
const featureCollaboratorsReadModel =
new FakeFeatureCollaboratorsReadModel();
const featureToggleService = new FeatureToggleService(
{
@ -218,6 +225,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
dependentFeaturesReadModel,
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
);
return {
featureToggleService,

View File

@ -705,6 +705,19 @@ export default class ProjectFeaturesController extends Controller {
) {
return {
...feature,
...(feature.collaborators
? {
collaborators: {
...feature.collaborators,
users: feature.collaborators.users.map(
(user) => ({
...user,
name: anonymise(user.name),
}),
),
},
}
: {}),
createdBy: {
...feature.createdBy,
name: anonymise(feature.createdBy?.name),

View File

@ -20,6 +20,7 @@ import {
type IAuditUser,
type IConstraint,
type IDependency,
type IFeatureCollaboratorsReadModel,
type IFeatureEnvironmentInfo,
type IFeatureEnvironmentStore,
type IFeatureLifecycleStage,
@ -110,6 +111,7 @@ import type EventEmitter from 'node:events';
import type { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
import type { ResourceLimitsSchema } from '../../openapi';
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
import type { Collaborator } from './types/feature-collaborators-read-model-type';
interface IFeatureContext {
featureName: string;
@ -177,6 +179,8 @@ class FeatureToggleService {
private featureLifecycleReadModel: IFeatureLifecycleReadModel;
private featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
private dependentFeaturesService: DependentFeaturesService;
private eventBus: EventEmitter;
@ -221,6 +225,7 @@ class FeatureToggleService {
dependentFeaturesReadModel: IDependentFeaturesReadModel,
dependentFeaturesService: DependentFeaturesService,
featureLifecycleReadModel: IFeatureLifecycleReadModel,
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
@ -240,6 +245,7 @@ class FeatureToggleService {
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
this.dependentFeaturesService = dependentFeaturesService;
this.featureLifecycleReadModel = featureLifecycleReadModel;
this.featureCollaboratorsReadModel = featureCollaboratorsReadModel;
this.eventBus = eventBus;
this.resourceLimits = resourceLimits;
}
@ -1088,10 +1094,19 @@ class FeatureToggleService {
let dependencies: IDependency[] = [];
let children: string[] = [];
let lifecycle: IFeatureLifecycleStage | undefined = undefined;
[dependencies, children, lifecycle] = await Promise.all([
let collaborators: Collaborator[] = [];
const featureCollaboratorsEnabled = this.flagResolver.isEnabled(
'featureCollaborators',
);
[dependencies, children, lifecycle, collaborators] = await Promise.all([
this.dependentFeaturesReadModel.getParents(featureName),
this.dependentFeaturesReadModel.getChildren([featureName]),
this.featureLifecycleReadModel.findCurrentStage(featureName),
featureCollaboratorsEnabled
? this.featureCollaboratorsReadModel.getFeatureCollaborators(
featureName,
)
: Promise.resolve([]),
]);
if (environmentVariants) {
@ -1106,6 +1121,9 @@ class FeatureToggleService {
dependencies,
children,
lifecycle,
...(featureCollaboratorsEnabled
? { collaborators: { users: collaborators } }
: {}),
};
} else {
const result =
@ -1119,6 +1137,9 @@ class FeatureToggleService {
dependencies,
children,
lifecycle,
...(featureCollaboratorsEnabled
? { collaborators: { users: collaborators } }
: {}),
};
}
}

View File

@ -18,14 +18,19 @@ let db: ITestDb;
beforeAll(async () => {
db = await dbInit('feature_strategy_auth_api_serial', getLogger);
app = await setupAppWithAuth(db.stores, {
experimental: {
flags: {
strictSchemaValidation: true,
anonymiseEventLog: true,
app = await setupAppWithAuth(
db.stores,
{
experimental: {
flags: {
strictSchemaValidation: true,
anonymiseEventLog: true,
featureCollaborators: true,
},
},
},
});
db.rawDatabase,
);
});
afterEach(async () => {
@ -140,7 +145,7 @@ test('Should not be possible auto-enable feature flag without CREATE_FEATURE_STR
.expect(403);
});
test('Should read flag creator', async () => {
test('Should read flag creator and collaborators', async () => {
const email = 'user@getunleash.io';
const url = '/api/admin/projects/default/features/';
const name = 'creator.flag';
@ -153,10 +158,14 @@ test('Should read flag creator', async () => {
TEST_AUDIT_USER,
);
await db.stores.featureToggleStore.create('default', {
name,
createdByUserId: user.id,
});
await app.services.featureToggleService.createFeatureToggle(
'default',
{
name,
createdByUserId: user.id,
},
{ id: user.id, username: 'irrelevant', ip: '::1' },
);
await app.request.post('/auth/demo/login').send({
email,
@ -166,10 +175,16 @@ test('Should read flag creator', async () => {
.get(`${url}/${name}`)
.expect(200);
expect(feature.createdBy).toEqual({
const expectedUser = {
id: user.id,
name: '3957b71c0@unleash.run',
imageUrl:
'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g',
};
expect(feature.createdBy).toEqual(expectedUser);
expect(feature.collaborators).toStrictEqual({
users: [expectedUser],
});
});

View File

@ -99,6 +99,7 @@ beforeAll(async () => {
experimental: {
flags: {
strictSchemaValidation: true,
featureCollaborators: true,
},
},
},

View File

@ -1,6 +1,7 @@
import {
FEATURE_POTENTIALLY_STALE_ON,
type IBaseEvent,
type IFeatureCollaboratorsReadModel,
type IUnleashConfig,
type IUnleashStores,
} from '../types';
@ -68,6 +69,7 @@ test('Should only store events for potentially stale on', async () => {
{} as IDependentFeaturesReadModel,
{} as DependentFeaturesService,
{} as IFeatureLifecycleReadModel,
{} as IFeatureCollaboratorsReadModel,
);
await featureToggleService.updatePotentiallyStaleFeatures();

View File

@ -135,6 +135,8 @@ import {
createFakeApiTokenService,
} from '../features/api-tokens/createApiTokenService';
import { IntegrationEventsService } from '../features/integration-events/integration-events-service';
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
import { FakeFeatureCollaboratorsReadModel } from '../features/feature-toggle/fake-feature-collaborators-read-model';
export const createServices = (
stores: IUnleashStores,
@ -263,6 +265,10 @@ export const createServices = (
? createFeatureSearchService(config)(db)
: createFakeFeatureSearchService(config);
const featureCollaboratorsReadModel = db
? new FeatureCollaboratorsReadModel(db)
: new FakeFeatureCollaboratorsReadModel();
const featureToggleServiceV2 = new FeatureToggleService(
stores,
config,
@ -274,6 +280,7 @@ export const createServices = (
dependentFeaturesReadModel,
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
);
const transactionalEnvironmentService = db
? withTransactional(createEnvironmentService(config), db)

View File

@ -9,6 +9,7 @@ import type { ProjectEnvironment } from '../features/project/project-store-type'
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
import type { IFlagResolver } from './experimental';
import type { Collaborator } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
export type Operator = (typeof ALL_OPERATORS)[number];
@ -114,6 +115,7 @@ export interface FeatureToggleView extends FeatureToggleWithEnvironment {
dependencies: IDependency[];
children: string[];
lifecycle: IFeatureLifecycleStage | undefined;
collaborators?: { users: Collaborator[] };
}
// @deprecated