Skip to content

Commit e7ac420

Browse files
authored
feat: project environments include visible property (#9427)
1 parent ae0fc86 commit e7ac420

9 files changed

+91
-42
lines changed

src/lib/error/from-legacy-error.ts

-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ const getStatusCode = (errorName: string): number => {
1919
return 400;
2020
case 'PasswordUndefinedError':
2121
return 400;
22-
case 'MinimumOneEnvironmentError':
23-
return 400;
2422
case 'InvalidTokenError':
2523
return 401;
2624
case 'UsedTokenError':

src/lib/error/index.ts

-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import FeatureHasTagError from './feature-has-tag-error';
55
import IncompatibleProjectError from './incompatible-project-error';
66
import InvalidOperationError from './invalid-operation-error';
77
import InvalidTokenError from './invalid-token-error';
8-
import MinimumOneEnvironmentError from './minimum-one-environment-error';
98
import NameExistsError from './name-exists-error';
109
import PermissionError from './permission-error';
1110
import { OperationDeniedError } from './operation-denied-error';
@@ -27,7 +26,6 @@ export {
2726
IncompatibleProjectError,
2827
InvalidOperationError,
2928
InvalidTokenError,
30-
MinimumOneEnvironmentError,
3129
NameExistsError,
3230
PermissionError,
3331
ForbiddenError,

src/lib/error/unleash-error.ts

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ export const UnleashApiErrorTypes = [
88
'IncompatibleProjectError',
99
'InvalidOperationError',
1010
'InvalidTokenError',
11-
'MinimumOneEnvironmentError',
1211
'NameExistsError',
1312
'NoAccessError',
1413
'NotFoundError',

src/lib/features/project-environments/environment-service.test.ts

+51
Original file line numberDiff line numberDiff line change
@@ -338,3 +338,54 @@ test('Override works correctly when enabling default and disabling prod and dev'
338338
expect(targetedEnvironment?.enabled).toBe(true);
339339
expect(allOtherEnvironments.every((x) => !x)).toBe(true);
340340
});
341+
342+
test('getProjectEnvironments also includes whether or not a given project is visible on a given environment', async () => {
343+
const assertContains = (environments, envName, visible) => {
344+
const env = environments.find((e) => e.name === envName);
345+
expect(env).toBeDefined();
346+
expect(env.visible).toBe(visible);
347+
};
348+
349+
const assertContainsVisible = (environments, envName) => {
350+
assertContains(environments, envName, true);
351+
};
352+
353+
const assertContainsNotVisible = (environments, envName) => {
354+
assertContains(environments, envName, false);
355+
};
356+
357+
const projectId = 'default';
358+
const firstEnvTest = 'some-connected-environment';
359+
const secondEnvTest = 'some-also-connected-environment';
360+
await db.stores.environmentStore.create({
361+
name: firstEnvTest,
362+
type: 'production',
363+
});
364+
await db.stores.environmentStore.create({
365+
name: secondEnvTest,
366+
type: 'production',
367+
});
368+
369+
await service.addEnvironmentToProject(
370+
firstEnvTest,
371+
projectId,
372+
SYSTEM_USER_AUDIT,
373+
);
374+
await service.addEnvironmentToProject(
375+
secondEnvTest,
376+
projectId,
377+
SYSTEM_USER_AUDIT,
378+
);
379+
let environments = await service.getProjectEnvironments(projectId);
380+
assertContainsVisible(environments, firstEnvTest);
381+
assertContainsVisible(environments, secondEnvTest);
382+
383+
await service.removeEnvironmentFromProject(
384+
firstEnvTest,
385+
projectId,
386+
SYSTEM_USER_AUDIT,
387+
);
388+
environments = await service.getProjectEnvironments(projectId);
389+
assertContainsNotVisible(environments, firstEnvTest);
390+
assertContainsVisible(environments, secondEnvTest);
391+
});

src/lib/features/project-environments/environment-service.ts

+25-19
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type IEnvironmentStore,
66
type IFeatureEnvironmentStore,
77
type IFeatureStrategiesStore,
8-
type IProjectEnvironment,
8+
type IProjectsAvailableOnEnvironment,
99
type ISortOrder,
1010
type IUnleashConfig,
1111
type IUnleashStores,
@@ -19,7 +19,6 @@ import NameExistsError from '../../error/name-exists-error';
1919
import { sortOrderSchema } from '../../services/sort-order-schema';
2020
import NotFoundError from '../../error/notfound-error';
2121
import type { IProjectStore } from '../../features/project/project-store-type';
22-
import MinimumOneEnvironmentError from '../../error/minimum-one-environment-error';
2322
import type { IFlagResolver } from '../../types/experimental';
2423
import type { CreateFeatureStrategySchema } from '../../openapi';
2524
import type EventService from '../events/event-service';
@@ -77,8 +76,24 @@ export default class EnvironmentService {
7776

7877
async getProjectEnvironments(
7978
projectId: string,
80-
): Promise<IProjectEnvironment[]> {
81-
return this.environmentStore.getProjectEnvironments(projectId);
79+
): Promise<IProjectsAvailableOnEnvironment[]> {
80+
// This function produces an object for every environment, in that object is a boolean
81+
// describing whether or not that environment is enabled - aka not deprecated
82+
const environments =
83+
await this.projectStore.getEnvironmentsForProject(projectId);
84+
const environmentsOnProject = new Set(
85+
environments.map((env) => env.environment),
86+
);
87+
88+
const allEnvironments =
89+
await this.environmentStore.getProjectEnvironments(projectId);
90+
91+
return allEnvironments.map((env) => {
92+
return {
93+
...env,
94+
visible: environmentsOnProject.has(env.name),
95+
};
96+
});
8297
}
8398

8499
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
@@ -254,22 +269,13 @@ export default class EnvironmentService {
254269
const projectEnvs =
255270
await this.projectStore.getEnvironmentsForProject(projectId);
256271

257-
if (projectEnvs.length > 1) {
258-
await this.forceRemoveEnvironmentFromProject(
272+
await this.forceRemoveEnvironmentFromProject(environment, projectId);
273+
await this.eventService.storeEvent(
274+
new ProjectEnvironmentRemoved({
275+
project: projectId,
259276
environment,
260-
projectId,
261-
);
262-
await this.eventService.storeEvent(
263-
new ProjectEnvironmentRemoved({
264-
project: projectId,
265-
environment,
266-
auditUser,
267-
}),
268-
);
269-
return;
270-
}
271-
throw new MinimumOneEnvironmentError(
272-
'You must always have one active environment',
277+
auditUser,
278+
}),
273279
);
274280
}
275281
}

src/lib/features/project-environments/environments.e2e.test.ts

-16
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,6 @@ test('Should remove environment from project', async () => {
9898
expect(envs).toHaveLength(1);
9999
});
100100

101-
test('Should not remove environment from project if project only has one environment enabled', async () => {
102-
await app.request
103-
.delete(`/api/admin/projects/default/environments/default`)
104-
.expect(400)
105-
.expect((r) => {
106-
expect(r.body.details[0].message).toBe(
107-
'You must always have one active environment',
108-
);
109-
});
110-
111-
const envs =
112-
await db.stores.projectStore.getEnvironmentsForProject('default');
113-
114-
expect(envs).toHaveLength(1);
115-
});
116-
117101
test('Should add default strategy to environment', async () => {
118102
const defaultStrategy = {
119103
name: 'flexibleRollout',

src/lib/features/project-environments/fake-environment-store.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import type { IEnvironment, IProjectEnvironment } from '../../types/model';
1+
import type {
2+
IEnvironment,
3+
IProjectsAvailableOnEnvironment,
4+
} from '../../types/model';
25
import NotFoundError from '../../error/notfound-error';
36
import type { IEnvironmentStore } from './environment-store-type';
47

@@ -140,7 +143,7 @@ export default class FakeEnvironmentStore implements IEnvironmentStore {
140143
async getProjectEnvironments(
141144
// eslint-disable-next-line @typescript-eslint/no-unused-vars
142145
projectId: string,
143-
): Promise<IProjectEnvironment[]> {
146+
): Promise<IProjectsAvailableOnEnvironment[]> {
144147
return Promise.reject(new Error('Not implemented'));
145148
}
146149

src/lib/openapi/spec/environment-project-schema.ts

+6
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ export const environmentProjectSchema = {
5757
'The strategy configuration to add when enabling a feature environment by default',
5858
$ref: '#/components/schemas/createFeatureStrategySchema',
5959
},
60+
visible: {
61+
type: 'boolean',
62+
example: true,
63+
description:
64+
'Indicates whether the environment can be enabled for feature flags in the project',
65+
},
6066
},
6167
components: {
6268
schemas: {

src/lib/types/model.ts

+4
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ export interface IProjectEnvironment extends IEnvironment {
212212
defaultStrategy?: CreateFeatureStrategySchema;
213213
}
214214

215+
export interface IProjectsAvailableOnEnvironment extends IProjectEnvironment {
216+
visible: boolean;
217+
}
218+
215219
export interface IEnvironmentCreate {
216220
name: string;
217221
type: string;

0 commit comments

Comments
 (0)