Skip to content

Commit

Permalink
feat: enabled environments override now also moves projects and toggl…
Browse files Browse the repository at this point in the history
…es to new environments
  • Loading branch information
sighphyre committed Mar 11, 2022
1 parent c3b064a commit 8410a8e
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 31 deletions.
14 changes: 10 additions & 4 deletions src/lib/db/environment-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
22 changes: 22 additions & 0 deletions src/lib/db/project-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const COLUMNS = [
];
const TABLE = 'projects';

export interface IEnvironmentProjectLink {
environmentName: string;
projectId: string;
}

class ProjectStore implements IProjectStore {
private db: Knex;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
90 changes: 74 additions & 16 deletions src/lib/services/environment-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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,
);
Expand Down
4 changes: 2 additions & 2 deletions src/lib/types/stores/environment-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
}
4 changes: 4 additions & 0 deletions src/lib/types/stores/project-store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IEnvironmentProjectLink } from 'lib/db/project-store';
import { IProject, IProjectWithCount } from '../model';
import { Store } from './store';

Expand Down Expand Up @@ -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[]>;
}
47 changes: 45 additions & 2 deletions src/test/e2e/services/environment-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);
});
Expand All @@ -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 changes: 6 additions & 4 deletions src/test/fixtures/fake-environment-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
14 changes: 11 additions & 3 deletions src/test/fixtures/fake-project-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 8410a8e

Please sign in to comment.