mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Fix/switch project endpoint (#923)
This commit is contained in:
parent
bb47c19d4d
commit
856c7a358b
@ -17,7 +17,10 @@ import {
|
||||
} from '../types/events';
|
||||
import { GLOBAL_ENV } from '../types/environment';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import { FeatureConfigurationClient, IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
|
||||
import {
|
||||
FeatureConfigurationClient,
|
||||
IFeatureStrategiesStore,
|
||||
} from '../types/stores/feature-strategies-store';
|
||||
import { IFeatureTypeStore } from '../types/stores/feature-type-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import { IEnvironmentStore } from '../types/stores/environment-store';
|
||||
@ -95,16 +98,15 @@ class FeatureToggleServiceV2 {
|
||||
environment: string = GLOBAL_ENV,
|
||||
): Promise<IStrategyConfig> {
|
||||
try {
|
||||
const newFeatureStrategy = await this.featureStrategiesStore.createStrategyConfig(
|
||||
{
|
||||
const newFeatureStrategy =
|
||||
await this.featureStrategiesStore.createStrategyConfig({
|
||||
strategyName: strategyConfig.name,
|
||||
constraints: strategyConfig.constraints,
|
||||
parameters: strategyConfig.parameters,
|
||||
projectName,
|
||||
featureName,
|
||||
environment,
|
||||
},
|
||||
);
|
||||
});
|
||||
return {
|
||||
id: newFeatureStrategy.id,
|
||||
name: newFeatureStrategy.strategyName,
|
||||
@ -150,7 +152,8 @@ class FeatureToggleServiceV2 {
|
||||
featureName,
|
||||
);
|
||||
if (hasEnv) {
|
||||
const featureStrategies = await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
const featureStrategies =
|
||||
await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
projectName,
|
||||
featureName,
|
||||
environment,
|
||||
@ -176,7 +179,10 @@ class FeatureToggleServiceV2 {
|
||||
featureName: string,
|
||||
archived: boolean = false,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, archived);
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(
|
||||
featureName,
|
||||
archived,
|
||||
);
|
||||
}
|
||||
|
||||
async getClientFeatures(
|
||||
@ -203,7 +209,10 @@ class FeatureToggleServiceV2 {
|
||||
async getFeatureToggle(
|
||||
featureName: string,
|
||||
): Promise<FeatureToggleWithEnvironment> {
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, false);
|
||||
return this.featureStrategiesStore.getFeatureToggleAdmin(
|
||||
featureName,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async createFeatureToggle(
|
||||
@ -215,7 +224,9 @@ class FeatureToggleServiceV2 {
|
||||
await this.validateName(value.name);
|
||||
const exists = await this.projectStore.hasProject(projectId);
|
||||
if (exists) {
|
||||
const featureData = await featureMetadataSchema.validateAsync(value);
|
||||
const featureData = await featureMetadataSchema.validateAsync(
|
||||
value,
|
||||
);
|
||||
const createdToggle = await this.featureToggleStore.createFeature(
|
||||
projectId,
|
||||
featureData,
|
||||
@ -250,7 +261,8 @@ class FeatureToggleServiceV2 {
|
||||
projectId,
|
||||
updatedFeature,
|
||||
);
|
||||
const tags = (await this.featureTagStore.getAllTagsForFeature(
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(
|
||||
updatedFeature.name,
|
||||
)) || [];
|
||||
await this.eventStore.store({
|
||||
@ -296,11 +308,13 @@ class FeatureToggleServiceV2 {
|
||||
environment: string,
|
||||
featureName: string,
|
||||
): Promise<IFeatureEnvironmentInfo> {
|
||||
const envMetadata = await this.featureEnvironmentStore.getEnvironmentMetaData(
|
||||
const envMetadata =
|
||||
await this.featureEnvironmentStore.getEnvironmentMetaData(
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
const strategies = await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
const strategies =
|
||||
await this.featureStrategiesStore.getStrategiesForFeature(
|
||||
project,
|
||||
featureName,
|
||||
environment,
|
||||
@ -321,7 +335,10 @@ class FeatureToggleServiceV2 {
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
await this.projectStore.deleteEnvironmentForProject(projectId, environment);
|
||||
await this.projectStore.deleteEnvironmentForProject(
|
||||
projectId,
|
||||
environment,
|
||||
);
|
||||
}
|
||||
|
||||
/** Validations */
|
||||
@ -358,8 +375,9 @@ class FeatureToggleServiceV2 {
|
||||
);
|
||||
feature.stale = isStale;
|
||||
await this.featureToggleStore.updateFeature(feature.project, feature);
|
||||
const tags = (await this.featureTagStore.getAllTagsForFeature(featureName))
|
||||
|| [];
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(featureName)) ||
|
||||
[];
|
||||
|
||||
await this.eventStore.store({
|
||||
type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF,
|
||||
@ -373,7 +391,8 @@ class FeatureToggleServiceV2 {
|
||||
async archiveToggle(name: string, userName: string): Promise<void> {
|
||||
await this.featureToggleStore.hasFeature(name);
|
||||
await this.featureToggleStore.archiveFeature(name);
|
||||
const tags = (await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(name)) || [];
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_ARCHIVED,
|
||||
createdBy: userName,
|
||||
@ -388,12 +407,14 @@ class FeatureToggleServiceV2 {
|
||||
enabled: boolean,
|
||||
userName: string,
|
||||
): Promise<FeatureToggle> {
|
||||
const hasEnvironment = await this.featureEnvironmentStore.featureHasEnvironment(
|
||||
const hasEnvironment =
|
||||
await this.featureEnvironmentStore.featureHasEnvironment(
|
||||
environment,
|
||||
featureName,
|
||||
);
|
||||
if (hasEnvironment) {
|
||||
const newEnabled = await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus(
|
||||
const newEnabled =
|
||||
await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus(
|
||||
environment,
|
||||
featureName,
|
||||
enabled,
|
||||
@ -401,7 +422,8 @@ class FeatureToggleServiceV2 {
|
||||
const feature = await this.featureToggleStore.getFeatureMetadata(
|
||||
featureName,
|
||||
);
|
||||
const tags = (await this.featureTagStore.getAllTagsForFeature(
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
)) || [];
|
||||
await this.eventStore.store({
|
||||
@ -424,7 +446,8 @@ class FeatureToggleServiceV2 {
|
||||
userName: string,
|
||||
): Promise<FeatureToggle> {
|
||||
await this.featureToggleStore.hasFeature(featureName);
|
||||
const isEnabled = await this.featureEnvironmentStore.isEnvironmentEnabled(
|
||||
const isEnabled =
|
||||
await this.featureEnvironmentStore.isEnvironmentEnabled(
|
||||
featureName,
|
||||
environment,
|
||||
);
|
||||
@ -443,17 +466,19 @@ class FeatureToggleServiceV2 {
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
value: any,
|
||||
userName: string,
|
||||
event?: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.getFeatureMetadata(
|
||||
featureName,
|
||||
);
|
||||
feature[field] = value;
|
||||
await this.featureToggleStore.updateFeature(feature.project, feature);
|
||||
const tags = (await this.featureTagStore.getAllTagsForFeature(featureName))
|
||||
|| [];
|
||||
const tags =
|
||||
(await this.featureTagStore.getAllTagsForFeature(featureName)) ||
|
||||
[];
|
||||
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_UPDATED,
|
||||
type: event || FEATURE_UPDATED,
|
||||
createdBy: userName,
|
||||
data: feature,
|
||||
tags,
|
||||
@ -478,7 +503,9 @@ class FeatureToggleServiceV2 {
|
||||
|
||||
async reviveToggle(featureName: string, userName: string): Promise<void> {
|
||||
const data = await this.featureToggleStore.reviveFeature(featureName);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(featureName);
|
||||
const tags = await this.featureTagStore.getAllTagsForFeature(
|
||||
featureName,
|
||||
);
|
||||
await this.eventStore.store({
|
||||
type: FEATURE_REVIVED,
|
||||
createdBy: userName,
|
||||
@ -487,12 +514,16 @@ class FeatureToggleServiceV2 {
|
||||
});
|
||||
}
|
||||
|
||||
async getMetadataForAllFeatures(archived: boolean): Promise<FeatureToggle[]> {
|
||||
async getMetadataForAllFeatures(
|
||||
archived: boolean,
|
||||
): Promise<FeatureToggle[]> {
|
||||
return this.featureToggleStore.getFeatures(archived);
|
||||
}
|
||||
|
||||
async getProjectId(name: string): Promise<string> {
|
||||
const { project } = await this.featureToggleStore.getFeatureMetadata(name);
|
||||
const { project } = await this.featureToggleStore.getFeatureMetadata(
|
||||
name,
|
||||
);
|
||||
return project;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import { nameType } from '../routes/util';
|
||||
import schema from './project-schema';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
FEATURE_PROJECT_CHANGE,
|
||||
PROJECT_CREATED,
|
||||
PROJECT_DELETED,
|
||||
PROJECT_UPDATED,
|
||||
@ -27,6 +28,8 @@ import { IProjectStore } from '../types/stores/project-store';
|
||||
import { IRole } from '../types/stores/access-store';
|
||||
import { IEventStore } from '../types/stores/event-store';
|
||||
import FeatureToggleServiceV2 from './feature-toggle-service-v2';
|
||||
import { CREATE_FEATURE, UPDATE_FEATURE } from '../types/permissions';
|
||||
import NoAccessError from '../error/no-access-error';
|
||||
|
||||
const getCreatedBy = (user: User) => user.email || user.username;
|
||||
|
||||
@ -140,6 +143,45 @@ export default class ProjectService {
|
||||
});
|
||||
}
|
||||
|
||||
async changeProject(
|
||||
newProjectId: string,
|
||||
featureName: string,
|
||||
user: User,
|
||||
currentProjectId: string,
|
||||
): Promise<any> {
|
||||
const feature = await this.featureToggleStore.get(featureName);
|
||||
|
||||
if (feature.project !== currentProjectId) {
|
||||
throw new NoAccessError(UPDATE_FEATURE);
|
||||
}
|
||||
|
||||
const project = await this.getProject(newProjectId);
|
||||
|
||||
if (!project) {
|
||||
throw new NotFoundError(`Project ${newProjectId} not found`);
|
||||
}
|
||||
|
||||
const authorized = await this.accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE,
|
||||
newProjectId,
|
||||
);
|
||||
|
||||
if (!authorized) {
|
||||
throw new NoAccessError(CREATE_FEATURE);
|
||||
}
|
||||
|
||||
const updatedFeature = await this.featureToggleService.updateField(
|
||||
featureName,
|
||||
'project',
|
||||
newProjectId,
|
||||
user.username,
|
||||
FEATURE_PROJECT_CHANGE,
|
||||
);
|
||||
|
||||
return updatedFeature;
|
||||
}
|
||||
|
||||
async deleteProject(id: string, user: User): Promise<void> {
|
||||
if (id === DEFAULT_PROJECT) {
|
||||
throw new InvalidOperationError(
|
||||
|
@ -2,6 +2,7 @@ export const APPLICATION_CREATED = 'application-created';
|
||||
export const FEATURE_CREATED = 'feature-created';
|
||||
export const FEATURE_DELETED = 'feature-deleted';
|
||||
export const FEATURE_UPDATED = 'feature-updated';
|
||||
export const FEATURE_PROJECT_CHANGE = 'feature-project-change';
|
||||
export const FEATURE_ARCHIVED = 'feature-archived';
|
||||
export const FEATURE_REVIVED = 'feature-revived';
|
||||
export const FEATURE_IMPORT = 'feature-import';
|
||||
|
@ -3,7 +3,11 @@ import getLogger from '../../fixtures/no-logger';
|
||||
import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2';
|
||||
import ProjectService from '../../../lib/services/project-service';
|
||||
import { AccessService } from '../../../lib/services/access-service';
|
||||
import { UPDATE_PROJECT } from '../../../lib/types/permissions';
|
||||
import {
|
||||
CREATE_FEATURE,
|
||||
UPDATE_FEATURE,
|
||||
UPDATE_PROJECT,
|
||||
} from '../../../lib/types/permissions';
|
||||
import NotFoundError from '../../../lib/error/notfound-error';
|
||||
import { createTestConfig } from '../../config/test-config';
|
||||
import { RoleName } from '../../../lib/types/model';
|
||||
@ -390,3 +394,119 @@ test('should not remove user from the project', async () => {
|
||||
new Error('A project must have at least one owner'),
|
||||
);
|
||||
});
|
||||
|
||||
test('should not change project if feature toggle project does not match current project id', async () => {
|
||||
const project = {
|
||||
id: 'test-change-project',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle' };
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
await projectService.changeProject(
|
||||
'newProject',
|
||||
toggle.name,
|
||||
user,
|
||||
'wrong-project-id',
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
`You need permission=${UPDATE_FEATURE} to perform this action`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should return 404 if no project is found with the project id', async () => {
|
||||
const project = {
|
||||
id: 'test-change-project-2',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle-2' };
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
await projectService.changeProject(
|
||||
'newProject',
|
||||
toggle.name,
|
||||
user,
|
||||
project.id,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(`No project found`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should fail if user is not authorized', async () => {
|
||||
const project = {
|
||||
id: 'test-change-project-3',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const projectDestination = {
|
||||
id: 'test-change-project-dest',
|
||||
name: 'New project 2',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle-3' };
|
||||
const projectAdmin1 = await stores.userStore.insert({
|
||||
name: 'test-change-project-creator',
|
||||
email: 'admin-change-project@getunleash.io',
|
||||
});
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
await projectService.createProject(projectDestination, projectAdmin1);
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
try {
|
||||
await projectService.changeProject(
|
||||
projectDestination.id,
|
||||
toggle.name,
|
||||
user,
|
||||
project.id,
|
||||
);
|
||||
} catch (err) {
|
||||
expect(err.message).toBe(
|
||||
`You need permission=${CREATE_FEATURE} to perform this action`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('should change project when checks pass', async () => {
|
||||
const project = {
|
||||
id: 'test-change-project-4',
|
||||
name: 'New project',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const projectDestination = {
|
||||
id: 'test-change-project-dest-2',
|
||||
name: 'New project 2',
|
||||
description: 'Blah',
|
||||
};
|
||||
|
||||
const toggle = { name: 'test-toggle-4' };
|
||||
|
||||
await projectService.createProject(project, user);
|
||||
await projectService.createProject(projectDestination, user);
|
||||
await featureToggleService.createFeatureToggle(project.id, toggle, user);
|
||||
|
||||
const updatedFeature = await projectService.changeProject(
|
||||
projectDestination.id,
|
||||
toggle.name,
|
||||
user,
|
||||
project.id,
|
||||
);
|
||||
|
||||
expect(updatedFeature.project).toBe(projectDestination.id);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user