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