diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index 1506b8f469..80f2c371f9 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -435,4 +435,17 @@ export class AccessStore implements IAccessStore { }) .delete(); } + + async cloneEnvironmentPermissions( + sourceEnvironment: string, + destinationEnvironment: string, + ): Promise { + return this.db.raw( + `insert into role_permission + (role_id, permission_id, environment) + (select role_id, permission_id, ? + from ${T.ROLE_PERMISSION} where environment = ?)`, + [destinationEnvironment, sourceEnvironment], + ); + } } diff --git a/src/lib/db/feature-environment-store.ts b/src/lib/db/feature-environment-store.ts index 1ee7ae7600..99333b7547 100644 --- a/src/lib/db/feature-environment-store.ts +++ b/src/lib/db/feature-environment-store.ts @@ -9,10 +9,12 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { IFeatureEnvironment } from '../types/model'; import NotFoundError from '../error/notfound-error'; +import { v4 as uuidv4 } from 'uuid'; const T = { featureEnvs: 'feature_environments', featureStrategies: 'feature_strategies', + features: 'features', }; interface IFeatureEnvironmentRow { @@ -21,6 +23,11 @@ interface IFeatureEnvironmentRow { enabled: boolean; } +interface ISegmentRow { + id: string; + segment_id: number; +} + export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { private db: Knex; @@ -268,4 +275,83 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { }), ); } + + async copyEnvironmentFeaturesByProjects( + sourceEnvironment: string, + destinationEnvironment: string, + projects: string[], + ): Promise { + await this.db.raw( + `INSERT INTO ${T.featureEnvs} ( + SELECT distinct ? AS environment, feature_name, enabled FROM ${T.featureEnvs} INNER JOIN ${T.features} ON ${T.featureEnvs}.feature_name = ${T.features}.name WHERE environment = ? AND project = ANY(?))`, + [destinationEnvironment, sourceEnvironment, projects], + ); + } + + async cloneStrategies( + sourceEnvironment: string, + destinationEnvironment: string, + ): Promise { + let sourceFeatureStrategies = await this.db('feature_strategies').where( + { + environment: sourceEnvironment, + }, + ); + + const clonedStrategyRows = sourceFeatureStrategies.map( + (featureStrategy) => { + return { + id: uuidv4(), + feature_name: featureStrategy.feature_name, + project_name: featureStrategy.project_name, + environment: destinationEnvironment, + strategy_name: featureStrategy.strategy_name, + parameters: JSON.stringify(featureStrategy.parameters), + constraints: JSON.stringify(featureStrategy.constraints), + sort_order: featureStrategy.sort_order, + }; + }, + ); + + if (clonedStrategyRows.length === 0) { + return Promise.resolve(); + } + await this.db('feature_strategies').insert(clonedStrategyRows); + + const newStrategyMapping = new Map(); + sourceFeatureStrategies.forEach((sourceStrategy, index) => { + newStrategyMapping.set( + sourceStrategy.id, + clonedStrategyRows[index].id, + ); + }); + + const segmentsToClone: ISegmentRow[] = await this.db( + 'feature_strategy_segment as fss', + ) + .select(['id', 'segment_id']) + .join( + 'feature_strategies AS fs', + 'fss.feature_strategy_id', + 'fs.id', + ) + .where('environment', sourceEnvironment); + + const clonedSegmentIdRows = segmentsToClone.map( + (existingSegmentRow) => { + return { + feature_strategy_id: newStrategyMapping.get( + existingSegmentRow.id, + ), + segment_id: existingSegmentRow.segment_id, + }; + }, + ); + + if (clonedSegmentIdRows.length > 0) { + await this.db('feature_strategy_segment').insert( + clonedSegmentIdRows, + ); + } + } } diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 1c661c0462..5f46bba893 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -246,6 +246,23 @@ class ProjectStore implements IProjectStore { .ignore(); } + async addEnvironmentToProjects( + environment: string, + projects: string[], + ): Promise { + const rows = projects.map((project) => { + return { + project_id: project, + environment_name: environment, + }; + }); + + await this.db('project_environments') + .insert(rows) + .onConflict(['project_id', 'environment_name']) + .ignore(); + } + async getEnvironmentsForProject(id: string): Promise { return this.db('project_environments') .where({ diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 14dc0c1a39..89a5c190fb 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -131,6 +131,13 @@ export interface IEnvironmentCreate { enabled?: boolean; } +export interface IEnvironmentClone { + name: string; + projects?: string[]; + type: string; + clonePermissions?: boolean; +} + export interface IEnvironmentOverview { name: string; enabled: boolean; diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts index b6bc5ca876..96662b2101 100644 --- a/src/lib/types/stores/access-store.ts +++ b/src/lib/types/stores/access-store.ts @@ -127,4 +127,9 @@ export interface IAccessStore extends Store { permission: string, environment?: string, ): Promise; + + cloneEnvironmentPermissions( + sourceEnvironment: string, + destinationEnvironment: string, + ): Promise; } diff --git a/src/lib/types/stores/feature-environment-store.ts b/src/lib/types/stores/feature-environment-store.ts index dc57c9762b..0df15eede4 100644 --- a/src/lib/types/stores/feature-environment-store.ts +++ b/src/lib/types/stores/feature-environment-store.ts @@ -48,4 +48,13 @@ export interface IFeatureEnvironmentStore connectProject(environment: string, projectId: string): Promise; disconnectProject(environment: string, projectId: string): Promise; + copyEnvironmentFeaturesByProjects( + sourceEnvironment: string, + destinationEnvironment: string, + projects: string[], + ): Promise; + cloneStrategies( + sourceEnvironment: string, + destinationEnvironment: string, + ): Promise; } diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index 56834ac937..82dc6ad877 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -47,4 +47,8 @@ export interface IProjectStore extends Store { getProjectLinksForEnvironments( environments: string[], ): Promise; + addEnvironmentToProjects( + environment: string, + projects: string[], + ): Promise; } diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index 92a6717dc1..280856286e 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -185,6 +185,13 @@ class AccessStoreMock implements IAccessStore { removeRolesOfTypeForUser(userId: number, roleType: string): Promise { return Promise.resolve(undefined); } + + cloneEnvironmentPermissions( + sourceEnvironment: string, + destinationEnvironment: string, + ): Promise { + return Promise.resolve(undefined); + } } module.exports = AccessStoreMock; diff --git a/src/test/fixtures/fake-feature-environment-store.ts b/src/test/fixtures/fake-feature-environment-store.ts index 197032c0ff..a0592e7988 100644 --- a/src/test/fixtures/fake-feature-environment-store.ts +++ b/src/test/fixtures/fake-feature-environment-store.ts @@ -161,4 +161,24 @@ export default class FakeFeatureEnvironmentStore ): Promise { return Promise.reject(new Error('Not implemented')); } + + copyEnvironmentFeaturesByProjects( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sourceEnvironment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + destinationEnvironment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projects: string[], + ): Promise { + throw new Error('Method not implemented.'); + } + + cloneStrategies( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + sourceEnvironment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + destinationEnvironment: string, + ): Promise { + throw new Error('Method not implemented.'); + } } diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index a447ef8fca..856f8c4efa 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -141,4 +141,13 @@ export default class FakeProjectStore implements IProjectStore { getProjectsByUser(userId: number): Promise { throw new Error('Method not implemented.'); } + + addEnvironmentToProjects( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projects: string[], + ): Promise { + throw new Error('Method not implemented.'); + } }