mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
feat: feature collaborators added to API behind a flag (#7627)
This commit is contained in:
parent
203b700e27
commit
71b3a2ae0a
@ -55,6 +55,8 @@ import { createEventsService } from '../events/createEventsService';
|
|||||||
import { EventEmitter } from 'stream';
|
import { EventEmitter } from 'stream';
|
||||||
import { FeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model';
|
import { FeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model';
|
||||||
import { FakeFeatureLifecycleReadModel } from '../feature-lifecycle/fake-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 = (
|
export const createFeatureToggleService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -131,6 +133,8 @@ export const createFeatureToggleService = (
|
|||||||
|
|
||||||
const dependentFeaturesService = createDependentFeaturesService(config)(db);
|
const dependentFeaturesService = createDependentFeaturesService(config)(db);
|
||||||
|
|
||||||
|
const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db);
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
@ -151,6 +155,7 @@ export const createFeatureToggleService = (
|
|||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
|
featureCollaboratorsReadModel,
|
||||||
);
|
);
|
||||||
return featureToggleService;
|
return featureToggleService;
|
||||||
};
|
};
|
||||||
@ -192,6 +197,8 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
const dependentFeaturesReadModel = new FakeDependentFeaturesReadModel();
|
||||||
const dependentFeaturesService = createFakeDependentFeaturesService(config);
|
const dependentFeaturesService = createFakeDependentFeaturesService(config);
|
||||||
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
|
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
|
||||||
|
const featureCollaboratorsReadModel =
|
||||||
|
new FakeFeatureCollaboratorsReadModel();
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
{
|
{
|
||||||
@ -218,6 +225,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
|
featureCollaboratorsReadModel,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
|
@ -705,6 +705,19 @@ export default class ProjectFeaturesController extends Controller {
|
|||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...feature,
|
...feature,
|
||||||
|
...(feature.collaborators
|
||||||
|
? {
|
||||||
|
collaborators: {
|
||||||
|
...feature.collaborators,
|
||||||
|
users: feature.collaborators.users.map(
|
||||||
|
(user) => ({
|
||||||
|
...user,
|
||||||
|
name: anonymise(user.name),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
createdBy: {
|
createdBy: {
|
||||||
...feature.createdBy,
|
...feature.createdBy,
|
||||||
name: anonymise(feature.createdBy?.name),
|
name: anonymise(feature.createdBy?.name),
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
type IAuditUser,
|
type IAuditUser,
|
||||||
type IConstraint,
|
type IConstraint,
|
||||||
type IDependency,
|
type IDependency,
|
||||||
|
type IFeatureCollaboratorsReadModel,
|
||||||
type IFeatureEnvironmentInfo,
|
type IFeatureEnvironmentInfo,
|
||||||
type IFeatureEnvironmentStore,
|
type IFeatureEnvironmentStore,
|
||||||
type IFeatureLifecycleStage,
|
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 { IFeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycle-read-model-type';
|
||||||
import type { ResourceLimitsSchema } from '../../openapi';
|
import type { ResourceLimitsSchema } from '../../openapi';
|
||||||
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||||
|
import type { Collaborator } from './types/feature-collaborators-read-model-type';
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -177,6 +179,8 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
private featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
private featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
|
|
||||||
|
private featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
||||||
|
|
||||||
private dependentFeaturesService: DependentFeaturesService;
|
private dependentFeaturesService: DependentFeaturesService;
|
||||||
|
|
||||||
private eventBus: EventEmitter;
|
private eventBus: EventEmitter;
|
||||||
@ -221,6 +225,7 @@ class FeatureToggleService {
|
|||||||
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
dependentFeaturesReadModel: IDependentFeaturesReadModel,
|
||||||
dependentFeaturesService: DependentFeaturesService,
|
dependentFeaturesService: DependentFeaturesService,
|
||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel,
|
featureLifecycleReadModel: IFeatureLifecycleReadModel,
|
||||||
|
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/feature-toggle-service.ts');
|
this.logger = getLogger('services/feature-toggle-service.ts');
|
||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureStrategiesStore = featureStrategiesStore;
|
||||||
@ -240,6 +245,7 @@ class FeatureToggleService {
|
|||||||
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
this.dependentFeaturesReadModel = dependentFeaturesReadModel;
|
||||||
this.dependentFeaturesService = dependentFeaturesService;
|
this.dependentFeaturesService = dependentFeaturesService;
|
||||||
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
||||||
|
this.featureCollaboratorsReadModel = featureCollaboratorsReadModel;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.resourceLimits = resourceLimits;
|
this.resourceLimits = resourceLimits;
|
||||||
}
|
}
|
||||||
@ -1088,10 +1094,19 @@ class FeatureToggleService {
|
|||||||
let dependencies: IDependency[] = [];
|
let dependencies: IDependency[] = [];
|
||||||
let children: string[] = [];
|
let children: string[] = [];
|
||||||
let lifecycle: IFeatureLifecycleStage | undefined = undefined;
|
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.getParents(featureName),
|
||||||
this.dependentFeaturesReadModel.getChildren([featureName]),
|
this.dependentFeaturesReadModel.getChildren([featureName]),
|
||||||
this.featureLifecycleReadModel.findCurrentStage(featureName),
|
this.featureLifecycleReadModel.findCurrentStage(featureName),
|
||||||
|
featureCollaboratorsEnabled
|
||||||
|
? this.featureCollaboratorsReadModel.getFeatureCollaborators(
|
||||||
|
featureName,
|
||||||
|
)
|
||||||
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (environmentVariants) {
|
if (environmentVariants) {
|
||||||
@ -1106,6 +1121,9 @@ class FeatureToggleService {
|
|||||||
dependencies,
|
dependencies,
|
||||||
children,
|
children,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
|
...(featureCollaboratorsEnabled
|
||||||
|
? { collaborators: { users: collaborators } }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const result =
|
const result =
|
||||||
@ -1119,6 +1137,9 @@ class FeatureToggleService {
|
|||||||
dependencies,
|
dependencies,
|
||||||
children,
|
children,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
|
...(featureCollaboratorsEnabled
|
||||||
|
? { collaborators: { users: collaborators } }
|
||||||
|
: {}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,14 +18,19 @@ let db: ITestDb;
|
|||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('feature_strategy_auth_api_serial', getLogger);
|
db = await dbInit('feature_strategy_auth_api_serial', getLogger);
|
||||||
app = await setupAppWithAuth(db.stores, {
|
app = await setupAppWithAuth(
|
||||||
|
db.stores,
|
||||||
|
{
|
||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
anonymiseEventLog: true,
|
anonymiseEventLog: true,
|
||||||
|
featureCollaborators: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
db.rawDatabase,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -140,7 +145,7 @@ test('Should not be possible auto-enable feature flag without CREATE_FEATURE_STR
|
|||||||
.expect(403);
|
.expect(403);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should read flag creator', async () => {
|
test('Should read flag creator and collaborators', async () => {
|
||||||
const email = 'user@getunleash.io';
|
const email = 'user@getunleash.io';
|
||||||
const url = '/api/admin/projects/default/features/';
|
const url = '/api/admin/projects/default/features/';
|
||||||
const name = 'creator.flag';
|
const name = 'creator.flag';
|
||||||
@ -153,10 +158,14 @@ test('Should read flag creator', async () => {
|
|||||||
TEST_AUDIT_USER,
|
TEST_AUDIT_USER,
|
||||||
);
|
);
|
||||||
|
|
||||||
await db.stores.featureToggleStore.create('default', {
|
await app.services.featureToggleService.createFeatureToggle(
|
||||||
|
'default',
|
||||||
|
{
|
||||||
name,
|
name,
|
||||||
createdByUserId: user.id,
|
createdByUserId: user.id,
|
||||||
});
|
},
|
||||||
|
{ id: user.id, username: 'irrelevant', ip: '::1' },
|
||||||
|
);
|
||||||
|
|
||||||
await app.request.post('/auth/demo/login').send({
|
await app.request.post('/auth/demo/login').send({
|
||||||
email,
|
email,
|
||||||
@ -166,10 +175,16 @@ test('Should read flag creator', async () => {
|
|||||||
.get(`${url}/${name}`)
|
.get(`${url}/${name}`)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(feature.createdBy).toEqual({
|
const expectedUser = {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: '3957b71c0@unleash.run',
|
name: '3957b71c0@unleash.run',
|
||||||
imageUrl:
|
imageUrl:
|
||||||
'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g',
|
'https://gravatar.com/avatar/3957b71c0a6d2528f03b423f432ed2efe855d263400f960248a1080493d9d68a?s=42&d=retro&r=g',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(feature.createdBy).toEqual(expectedUser);
|
||||||
|
|
||||||
|
expect(feature.collaborators).toStrictEqual({
|
||||||
|
users: [expectedUser],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -99,6 +99,7 @@ beforeAll(async () => {
|
|||||||
experimental: {
|
experimental: {
|
||||||
flags: {
|
flags: {
|
||||||
strictSchemaValidation: true,
|
strictSchemaValidation: true,
|
||||||
|
featureCollaborators: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
FEATURE_POTENTIALLY_STALE_ON,
|
FEATURE_POTENTIALLY_STALE_ON,
|
||||||
type IBaseEvent,
|
type IBaseEvent,
|
||||||
|
type IFeatureCollaboratorsReadModel,
|
||||||
type IUnleashConfig,
|
type IUnleashConfig,
|
||||||
type IUnleashStores,
|
type IUnleashStores,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
@ -68,6 +69,7 @@ test('Should only store events for potentially stale on', async () => {
|
|||||||
{} as IDependentFeaturesReadModel,
|
{} as IDependentFeaturesReadModel,
|
||||||
{} as DependentFeaturesService,
|
{} as DependentFeaturesService,
|
||||||
{} as IFeatureLifecycleReadModel,
|
{} as IFeatureLifecycleReadModel,
|
||||||
|
{} as IFeatureCollaboratorsReadModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
await featureToggleService.updatePotentiallyStaleFeatures();
|
await featureToggleService.updatePotentiallyStaleFeatures();
|
||||||
|
@ -135,6 +135,8 @@ import {
|
|||||||
createFakeApiTokenService,
|
createFakeApiTokenService,
|
||||||
} from '../features/api-tokens/createApiTokenService';
|
} from '../features/api-tokens/createApiTokenService';
|
||||||
import { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
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 = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -263,6 +265,10 @@ export const createServices = (
|
|||||||
? createFeatureSearchService(config)(db)
|
? createFeatureSearchService(config)(db)
|
||||||
: createFakeFeatureSearchService(config);
|
: createFakeFeatureSearchService(config);
|
||||||
|
|
||||||
|
const featureCollaboratorsReadModel = db
|
||||||
|
? new FeatureCollaboratorsReadModel(db)
|
||||||
|
: new FakeFeatureCollaboratorsReadModel();
|
||||||
|
|
||||||
const featureToggleServiceV2 = new FeatureToggleService(
|
const featureToggleServiceV2 = new FeatureToggleService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
@ -274,6 +280,7 @@ export const createServices = (
|
|||||||
dependentFeaturesReadModel,
|
dependentFeaturesReadModel,
|
||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
|
featureCollaboratorsReadModel,
|
||||||
);
|
);
|
||||||
const transactionalEnvironmentService = db
|
const transactionalEnvironmentService = db
|
||||||
? withTransactional(createEnvironmentService(config), db)
|
? withTransactional(createEnvironmentService(config), db)
|
||||||
|
@ -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 { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
||||||
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||||
import type { IFlagResolver } from './experimental';
|
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];
|
export type Operator = (typeof ALL_OPERATORS)[number];
|
||||||
|
|
||||||
@ -114,6 +115,7 @@ export interface FeatureToggleView extends FeatureToggleWithEnvironment {
|
|||||||
dependencies: IDependency[];
|
dependencies: IDependency[];
|
||||||
children: string[];
|
children: string[];
|
||||||
lifecycle: IFeatureLifecycleStage | undefined;
|
lifecycle: IFeatureLifecycleStage | undefined;
|
||||||
|
collaborators?: { users: Collaborator[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
// @deprecated
|
// @deprecated
|
||||||
|
Loading…
Reference in New Issue
Block a user