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

feat: project environments include visible property (#9427)

This commit is contained in:
Simon Hornby 2025-03-05 10:09:25 +02:00 committed by GitHub
parent ae0fc86e99
commit e7ac42080d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 91 additions and 42 deletions

View File

@ -19,8 +19,6 @@ const getStatusCode = (errorName: string): number => {
return 400;
case 'PasswordUndefinedError':
return 400;
case 'MinimumOneEnvironmentError':
return 400;
case 'InvalidTokenError':
return 401;
case 'UsedTokenError':

View File

@ -5,7 +5,6 @@ import FeatureHasTagError from './feature-has-tag-error';
import IncompatibleProjectError from './incompatible-project-error';
import InvalidOperationError from './invalid-operation-error';
import InvalidTokenError from './invalid-token-error';
import MinimumOneEnvironmentError from './minimum-one-environment-error';
import NameExistsError from './name-exists-error';
import PermissionError from './permission-error';
import { OperationDeniedError } from './operation-denied-error';
@ -27,7 +26,6 @@ export {
IncompatibleProjectError,
InvalidOperationError,
InvalidTokenError,
MinimumOneEnvironmentError,
NameExistsError,
PermissionError,
ForbiddenError,

View File

@ -8,7 +8,6 @@ export const UnleashApiErrorTypes = [
'IncompatibleProjectError',
'InvalidOperationError',
'InvalidTokenError',
'MinimumOneEnvironmentError',
'NameExistsError',
'NoAccessError',
'NotFoundError',

View File

@ -338,3 +338,54 @@ test('Override works correctly when enabling default and disabling prod and dev'
expect(targetedEnvironment?.enabled).toBe(true);
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
});
test('getProjectEnvironments also includes whether or not a given project is visible on a given environment', async () => {
const assertContains = (environments, envName, visible) => {
const env = environments.find((e) => e.name === envName);
expect(env).toBeDefined();
expect(env.visible).toBe(visible);
};
const assertContainsVisible = (environments, envName) => {
assertContains(environments, envName, true);
};
const assertContainsNotVisible = (environments, envName) => {
assertContains(environments, envName, false);
};
const projectId = 'default';
const firstEnvTest = 'some-connected-environment';
const secondEnvTest = 'some-also-connected-environment';
await db.stores.environmentStore.create({
name: firstEnvTest,
type: 'production',
});
await db.stores.environmentStore.create({
name: secondEnvTest,
type: 'production',
});
await service.addEnvironmentToProject(
firstEnvTest,
projectId,
SYSTEM_USER_AUDIT,
);
await service.addEnvironmentToProject(
secondEnvTest,
projectId,
SYSTEM_USER_AUDIT,
);
let environments = await service.getProjectEnvironments(projectId);
assertContainsVisible(environments, firstEnvTest);
assertContainsVisible(environments, secondEnvTest);
await service.removeEnvironmentFromProject(
firstEnvTest,
projectId,
SYSTEM_USER_AUDIT,
);
environments = await service.getProjectEnvironments(projectId);
assertContainsNotVisible(environments, firstEnvTest);
assertContainsVisible(environments, secondEnvTest);
});

View File

@ -5,7 +5,7 @@ import {
type IEnvironmentStore,
type IFeatureEnvironmentStore,
type IFeatureStrategiesStore,
type IProjectEnvironment,
type IProjectsAvailableOnEnvironment,
type ISortOrder,
type IUnleashConfig,
type IUnleashStores,
@ -19,7 +19,6 @@ import NameExistsError from '../../error/name-exists-error';
import { sortOrderSchema } from '../../services/sort-order-schema';
import NotFoundError from '../../error/notfound-error';
import type { IProjectStore } from '../../features/project/project-store-type';
import MinimumOneEnvironmentError from '../../error/minimum-one-environment-error';
import type { IFlagResolver } from '../../types/experimental';
import type { CreateFeatureStrategySchema } from '../../openapi';
import type EventService from '../events/event-service';
@ -77,8 +76,24 @@ export default class EnvironmentService {
async getProjectEnvironments(
projectId: string,
): Promise<IProjectEnvironment[]> {
return this.environmentStore.getProjectEnvironments(projectId);
): Promise<IProjectsAvailableOnEnvironment[]> {
// This function produces an object for every environment, in that object is a boolean
// describing whether or not that environment is enabled - aka not deprecated
const environments =
await this.projectStore.getEnvironmentsForProject(projectId);
const environmentsOnProject = new Set(
environments.map((env) => env.environment),
);
const allEnvironments =
await this.environmentStore.getProjectEnvironments(projectId);
return allEnvironments.map((env) => {
return {
...env,
visible: environmentsOnProject.has(env.name),
};
});
}
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
@ -254,22 +269,13 @@ export default class EnvironmentService {
const projectEnvs =
await this.projectStore.getEnvironmentsForProject(projectId);
if (projectEnvs.length > 1) {
await this.forceRemoveEnvironmentFromProject(
await this.forceRemoveEnvironmentFromProject(environment, projectId);
await this.eventService.storeEvent(
new ProjectEnvironmentRemoved({
project: projectId,
environment,
projectId,
);
await this.eventService.storeEvent(
new ProjectEnvironmentRemoved({
project: projectId,
environment,
auditUser,
}),
);
return;
}
throw new MinimumOneEnvironmentError(
'You must always have one active environment',
auditUser,
}),
);
}
}

View File

@ -98,22 +98,6 @@ test('Should remove environment from project', async () => {
expect(envs).toHaveLength(1);
});
test('Should not remove environment from project if project only has one environment enabled', async () => {
await app.request
.delete(`/api/admin/projects/default/environments/default`)
.expect(400)
.expect((r) => {
expect(r.body.details[0].message).toBe(
'You must always have one active environment',
);
});
const envs =
await db.stores.projectStore.getEnvironmentsForProject('default');
expect(envs).toHaveLength(1);
});
test('Should add default strategy to environment', async () => {
const defaultStrategy = {
name: 'flexibleRollout',

View File

@ -1,4 +1,7 @@
import type { IEnvironment, IProjectEnvironment } from '../../types/model';
import type {
IEnvironment,
IProjectsAvailableOnEnvironment,
} from '../../types/model';
import NotFoundError from '../../error/notfound-error';
import type { IEnvironmentStore } from './environment-store-type';
@ -140,7 +143,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
async getProjectEnvironments(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
projectId: string,
): Promise<IProjectEnvironment[]> {
): Promise<IProjectsAvailableOnEnvironment[]> {
return Promise.reject(new Error('Not implemented'));
}

View File

@ -57,6 +57,12 @@ export const environmentProjectSchema = {
'The strategy configuration to add when enabling a feature environment by default',
$ref: '#/components/schemas/createFeatureStrategySchema',
},
visible: {
type: 'boolean',
example: true,
description:
'Indicates whether the environment can be enabled for feature flags in the project',
},
},
components: {
schemas: {

View File

@ -212,6 +212,10 @@ export interface IProjectEnvironment extends IEnvironment {
defaultStrategy?: CreateFeatureStrategySchema;
}
export interface IProjectsAvailableOnEnvironment extends IProjectEnvironment {
visible: boolean;
}
export interface IEnvironmentCreate {
name: string;
type: string;