diff --git a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts index 9cbafdfadc..428a603111 100644 --- a/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/archive-feature-toggle-controller.ts @@ -16,10 +16,7 @@ import { emptyResponse, getStandardResponses, } from '../../openapi/util/standard-responses'; -import type { - TransactionCreator, - UnleashTransaction, -} from '../../db/transaction'; +import type { WithTransactional } from '../../db/transaction'; import { archivedFeaturesSchema, type ArchivedFeaturesSchema, @@ -27,10 +24,7 @@ import { export default class ArchiveController extends Controller { private featureService: FeatureToggleService; - private transactionalFeatureToggleService: ( - db: UnleashTransaction, - ) => FeatureToggleService; - private readonly startTransaction: TransactionCreator; + private transactionalFeatureToggleService: WithTransactional; private openApiService: OpenApiService; constructor( @@ -45,14 +39,12 @@ export default class ArchiveController extends Controller { | 'featureToggleService' | 'openApiService' >, - startTransaction: TransactionCreator, ) { super(config); this.featureService = featureToggleService; this.openApiService = openApiService; this.transactionalFeatureToggleService = transactionalFeatureToggleService; - this.startTransaction = startTransaction; this.route({ method: 'get', @@ -204,11 +196,8 @@ export default class ArchiveController extends Controller { ): Promise { const { featureName } = req.params; - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).reviveFeature( - featureName, - req.audit, - ), + await this.transactionalFeatureToggleService.transactional((service) => + service.reviveFeature(featureName, req.audit), ); res.status(200).end(); } diff --git a/src/lib/features/feature-toggle/feature-toggle-controller.ts b/src/lib/features/feature-toggle/feature-toggle-controller.ts index 25a2fa4bca..d591e557a3 100644 --- a/src/lib/features/feature-toggle/feature-toggle-controller.ts +++ b/src/lib/features/feature-toggle/feature-toggle-controller.ts @@ -48,10 +48,7 @@ import type { } from '../../services'; import { querySchema } from '../../schema/feature-schema'; import type { BatchStaleSchema } from '../../openapi/spec/batch-stale-schema'; -import type { - TransactionCreator, - UnleashTransaction, -} from '../../db/transaction'; +import type { WithTransactional } from '../../db/transaction'; import { BadDataError } from '../../error'; import { anonymise } from '../../util'; import { throwOnInvalidSchema } from '../../openapi/validate'; @@ -116,9 +113,7 @@ export default class ProjectFeaturesController extends Controller { private featureTagService: FeatureTagService; - private transactionalFeatureToggleService: ( - db: UnleashTransaction, - ) => FeatureToggleService; + private transactionalFeatureToggleService: WithTransactional; private openApiService: OpenApiService; @@ -126,8 +121,6 @@ export default class ProjectFeaturesController extends Controller { private readonly logger: Logger; - private readonly startTransaction: TransactionCreator; - constructor( config: IUnleashConfig, { @@ -136,13 +129,11 @@ export default class ProjectFeaturesController extends Controller { transactionalFeatureToggleService, featureTagService, }: ProjectFeaturesServices, - startTransaction: TransactionCreator, ) { super(config); this.featureService = featureToggleService; this.transactionalFeatureToggleService = transactionalFeatureToggleService; - this.startTransaction = startTransaction; this.openApiService = openApiService; this.featureTagService = featureTagService; this.flagResolver = config.flagResolver; @@ -658,13 +649,17 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId, featureName } = req.params; const { name, replaceGroupId } = req.body; - const created = await this.featureService.cloneFeatureToggle( - featureName, - projectId, - name, - req.audit, - replaceGroupId, - ); + const created = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.cloneFeatureToggle( + featureName, + projectId, + name, + req.audit, + replaceGroupId, + ), + ); this.openApiService.respondWithValidation( 201, @@ -680,14 +675,18 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId } = req.params; - const created = await this.featureService.createFeatureToggle( - projectId, - { - ...req.body, - description: req.body.description || undefined, - }, - req.audit, - ); + const created = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.createFeatureToggle( + projectId, + { + ...req.body, + description: req.body.description || undefined, + }, + req.audit, + ), + ); this.openApiService.respondWithValidation( 201, @@ -762,15 +761,19 @@ export default class ProjectFeaturesController extends Controller { if (data.name && data.name !== featureName) { throw new BadDataError('Cannot change name of feature flag'); } - const created = await this.featureService.updateFeatureToggle( - projectId, - { - ...data, - name: featureName, - }, - featureName, - req.audit, - ); + const created = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.updateFeatureToggle( + projectId, + { + ...data, + name: featureName, + }, + featureName, + req.audit, + ), + ); this.openApiService.respondWithValidation( 200, @@ -790,12 +793,16 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { projectId, featureName } = req.params; - const updated = await this.featureService.patchFeature( - projectId, - featureName, - req.body, - req.audit, - ); + const updated = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.patchFeature( + projectId, + featureName, + req.body, + req.audit, + ), + ); this.openApiService.respondWithValidation( 200, res, @@ -814,13 +821,8 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { featureName, projectId } = req.params; - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).archiveToggle( - featureName, - req.user, - req.audit, - projectId, - ), + await this.transactionalFeatureToggleService.transactional((service) => + service.archiveToggle(featureName, req.user, req.audit, projectId), ); res.status(202).send(); } @@ -887,14 +889,16 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { featureName, environment, projectId } = req.params; const { shouldActivateDisabledStrategies } = req.query; - await this.featureService.updateEnabled( - projectId, - featureName, - environment, - true, - req.audit, - req.user, - shouldActivateDisabledStrategies === 'true', + await this.transactionalFeatureToggleService.transactional((service) => + service.updateEnabled( + projectId, + featureName, + environment, + true, + req.audit, + req.user, + shouldActivateDisabledStrategies === 'true', + ), ); res.status(200).end(); } @@ -917,8 +921,8 @@ export default class ProjectFeaturesController extends Controller { return; } - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).bulkUpdateEnabled( + await this.transactionalFeatureToggleService.transactional((service) => + service.bulkUpdateEnabled( projectId, features, environment, @@ -949,8 +953,8 @@ export default class ProjectFeaturesController extends Controller { return; } - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).bulkUpdateEnabled( + await this.transactionalFeatureToggleService.transactional((service) => + service.bulkUpdateEnabled( projectId, features, environment, @@ -968,13 +972,15 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { featureName, environment, projectId } = req.params; - await this.featureService.updateEnabled( - projectId, - featureName, - environment, - false, - req.audit, - req.user, + await this.transactionalFeatureToggleService.transactional((service) => + service.updateEnabled( + projectId, + featureName, + environment, + false, + req.audit, + req.user, + ), ); res.status(200).end(); } @@ -994,12 +1000,16 @@ export default class ProjectFeaturesController extends Controller { strategyConfig.segmentIds = []; } - const strategy = await this.featureService.createStrategy( - strategyConfig, - { environment, projectId, featureName }, - req.audit, - req.user, - ); + const strategy = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.createStrategy( + strategyConfig, + { environment, projectId, featureName }, + req.audit, + req.user, + ), + ); const updatedStrategy = await this.featureService.getStrategy( strategy.id, @@ -1031,10 +1041,8 @@ export default class ProjectFeaturesController extends Controller { res: Response, ): Promise { const { featureName, projectId, environment } = req.params; - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService( - tx, - ).updateStrategiesSortOrder( + await this.transactionalFeatureToggleService.transactional((service) => + service.updateStrategiesSortOrder( { featureName, environment, @@ -1058,15 +1066,17 @@ export default class ProjectFeaturesController extends Controller { req.body.segmentIds = []; } - const updatedStrategy = await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).updateStrategy( - strategyId, - req.body, - { environment, projectId, featureName }, - req.audit, - req.user, - ), - ); + const updatedStrategy = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.updateStrategy( + strategyId, + req.body, + { environment, projectId, featureName }, + req.audit, + req.user, + ), + ); res.status(200).json(updatedStrategy); } @@ -1083,15 +1093,17 @@ export default class ProjectFeaturesController extends Controller { throwOnInvalidSchema(featureStrategySchema.$id, newDocument); - const updatedStrategy = await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).updateStrategy( - strategyId, - newDocument, - { environment, projectId, featureName }, - req.audit, - req.user, - ), - ); + const updatedStrategy = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.updateStrategy( + strategyId, + newDocument, + { environment, projectId, featureName }, + req.audit, + req.user, + ), + ); res.status(200).json(updatedStrategy); } @@ -1115,36 +1127,17 @@ export default class ProjectFeaturesController extends Controller { const { environment, projectId, featureName } = req.params; const { strategyId } = req.params; this.logger.info(strategyId); - const strategy = await this.featureService.deleteStrategy( - strategyId, - { environment, projectId, featureName }, - req.audit, - req.user, - ); - res.status(200).json(strategy); - } - - async updateStrategyParameter( - req: IAuthRequest< - StrategyIdParams, - any, - { name: string; value: string | number }, - any - >, - res: Response, - ): Promise { - const { strategyId, environment, projectId, featureName } = req.params; - const { name, value } = req.body; - - const updatedStrategy = - await this.featureService.updateStrategyParameter( - strategyId, - name, - value, - { environment, projectId, featureName }, - req.audit, + const strategy = + await this.transactionalFeatureToggleService.transactional( + (service) => + service.deleteStrategy( + strategyId, + { environment, projectId, featureName }, + req.audit, + req.user, + ), ); - res.status(200).json(updatedStrategy); + res.status(200).json(strategy); } async updateFeaturesTags( diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index bfdecf14cf..0bbe7311b6 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -31,7 +31,6 @@ import type { OpenApiService } from '../../services'; import type { IAuthRequest } from '../../routes/unleash-types'; import { ProjectApiTokenController } from '../../routes/admin-api/project/api-token'; import ProjectArchiveController from '../../routes/admin-api/project/project-archive'; -import { createKnexTransactionStarter } from '../../db/transaction'; import type { Db } from '../../db/db'; import DependentFeaturesController from '../dependent-features/dependent-features-controller'; import type { ProjectOverviewSchema } from '../../openapi/spec/project-overview-schema'; @@ -222,14 +221,7 @@ export default class ProjectController extends Controller { ], }); - this.use( - '/', - new ProjectFeaturesController( - config, - services, - createKnexTransactionStarter(db), - ).router, - ); + this.use('/', new ProjectFeaturesController(config, services).router); this.use('/', new DependentFeaturesController(config, services).router); this.use( '/', @@ -238,14 +230,7 @@ export default class ProjectController extends Controller { this.use('/', new ProjectHealthReport(config, services).router); this.use('/', new VariantsController(config, services).router); this.use('/', new ProjectApiTokenController(config, services).router); - this.use( - '/', - new ProjectArchiveController( - config, - services, - createKnexTransactionStarter(db), - ).router, - ); + this.use('/', new ProjectArchiveController(config, services).router); this.use('/', new ProjectInsightsController(config, services).router); this.use('/', new ProjectStatusController(config, services).router); this.use('/', new FeatureLifecycleController(config, services).router); diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 77061487b8..1d50fd18ad 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -28,7 +28,6 @@ import InstanceAdminController from './instance-admin'; import TelemetryController from './telemetry'; import FavoritesController from './favorites'; import MaintenanceController from '../../features/maintenance/maintenance-controller'; -import { createKnexTransactionStarter } from '../../db/transaction'; import type { Db } from '../../db/db'; import ExportImportController from '../../features/export-import-toggles/export-import-controller'; import { SegmentsController } from '../../features/segment/segment-controller'; @@ -53,11 +52,7 @@ export class AdminApi extends Controller { ); this.app.use( '/archive', - new ArchiveController( - config, - services, - createKnexTransactionStarter(db), - ).router, + new ArchiveController(config, services).router, ); this.app.use( '/strategies', diff --git a/src/lib/routes/admin-api/project/project-archive.ts b/src/lib/routes/admin-api/project/project-archive.ts index 74bf50ec41..ac72074da8 100644 --- a/src/lib/routes/admin-api/project/project-archive.ts +++ b/src/lib/routes/admin-api/project/project-archive.ts @@ -21,10 +21,7 @@ import { createResponseSchema, } from '../../../openapi'; import Controller from '../../controller'; -import type { - TransactionCreator, - UnleashTransaction, -} from '../../../db/transaction'; +import type { WithTransactional } from '../../../db/transaction'; const PATH = '/:projectId'; const PATH_ARCHIVE = `${PATH}/archive`; @@ -37,10 +34,7 @@ export default class ProjectArchiveController extends Controller { private featureService: FeatureToggleService; - private transactionalFeatureToggleService: ( - db: UnleashTransaction, - ) => FeatureToggleService; - private readonly startTransaction: TransactionCreator; + private transactionalFeatureToggleService: WithTransactional; private openApiService: OpenApiService; @@ -58,7 +52,6 @@ export default class ProjectArchiveController extends Controller { | 'featureToggleService' | 'openApiService' >, - startTransaction: TransactionCreator, ) { super(config); this.logger = config.getLogger('/admin-api/archive.js'); @@ -67,7 +60,6 @@ export default class ProjectArchiveController extends Controller { this.flagResolver = config.flagResolver; this.transactionalFeatureToggleService = transactionalFeatureToggleService; - this.startTransaction = startTransaction; this.route({ method: 'post', @@ -178,12 +170,8 @@ export default class ProjectArchiveController extends Controller { ): Promise { const { projectId } = req.params; const { features } = req.body; - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).reviveFeatures( - features, - projectId, - req.audit, - ), + await this.transactionalFeatureToggleService.transactional((service) => + service.reviveFeatures(features, projectId, req.audit), ); res.status(200).end(); } @@ -195,13 +183,8 @@ export default class ProjectArchiveController extends Controller { const { features } = req.body; const { projectId } = req.params; - await this.startTransaction(async (tx) => - this.transactionalFeatureToggleService(tx).archiveToggles( - features, - req.user, - req.audit, - projectId, - ), + await this.transactionalFeatureToggleService.transactional((service) => + service.archiveToggles(features, req.user, req.audit, projectId), ); res.status(202).end(); diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 6f2c6e82c1..29c47eb5f6 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -69,6 +69,7 @@ import { createFakeAccessService, createFakeEnvironmentService, createFakeEventsService, + createFakeFeatureToggleService, createFakeProjectService, createFakeUserSubscriptionsService, createFeatureLifecycleService, @@ -133,8 +134,6 @@ import { createFakeApiTokenService, } from '../features/api-tokens/createApiTokenService'; import { IntegrationEventsService } from '../features/integration-events/integration-events-service'; -import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model'; -import { FakeFeatureCollaboratorsReadModel } from '../features/feature-toggle/fake-feature-collaborators-read-model'; import { createFakePlaygroundService, createPlaygroundService, @@ -160,8 +159,6 @@ import { } from '../features/context/createContextService'; import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; import { createFakeFeatureLinkService } from '../features/feature-links/createFeatureLinkService'; -import { FeatureLinksReadModel } from '../features/feature-links/feature-links-read-model'; -import { FakeFeatureLinksReadModel } from '../features/feature-links/fake-feature-links-read-model'; import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service'; export const createServices = ( @@ -287,26 +284,6 @@ export const createServices = ( ? createFeatureSearchService(config)(db) : createFakeFeatureSearchService(config); - const featureCollaboratorsReadModel = db - ? new FeatureCollaboratorsReadModel(db) - : new FakeFeatureCollaboratorsReadModel(); - - const featureLinksReadModel = db - ? new FeatureLinksReadModel(db, config.eventBus) - : new FakeFeatureLinksReadModel(); - - const featureToggleService = new FeatureToggleService(stores, config, { - segmentService, - accessService, - eventService, - changeRequestAccessReadModel, - privateProjectChecker, - dependentFeaturesReadModel, - dependentFeaturesService, - featureLifecycleReadModel, - featureCollaboratorsReadModel, - featureLinksReadModel, - }); const transactionalEnvironmentService = db ? withTransactional(createEnvironmentService(config), db) : withFakeTransactional(createFakeEnvironmentService(config)); @@ -346,8 +323,12 @@ export const createServices = ( const importService = db ? withTransactional(deferredExportImportTogglesService(config), db) : withFakeTransactional(createFakeExportImportTogglesService(config)); - const transactionalFeatureToggleService = (txDb: Knex.Transaction) => - createFeatureToggleService(txDb, config); + const featureToggleService = db + ? withTransactional((db) => createFeatureToggleService(db, config), db) + : withFakeTransactional( + createFakeFeatureToggleService(config).featureToggleService, + ); + const transactionalFeatureToggleService = featureToggleService; const transactionalGroupService = (txDb: Knex.Transaction) => createGroupService(txDb, config); const userSplashService = new UserSplashService(stores, config); diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 54c6963c55..0e597f5a15 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -114,9 +114,7 @@ export interface IUnleashServices { configurationRevisionService: ConfigurationRevisionService; schedulerService: SchedulerService; eventAnnouncerService: EventAnnouncerService; - transactionalFeatureToggleService: ( - db: Knex.Transaction, - ) => FeatureToggleService; + transactionalFeatureToggleService: WithTransactional; transactionalGroupService: (db: Knex.Transaction) => GroupService; privateProjectChecker: IPrivateProjectChecker; dependentFeaturesService: DependentFeaturesService; diff --git a/src/test/e2e/api/admin/maintenance.e2e.test.ts b/src/test/e2e/api/admin/maintenance.e2e.test.ts index 9e199e45be..cec0f71263 100644 --- a/src/test/e2e/api/admin/maintenance.e2e.test.ts +++ b/src/test/e2e/api/admin/maintenance.e2e.test.ts @@ -35,7 +35,11 @@ test('should not allow to create feature flags in maintenance mode', async () => }); test('maintenance mode is off by default', async () => { - const appWithMaintenanceMode = await setupApp(db.stores); + const appWithMaintenanceMode = await setupAppWithCustomConfig( + db.stores, + {}, + db.rawDatabase, + ); return appWithMaintenanceMode.request .post('/api/admin/projects/default/features') diff --git a/src/test/e2e/api/admin/project/variants-sunset.e2e.test.ts b/src/test/e2e/api/admin/project/variants-sunset.e2e.test.ts index 8c6fa8bf4f..0284194669 100644 --- a/src/test/e2e/api/admin/project/variants-sunset.e2e.test.ts +++ b/src/test/e2e/api/admin/project/variants-sunset.e2e.test.ts @@ -11,14 +11,18 @@ let db: ITestDb; beforeAll(async () => { db = await dbInit('project_feature_variants_api_sunset', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, - enableLegacyVariants: false, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + enableLegacyVariants: false, + }, }, }, - }); + db.rawDatabase, + ); }); afterAll(async () => { diff --git a/src/test/e2e/api/admin/project/variants.e2e.test.ts b/src/test/e2e/api/admin/project/variants.e2e.test.ts index 73d5c9188b..e0619cbe1e 100644 --- a/src/test/e2e/api/admin/project/variants.e2e.test.ts +++ b/src/test/e2e/api/admin/project/variants.e2e.test.ts @@ -12,14 +12,18 @@ let db: ITestDb; beforeAll(async () => { db = await dbInit('project_feature_variants_api_serial', getLogger); - app = await setupAppWithCustomConfig(db.stores, { - experimental: { - flags: { - strictSchemaValidation: true, - enableLegacyVariants: true, + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + enableLegacyVariants: true, + }, }, }, - }); + db.rawDatabase, + ); }); afterAll(async () => { diff --git a/src/test/e2e/api/admin/user/pat.e2e.test.ts b/src/test/e2e/api/admin/user/pat.e2e.test.ts index 50d93c1628..7b7a25736d 100644 --- a/src/test/e2e/api/admin/user/pat.e2e.test.ts +++ b/src/test/e2e/api/admin/user/pat.e2e.test.ts @@ -19,7 +19,7 @@ beforeAll(async () => { getLogger.setMuteError(true); db = await dbInit('user_pat', getLogger); patStore = db.stores.patStore; - app = await setupAppWithAuth(db.stores); + app = await setupAppWithAuth(db.stores, {}, db.rawDatabase); await app.request .post(`/auth/demo/login`)