1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/lib/services/environment-service.ts
Gastón Fournier efd47b72a8
feat: Add variants per env (#2471)
## About the changes
Variants are now stored in each environment rather than in the feature
toggle. This enables RBAC, suggest changes, etc to also apply to
variants.

Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item:
#2254

### Important files
- **src/lib/db/feature-strategy-store.ts** a complex query was moved to
a view named `features_view`
- **src/lib/services/state-service.ts** export version number increased
due to the new format

## Discussion points
We're keeping the old column as a safeguard to be able to go back

Co-authored-by: sighphyre <liquidwicked64@gmail.com>
Co-authored-by: Christopher Kolstad <chriswk@getunleash.ai>
2022-11-21 10:37:16 +01:00

221 lines
7.1 KiB
TypeScript

import { IUnleashStores } from '../types/stores';
import { IUnleashConfig } from '../types/option';
import { Logger } from '../logger';
import { IEnvironment, IProjectEnvironment, ISortOrder } from '../types/model';
import { UNIQUE_CONSTRAINT_VIOLATION } from '../error/db-error';
import NameExistsError from '../error/name-exists-error';
import { sortOrderSchema } from './state-schema';
import NotFoundError from '../error/notfound-error';
import { IEnvironmentStore } from '../types/stores/environment-store';
import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store';
import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store';
import { IProjectStore } from 'lib/types/stores/project-store';
import MinimumOneEnvironmentError from '../error/minimum-one-environment-error';
import { IFlagResolver } from 'lib/types/experimental';
export default class EnvironmentService {
private logger: Logger;
private environmentStore: IEnvironmentStore;
private featureStrategiesStore: IFeatureStrategiesStore;
private projectStore: IProjectStore;
private featureEnvironmentStore: IFeatureEnvironmentStore;
private flagResolver: IFlagResolver;
constructor(
{
environmentStore,
featureStrategiesStore,
featureEnvironmentStore,
projectStore,
}: Pick<
IUnleashStores,
| 'environmentStore'
| 'featureStrategiesStore'
| 'featureEnvironmentStore'
| 'projectStore'
>,
{
getLogger,
flagResolver,
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
) {
this.logger = getLogger('services/environment-service.ts');
this.environmentStore = environmentStore;
this.featureStrategiesStore = featureStrategiesStore;
this.featureEnvironmentStore = featureEnvironmentStore;
this.projectStore = projectStore;
this.flagResolver = flagResolver;
}
async getAll(): Promise<IEnvironment[]> {
return this.environmentStore.getAllWithCounts();
}
async get(name: string): Promise<IEnvironment> {
return this.environmentStore.get(name);
}
async getProjectEnvironments(
projectId: string,
): Promise<IProjectEnvironment[]> {
return this.environmentStore.getProjectEnvironments(projectId);
}
async updateSortOrder(sortOrder: ISortOrder): Promise<void> {
await sortOrderSchema.validateAsync(sortOrder);
await Promise.all(
Object.keys(sortOrder).map((key) => {
const value = sortOrder[key];
return this.environmentStore.updateSortOrder(key, value);
}),
);
}
async toggleEnvironment(name: string, value: boolean): Promise<void> {
const exists = await this.environmentStore.exists(name);
if (exists) {
return this.environmentStore.updateProperty(name, 'enabled', value);
}
throw new NotFoundError(`Could not find environment ${name}`);
}
async addEnvironmentToProject(
environment: string,
projectId: string,
): Promise<void> {
try {
await this.featureEnvironmentStore.connectProject(
environment,
projectId,
);
await this.featureEnvironmentStore.connectFeatures(
environment,
projectId,
);
if (!this.flagResolver.isEnabled('variantsPerEnvironment')) {
await this.featureEnvironmentStore.clonePreviousVariants(
environment,
projectId,
);
}
} catch (e) {
if (e.code === UNIQUE_CONSTRAINT_VIOLATION) {
throw new NameExistsError(
`${projectId} already has the environment ${environment} enabled`,
);
}
throw e;
}
}
async overrideEnabledProjects(
environmentNamesToEnable: string[],
): Promise<void> {
if (environmentNamesToEnable.length === 0) {
return Promise.resolve();
}
const allEnvironments = await this.environmentStore.getAll();
const existingEnvironmentsToEnable = allEnvironments.filter((env) =>
environmentNamesToEnable.includes(env.name),
);
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 Promise.resolve();
}
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(
environment: string,
projectId: string,
): Promise<void> {
const projectEnvs = await this.projectStore.getEnvironmentsForProject(
projectId,
);
if (projectEnvs.length > 1) {
await this.forceRemoveEnvironmentFromProject(
environment,
projectId,
);
return;
}
throw new MinimumOneEnvironmentError(
'You must always have one active environment',
);
}
}