mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: enabled environments override now also moves projects and toggles to new environments
This commit is contained in:
		
							parent
							
								
									c3b064adfc
								
							
						
					
					
						commit
						8410a8e3ac
					
				| @ -163,20 +163,26 @@ export default class EnvironmentStore implements IEnvironmentStore { | ||||
|         return mapRow(row[0]); | ||||
|     } | ||||
| 
 | ||||
|     async disableAllExcept(environments: string[]): Promise<void> { | ||||
|     async disable(environments: IEnvironment[]): Promise<void> { | ||||
|         await this.db(TABLE) | ||||
|             .update({ | ||||
|                 enabled: false, | ||||
|             }) | ||||
|             .whereNotIn('name', environments); | ||||
|             .whereIn( | ||||
|                 'name', | ||||
|                 environments.map((env) => env.name), | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     async enable(environments: string[]): Promise<void> { | ||||
|     async enable(environments: IEnvironment[]): Promise<void> { | ||||
|         await this.db(TABLE) | ||||
|             .update({ | ||||
|                 enabled: true, | ||||
|             }) | ||||
|             .whereIn('name', environments); | ||||
|             .whereIn( | ||||
|                 'name', | ||||
|                 environments.map((env) => env.name), | ||||
|             ); | ||||
|     } | ||||
| 
 | ||||
|     async delete(name: string): Promise<void> { | ||||
|  | ||||
| @ -24,6 +24,11 @@ const COLUMNS = [ | ||||
| ]; | ||||
| const TABLE = 'projects'; | ||||
| 
 | ||||
| export interface IEnvironmentProjectLink { | ||||
|     environmentName: string; | ||||
|     projectId: string; | ||||
| } | ||||
| 
 | ||||
| class ProjectStore implements IProjectStore { | ||||
|     private db: Knex; | ||||
| 
 | ||||
| @ -197,6 +202,15 @@ class ProjectStore implements IProjectStore { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async getProjectLinksForEnvironments( | ||||
|         environments: string[], | ||||
|     ): Promise<IEnvironmentProjectLink[]> { | ||||
|         let rows = await this.db('project_environments') | ||||
|             .select(['project_id', 'environment_name']) | ||||
|             .whereIn('environment_name', environments); | ||||
|         return rows.map(this.mapLinkRow); | ||||
|     } | ||||
| 
 | ||||
|     async deleteEnvironmentForProject( | ||||
|         id: string, | ||||
|         environment: string, | ||||
| @ -251,6 +265,14 @@ class ProjectStore implements IProjectStore { | ||||
|             .then((res) => Number(res[0].count)); | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||
|     mapLinkRow(row): IEnvironmentProjectLink { | ||||
|         return { | ||||
|             environmentName: row.environment_name, | ||||
|             projectId: row.project_id, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
 | ||||
|     mapRow(row): IProject { | ||||
|         if (!row) { | ||||
|  | ||||
| @ -95,25 +95,87 @@ export default class EnvironmentService { | ||||
|     } | ||||
| 
 | ||||
|     async overrideEnabledProjects( | ||||
|         environmentsToEnable: string[], | ||||
|         environmentNamesToEnable: string[], | ||||
|     ): Promise<void> { | ||||
|         if (environmentsToEnable.length === 0) { | ||||
|         if (environmentNamesToEnable.length === 0) { | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
| 
 | ||||
|         const environmentsExist = await Promise.all( | ||||
|             environmentsToEnable.map((env) => | ||||
|                 this.environmentStore.exists(env), | ||||
|             ), | ||||
|         const allEnvironments = await this.environmentStore.getAll(); | ||||
|         const existingEnvironmentsToEnable = allEnvironments.filter((env) => | ||||
|             environmentNamesToEnable.includes(env.name), | ||||
|         ); | ||||
|         if (!environmentsExist.every((exists) => exists)) { | ||||
|             this.logger.error( | ||||
| 
 | ||||
|         if ( | ||||
|             existingEnvironmentsToEnable.length !== | ||||
|             environmentNamesToEnable.length | ||||
|         ) { | ||||
|             this.logger.warn( | ||||
|                 "Found environment enabled overrides but some of the specified environments don't exist, no overrides will be executed", | ||||
|             ); | ||||
|             return; | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
|         await this.environmentStore.disableAllExcept(environmentsToEnable); | ||||
|         await this.environmentStore.enable(environmentsToEnable); | ||||
| 
 | ||||
|         const environmentsNotAlreadyEnabled = | ||||
|             existingEnvironmentsToEnable.filter((env) => env.enabled == false); | ||||
|         const environmentsToDisable = allEnvironments.filter((env) => { | ||||
|             return ( | ||||
|                 !environmentNamesToEnable.includes(env.name) && | ||||
|                 env.enabled == true | ||||
|             ); | ||||
|         }); | ||||
| 
 | ||||
|         await this.environmentStore.disable(environmentsToDisable); | ||||
|         await this.environmentStore.enable(environmentsNotAlreadyEnabled); | ||||
| 
 | ||||
|         await this.remapProjectsLinks( | ||||
|             environmentsToDisable, | ||||
|             environmentsNotAlreadyEnabled, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private async remapProjectsLinks( | ||||
|         toDisable: IEnvironment[], | ||||
|         toEnable: IEnvironment[], | ||||
|     ) { | ||||
|         const projectLinks = | ||||
|             await this.projectStore.getProjectLinksForEnvironments( | ||||
|                 toDisable.map((env) => env.name), | ||||
|             ); | ||||
| 
 | ||||
|         const unlinkTasks = projectLinks.map((link) => { | ||||
|             return this.forceRemoveEnvironmentFromProject( | ||||
|                 link.environmentName, | ||||
|                 link.projectId, | ||||
|             ); | ||||
|         }); | ||||
|         await Promise.all(unlinkTasks.flat()); | ||||
| 
 | ||||
|         const uniqueProjects = [ | ||||
|             ...new Set(projectLinks.map((link) => link.projectId)), | ||||
|         ]; | ||||
| 
 | ||||
|         let linkTasks = uniqueProjects.map((project) => { | ||||
|             return toEnable.map((enabledEnv) => { | ||||
|                 return this.addEnvironmentToProject(enabledEnv.name, project); | ||||
|             }); | ||||
|         }); | ||||
| 
 | ||||
|         await Promise.all(linkTasks.flat()); | ||||
|     } | ||||
| 
 | ||||
|     async forceRemoveEnvironmentFromProject( | ||||
|         environment: string, | ||||
|         projectId: string, | ||||
|     ): Promise<void> { | ||||
|         await this.featureEnvironmentStore.disconnectFeatures( | ||||
|             environment, | ||||
|             projectId, | ||||
|         ); | ||||
|         await this.featureEnvironmentStore.disconnectProject( | ||||
|             environment, | ||||
|             projectId, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     async removeEnvironmentFromProject( | ||||
| @ -125,11 +187,7 @@ export default class EnvironmentService { | ||||
|         ); | ||||
| 
 | ||||
|         if (projectEnvs.length > 1) { | ||||
|             await this.featureEnvironmentStore.disconnectFeatures( | ||||
|                 environment, | ||||
|                 projectId, | ||||
|             ); | ||||
|             await this.featureEnvironmentStore.disconnectProject( | ||||
|             await this.forceRemoveEnvironmentFromProject( | ||||
|                 environment, | ||||
|                 projectId, | ||||
|             ); | ||||
|  | ||||
| @ -16,6 +16,6 @@ export interface IEnvironmentStore extends Store<IEnvironment, string> { | ||||
|     updateSortOrder(id: string, value: number): Promise<void>; | ||||
|     importEnvironments(environments: IEnvironment[]): Promise<IEnvironment[]>; | ||||
|     delete(name: string): Promise<void>; | ||||
|     disableAllExcept(environments: string[]): Promise<void>; | ||||
|     enable(environments: string[]): Promise<void>; | ||||
|     disable(environments: IEnvironment[]): Promise<void>; | ||||
|     enable(environments: IEnvironment[]): Promise<void>; | ||||
| } | ||||
|  | ||||
| @ -1,3 +1,4 @@ | ||||
| import { IEnvironmentProjectLink } from 'lib/db/project-store'; | ||||
| import { IProject, IProjectWithCount } from '../model'; | ||||
| import { Store } from './store'; | ||||
| 
 | ||||
| @ -35,4 +36,7 @@ export interface IProjectStore extends Store<IProject, string> { | ||||
|     getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>; | ||||
|     count(): Promise<number>; | ||||
|     getAll(query?: IProjectQuery): Promise<IProject[]>; | ||||
|     getProjectLinksForEnvironments( | ||||
|         environments: string[], | ||||
|     ): Promise<IEnvironmentProjectLink[]>; | ||||
| } | ||||
|  | ||||
| @ -150,7 +150,7 @@ test('Setting an override disables all other envs', async () => { | ||||
|         type: 'production', | ||||
|     }); | ||||
| 
 | ||||
|     //Set these to the wrong state so we can assert that overriding them flips
 | ||||
|     //Set these to the wrong state so we can assert that overriding them flips their state
 | ||||
|     await service.toggleEnvironment(disabledEnvName, true); | ||||
|     await service.toggleEnvironment(enabledEnvName, false); | ||||
| 
 | ||||
| @ -165,7 +165,6 @@ test('Setting an override disables all other envs', async () => { | ||||
|         .filter((x) => x.name != enabledEnvName) | ||||
|         .map((env) => env.enabled); | ||||
| 
 | ||||
|     console.log(allOtherEnvironments); | ||||
|     expect(targetedEnvironment.enabled).toBe(true); | ||||
|     expect(allOtherEnvironments.every((x) => x === false)).toBe(true); | ||||
| }); | ||||
| @ -189,3 +188,47 @@ test('Passing an empty override does nothing', async () => { | ||||
| 
 | ||||
|     expect(targetedEnvironment.enabled).toBe(true); | ||||
| }); | ||||
| 
 | ||||
| test('When given overrides should remap projects to override environments', async () => { | ||||
|     const enabledEnvName = 'enabled'; | ||||
|     const ignoredEnvName = 'ignored'; | ||||
|     const disabledEnvName = 'disabled'; | ||||
|     const toggleName = 'test-toggle'; | ||||
| 
 | ||||
|     await db.stores.environmentStore.create({ | ||||
|         name: enabledEnvName, | ||||
|         type: 'production', | ||||
|     }); | ||||
| 
 | ||||
|     await db.stores.environmentStore.create({ | ||||
|         name: ignoredEnvName, | ||||
|         type: 'production', | ||||
|     }); | ||||
| 
 | ||||
|     await db.stores.environmentStore.create({ | ||||
|         name: disabledEnvName, | ||||
|         type: 'production', | ||||
|     }); | ||||
| 
 | ||||
|     await service.toggleEnvironment(disabledEnvName, true); | ||||
|     await service.toggleEnvironment(ignoredEnvName, true); | ||||
|     await service.toggleEnvironment(enabledEnvName, false); | ||||
| 
 | ||||
|     await stores.featureToggleStore.create('default', { | ||||
|         name: toggleName, | ||||
|         type: 'release', | ||||
|         description: '', | ||||
|         stale: false, | ||||
|     }); | ||||
| 
 | ||||
|     await service.addEnvironmentToProject(disabledEnvName, 'default'); | ||||
| 
 | ||||
|     await service.overrideEnabledProjects([enabledEnvName]); | ||||
| 
 | ||||
|     const projects = await stores.projectStore.getEnvironmentsForProject( | ||||
|         'default', | ||||
|     ); | ||||
| 
 | ||||
|     expect(projects).toContain('enabled'); | ||||
|     expect(projects).not.toContain('default'); | ||||
| }); | ||||
|  | ||||
							
								
								
									
										10
									
								
								src/test/fixtures/fake-environment-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								src/test/fixtures/fake-environment-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -10,16 +10,18 @@ export default class FakeEnvironmentStore implements IEnvironmentStore { | ||||
| 
 | ||||
|     environments: IEnvironment[] = []; | ||||
| 
 | ||||
|     disableAllExcept(environments: string[]): Promise<void> { | ||||
|     disable(environments: IEnvironment[]): Promise<void> { | ||||
|         for (let env of this.environments) { | ||||
|             if (!environments.includes(env.name)) env.enabled = false; | ||||
|             if (environments.map((e) => e.name).includes(env.name)) | ||||
|                 env.enabled = false; | ||||
|         } | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
| 
 | ||||
|     enable(environments: string[]): Promise<void> { | ||||
|     enable(environments: IEnvironment[]): Promise<void> { | ||||
|         for (let env of this.environments) { | ||||
|             if (environments.includes(env.name)) env.enabled = true; | ||||
|             if (environments.map((e) => e.name).includes(env.name)) | ||||
|                 env.enabled = true; | ||||
|         } | ||||
|         return Promise.resolve(); | ||||
|     } | ||||
|  | ||||
							
								
								
									
										14
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -5,15 +5,23 @@ import { | ||||
| } from '../../lib/types/stores/project-store'; | ||||
| import { IProject, IProjectWithCount } from '../../lib/types/model'; | ||||
| import NotFoundError from '../../lib/error/notfound-error'; | ||||
| import { IEnvironmentProjectLink } from 'lib/db/project-store'; | ||||
| 
 | ||||
| export default class FakeProjectStore implements IProjectStore { | ||||
|     projects: IProject[] = []; | ||||
| 
 | ||||
|     projectEnvironment: Map<string, Set<string>> = new Map(); | ||||
| 
 | ||||
|     getEnvironmentsForProject(): Promise<string[]> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
| 
 | ||||
|     projects: IProject[] = []; | ||||
| 
 | ||||
|     projectEnvironment: Map<string, Set<string>> = new Map(); | ||||
|     getProjectLinksForEnvironments( | ||||
|         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|         environments: string[], | ||||
|     ): Promise<IEnvironmentProjectLink[]> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
| 
 | ||||
|     async addEnvironmentToProject( | ||||
|         // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user