diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index 318b9a9897..9e7a22cd00 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -61,6 +61,10 @@ import { FakeFeatureCollaboratorsReadModel } from './fake-feature-collaborators- import { FeatureCollaboratorsReadModel } from './feature-collaborators-read-model'; import { FeatureLinksReadModel } from '../feature-links/feature-links-read-model'; import { FakeFeatureLinksReadModel } from '../feature-links/fake-feature-links-read-model'; +import { + createFakeFeatureLinkService, + createFeatureLinkService, +} from '../feature-links/createFeatureLinkService'; export const createFeatureToggleService = ( db: Db, @@ -132,6 +136,8 @@ export const createFeatureToggleService = ( const featureLinksReadModel = new FeatureLinksReadModel(db, eventBus); + const featureLinkService = createFeatureLinkService(config)(db); + const featureToggleService = new FeatureToggleService( { featureStrategiesStore, @@ -155,6 +161,7 @@ export const createFeatureToggleService = ( featureLifecycleReadModel, featureCollaboratorsReadModel, featureLinksReadModel, + featureLinkService, }, ); return featureToggleService; @@ -197,6 +204,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => { const featureCollaboratorsReadModel = new FakeFeatureCollaboratorsReadModel(); const featureLinksReadModel = new FakeFeatureLinksReadModel(); + const { featureLinkService } = createFakeFeatureLinkService(config); const featureToggleService = new FeatureToggleService( { @@ -226,6 +234,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => { featureLifecycleReadModel, featureCollaboratorsReadModel, featureLinksReadModel, + featureLinkService, }, ); return { diff --git a/src/lib/features/feature-toggle/feature-toggle-service.ts b/src/lib/features/feature-toggle/feature-toggle-service.ts index 43de941617..105f725edf 100644 --- a/src/lib/features/feature-toggle/feature-toggle-service.ts +++ b/src/lib/features/feature-toggle/feature-toggle-service.ts @@ -117,6 +117,7 @@ import type { IFeatureLink, IFeatureLinksReadModel, } from '../feature-links/feature-links-read-model-type'; +import type FeatureLinkService from '../feature-links/feature-link-service'; interface IFeatureContext { featureName: string; @@ -176,6 +177,7 @@ export type ServicesAndReadModels = { dependentFeaturesService: DependentFeaturesService; featureLifecycleReadModel: IFeatureLifecycleReadModel; featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel; + featureLinkService: FeatureLinkService; featureLinksReadModel: IFeatureLinksReadModel; }; @@ -218,6 +220,8 @@ class FeatureToggleService { private featureLinksReadModel: IFeatureLinksReadModel; + private featureLinkService: FeatureLinkService; + private dependentFeaturesService: DependentFeaturesService; private eventBus: EventEmitter; @@ -247,6 +251,7 @@ class FeatureToggleService { featureLifecycleReadModel, featureCollaboratorsReadModel, featureLinksReadModel, + featureLinkService, }: ServicesAndReadModels, ) { this.logger = getLogger('services/feature-toggle-service.ts'); @@ -269,6 +274,7 @@ class FeatureToggleService { this.featureLifecycleReadModel = featureLifecycleReadModel; this.featureCollaboratorsReadModel = featureCollaboratorsReadModel; this.featureLinksReadModel = featureLinksReadModel; + this.featureLinkService = featureLinkService; this.eventBus = eventBus; this.resourceLimits = resourceLimits; } @@ -1340,6 +1346,8 @@ class FeatureToggleService { }), ); + await this.addLinksFromTemplates(projectId, featureName, auditUser); + return createdToggle; } throw new NotFoundError( @@ -2543,6 +2551,32 @@ class FeatureToggleService { }); } } + + async addLinksFromTemplates( + projectId: string, + featureName: string, + auditUser: IAuditUser, + ) { + if (!this.flagResolver.isEnabled('projectLinkTemplates')) { + return; + } + + const featureLinksFromTemplates = ( + await this.projectStore.getProjectLinkTemplates(projectId) + ).map((template) => ({ + title: template.title, + url: template.urlTemplate + .replace(/{{project}}/g, projectId) + .replace(/{{feature}}/g, featureName), + featureName, + })); + + return Promise.all( + featureLinksFromTemplates.map((link) => + this.featureLinkService.createLink(projectId, link, auditUser), + ), + ); + } } export default FeatureToggleService; diff --git a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts index d311ff6562..04bda920f8 100644 --- a/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts +++ b/src/lib/features/feature-toggle/tests/feature-toggle-service.e2e.test.ts @@ -25,16 +25,19 @@ import { import type { ISegmentService } from '../../segment/segment-service-interface'; import { createEventsService, + createFeatureLinkService, createFeatureToggleService, createSegmentService, } from '../..'; import { insertLastSeenAt } from '../../../../test/e2e/helpers/test-helper'; import type { EventService } from '../../../services'; +import type FeatureLinkService from '../../feature-links/feature-link-service'; let stores: IUnleashStores; let db: ITestDb; let service: FeatureToggleService; let segmentService: ISegmentService; +let featureLinkService: FeatureLinkService; let eventService: EventService; let environmentService: EnvironmentService; let unleashConfig: IUnleashConfig; @@ -49,19 +52,27 @@ const mockConstraints = (): IConstraint[] => { const irrelevantDate = new Date(); beforeAll(async () => { - const config = createTestConfig({ - experimental: { flags: {} }, - }); + const flags = { + featureLinks: true, + projectLinkTemplates: true, + }; + const config = createTestConfig({ experimental: { flags } }); + db = await dbInit( 'feature_toggle_service_v2_service_serial', config.getLogger, - { dbInitMethod: 'legacy' as const }, + { + dbInitMethod: 'legacy' as const, + experimental: { flags }, + }, ); unleashConfig = config; stores = db.stores; segmentService = createSegmentService(db.rawDatabase, config); + featureLinkService = createFeatureLinkService(config)(db.rawDatabase); + service = createFeatureToggleService(db.rawDatabase, config); eventService = createEventsService(db.rawDatabase, config); @@ -878,3 +889,47 @@ test('Should enable disabled strategies on feature environment enabled', async ( const strategy = await service.getStrategy(createdConfig.id); expect(strategy).toMatchObject({ ...config, disabled: false }); }); + +test('Should add links from templates when creating a feature flag', async () => { + const projectId = 'default'; + const featureName = 'test-link-feature'; + + await stores.projectStore.updateProjectEnterpriseSettings({ + id: projectId, + linkTemplates: [ + { + title: 'Issue tracker', + urlTemplate: + 'https://issues.example.com/project/{{project}}/tasks/{{feature}}', + }, + { + title: 'Docs', + urlTemplate: 'https://docs.example.com/{{project}}/{{feature}}', + }, + ], + }); + + await service.createFeatureToggle( + projectId, + { name: featureName }, + TEST_AUDIT_USER, + ); + + const links = await featureLinkService.getAll(); + + expect(links.length).toBe(2); + expect(links).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + title: 'Issue tracker', + url: `https://issues.example.com/project/${projectId}/tasks/${featureName}`, + featureName, + }), + expect.objectContaining({ + title: 'Docs', + url: `https://docs.example.com/${projectId}/${featureName}`, + featureName, + }), + ]), + ); +}); diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 29c47eb5f6..1a144a2201 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -69,6 +69,7 @@ import { createFakeAccessService, createFakeEnvironmentService, createFakeEventsService, + createFakeFeatureLinkService, createFakeFeatureToggleService, createFakeProjectService, createFakeUserSubscriptionsService, @@ -158,7 +159,6 @@ import { createFakeContextService, } from '../features/context/createContextService'; import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; -import { createFakeFeatureLinkService } from '../features/feature-links/createFeatureLinkService'; import { UnknownFlagsService } from '../features/metrics/unknown-flags/unknown-flags-service'; export const createServices = ( @@ -323,6 +323,15 @@ export const createServices = ( const importService = db ? withTransactional(deferredExportImportTogglesService(config), db) : withFakeTransactional(createFakeExportImportTogglesService(config)); + + const transactionalFeatureLinkService = db + ? withTransactional(createFeatureLinkService(config), db) + : withFakeTransactional( + createFakeFeatureLinkService(config).featureLinkService, + ); + + const featureLinkService = transactionalFeatureLinkService; + const featureToggleService = db ? withTransactional((db) => createFeatureToggleService(db, config), db) : withFakeTransactional( @@ -417,14 +426,6 @@ export const createServices = ( ? withTransactional(createUserSubscriptionsService(config), db) : withFakeTransactional(createFakeUserSubscriptionsService(config)); - const transactionalFeatureLinkService = db - ? withTransactional(createFeatureLinkService(config), db) - : withFakeTransactional( - createFakeFeatureLinkService(config).featureLinkService, - ); - - const featureLinkService = transactionalFeatureLinkService; - return { transactionalAccessService, accessService,