From 002233e7f6f01e35f31735adf02692cd582141f7 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 6 May 2025 09:31:45 +0200 Subject: [PATCH] feat: feature link backend stub (#9893) --- src/lib/db/index.ts | 2 + .../feature-links/createFeatureLinkService.ts | 17 ++ .../feature-links/fake-feature-link-store.ts | 53 ++++++ .../feature-links/feature-link-controller.ts | 158 ++++++++++++++++++ .../feature-link-service.test.ts | 60 +++++++ .../feature-links/feature-link-service.ts | 108 ++++++++++++ .../feature-links/feature-link-store-type.ts | 13 ++ .../feature-links/feature-link.e2e.test.ts | 63 +++++++ src/lib/features/index.ts | 1 + .../features/project/project-controller.ts | 2 + src/lib/openapi/spec/feature-link-schema.ts | 26 +++ src/lib/openapi/spec/index.ts | 1 + src/lib/services/index.ts | 6 + src/lib/types/events.ts | 77 +++++++++ src/lib/types/services.ts | 2 + src/lib/types/stores.ts | 3 + src/test/fixtures/store.ts | 2 + 17 files changed, 594 insertions(+) create mode 100644 src/lib/features/feature-links/createFeatureLinkService.ts create mode 100644 src/lib/features/feature-links/fake-feature-link-store.ts create mode 100644 src/lib/features/feature-links/feature-link-controller.ts create mode 100644 src/lib/features/feature-links/feature-link-service.test.ts create mode 100644 src/lib/features/feature-links/feature-link-service.ts create mode 100644 src/lib/features/feature-links/feature-link-store-type.ts create mode 100644 src/lib/features/feature-links/feature-link.e2e.test.ts create mode 100644 src/lib/openapi/spec/feature-link-schema.ts diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index f27547a8e7..9fe2894f51 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -65,6 +65,7 @@ import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubs import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model'; import { UniqueConnectionStore } from '../features/unique-connection/unique-connection-store'; import { UniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model'; +import FakeFeatureLinkStore from '../features/feature-links/fake-feature-link-store'; export const createStores = ( config: IUnleashConfig, @@ -201,6 +202,7 @@ export const createStores = ( releasePlanMilestoneStore: new ReleasePlanMilestoneStore(db, config), releasePlanMilestoneStrategyStore: new ReleasePlanMilestoneStrategyStore(db, config), + featureLinkStore: new FakeFeatureLinkStore(), }; }; diff --git a/src/lib/features/feature-links/createFeatureLinkService.ts b/src/lib/features/feature-links/createFeatureLinkService.ts new file mode 100644 index 0000000000..b40f2fd921 --- /dev/null +++ b/src/lib/features/feature-links/createFeatureLinkService.ts @@ -0,0 +1,17 @@ +import type { IUnleashConfig } from '../../types'; +import FeatureLinkService from './feature-link-service'; +import FakeFeatureLinkStore from './fake-feature-link-store'; +import { createFakeEventsService } from '../events/createEventsService'; + +export const createFakeFeatureLinkService = (config: IUnleashConfig) => { + const eventService = createFakeEventsService(config); + const featureLinkStore = new FakeFeatureLinkStore(); + + const featureLinkService = new FeatureLinkService( + { featureLinkStore }, + config, + eventService, + ); + + return { featureLinkService, featureLinkStore }; +}; diff --git a/src/lib/features/feature-links/fake-feature-link-store.ts b/src/lib/features/feature-links/fake-feature-link-store.ts new file mode 100644 index 0000000000..d9cd2a32c2 --- /dev/null +++ b/src/lib/features/feature-links/fake-feature-link-store.ts @@ -0,0 +1,53 @@ +import { NotFoundError } from '../../error'; +import type { + IFeatureLink, + IFeatureLinkStore, +} from './feature-link-store-type'; + +export default class FakeFeatureLinkStore implements IFeatureLinkStore { + private links: IFeatureLink[] = []; + + async create(link: Omit): Promise { + const newLink: IFeatureLink = { + ...link, + id: String(Math.random()), + }; + this.links.push(newLink); + return newLink; + } + + async delete(id: string): Promise { + const index = this.links.findIndex((link) => link.id === id); + if (index !== -1) { + this.links.splice(index, 1); + } + } + + async deleteAll(): Promise { + this.links = []; + } + + destroy(): void {} + + async exists(id: string): Promise { + return this.links.some((link) => link.id === id); + } + + async get(id: string): Promise { + const link = this.links.find((link) => link.id === id); + if (link) { + return link; + } + throw new NotFoundError('Could not find feature link'); + } + + async getAll(): Promise { + return this.links; + } + + async update(link: IFeatureLink): Promise { + await this.delete(link.id); + this.links.push(link); + return link; + } +} diff --git a/src/lib/features/feature-links/feature-link-controller.ts b/src/lib/features/feature-links/feature-link-controller.ts new file mode 100644 index 0000000000..a191272897 --- /dev/null +++ b/src/lib/features/feature-links/feature-link-controller.ts @@ -0,0 +1,158 @@ +import type { Response } from 'express'; +import Controller from '../../routes/controller'; +import type { IAuthRequest } from '../../routes/unleash-types'; +import type { IUnleashConfig } from '../../types/option'; +import type { WithTransactional } from '../../db/transaction'; +import type FeatureLinkService from './feature-link-service'; +import { UPDATE_FEATURE } from '../../types/permissions'; +import type { OpenApiService } from '../../services/openapi-service'; +import { + emptyResponse, + getStandardResponses, +} from '../../openapi/util/standard-responses'; +import { createRequestSchema } from '../../openapi/util/create-request-schema'; +import type { IFeatureLink } from './feature-link-store-type'; + +interface FeatureLinkServices { + transactionalFeatureLinkService: WithTransactional; + openApiService: OpenApiService; +} + +const PATH = '/:projectId/features/:featureName/link'; +const PATH_LINK = '/:projectId/features/:featureName/link/:linkId'; + +export default class FeatureLinkController extends Controller { + private transactionalFeatureLinkService: WithTransactional; + private openApiService: OpenApiService; + + constructor( + config: IUnleashConfig, + { + transactionalFeatureLinkService, + openApiService, + }: FeatureLinkServices, + ) { + super(config); + this.transactionalFeatureLinkService = transactionalFeatureLinkService; + this.openApiService = openApiService; + + this.route({ + method: 'post', + path: PATH, + handler: this.createFeatureLink, + permission: UPDATE_FEATURE, + middleware: [ + openApiService.validPath({ + tags: ['Unstable'], + operationId: 'createFeatureLink', + summary: 'Create a feature link', + description: 'Create a new link for a feature.', + responses: { + 204: emptyResponse, + ...getStandardResponses(400, 401, 403, 415), + }, + requestBody: createRequestSchema('featureLinkSchema'), + }), + ], + }); + + this.route({ + method: 'put', + path: PATH_LINK, + handler: this.updateFeatureLink, + permission: UPDATE_FEATURE, + middleware: [ + openApiService.validPath({ + tags: ['Unstable'], + operationId: 'updateFeatureLink', + summary: 'Update a feature link', + description: 'Update an existing feature link.', + responses: { + 204: emptyResponse, + ...getStandardResponses(400, 401, 403, 404, 415), + }, + requestBody: createRequestSchema('featureLinkSchema'), + }), + ], + }); + + this.route({ + method: 'delete', + path: PATH_LINK, + handler: this.deleteFeatureLink, + acceptAnyContentType: true, + permission: UPDATE_FEATURE, + middleware: [ + openApiService.validPath({ + tags: ['Unstable'], + operationId: 'deleteFeatureLink', + summary: 'Delete a feature link', + description: 'Delete a feature link by id.', + responses: { + 204: emptyResponse, + ...getStandardResponses(401, 403, 404), + }, + }), + ], + }); + } + + async createFeatureLink( + req: IAuthRequest< + { projectId: string; featureName: string }, + unknown, + Omit + >, + res: Response, + ): Promise { + const { projectId, featureName } = req.params; + + await this.transactionalFeatureLinkService.transactional((service) => + service.createLink( + projectId, + { ...req.body, featureName }, + req.audit, + ), + ); + + res.status(204).end(); + } + + async updateFeatureLink( + req: IAuthRequest< + { projectId: string; linkId: string; featureName: string }, + unknown, + Omit + >, + res: Response, + ): Promise { + const { projectId, linkId, featureName } = req.params; + + await this.transactionalFeatureLinkService.transactional((service) => + service.updateLink( + { projectId, linkId }, + { ...req.body, featureName }, + req.audit, + ), + ); + + res.status(204).end(); + } + + async deleteFeatureLink( + req: IAuthRequest< + { projectId: string; linkId: string }, + unknown, + Omit + >, + res: Response, + ): Promise { + const { projectId, linkId } = req.params; + + await this.transactionalFeatureLinkService.transactional((service) => + service.deleteLink({ projectId, linkId }, req.audit), + ); + + res.status(204).end(); + } +} diff --git a/src/lib/features/feature-links/feature-link-service.test.ts b/src/lib/features/feature-links/feature-link-service.test.ts new file mode 100644 index 0000000000..b26dfc19c3 --- /dev/null +++ b/src/lib/features/feature-links/feature-link-service.test.ts @@ -0,0 +1,60 @@ +import { createFakeFeatureLinkService } from './createFeatureLinkService'; +import type { IAuditUser, IUnleashConfig } from '../../types'; +import getLogger from '../../../test/fixtures/no-logger'; +import { NotFoundError } from '../../error'; + +test('create, update and delete feature link', async () => { + const { featureLinkStore, featureLinkService } = + createFakeFeatureLinkService({ + getLogger, + } as unknown as IUnleashConfig); + + const link = await featureLinkService.createLink( + 'default', + { featureName: 'feature', url: 'example.com', title: 'some title' }, + {} as IAuditUser, + ); + expect(link).toMatchObject({ + featureName: 'feature', + url: 'example.com', + title: 'some title', + }); + + const newLink = await featureLinkService.updateLink( + { projectId: 'default', linkId: link.id }, + { title: 'new title', url: 'example1.com', featureName: 'feature' }, + {} as IAuditUser, + ); + expect(newLink).toMatchObject({ + featureName: 'feature', + url: 'example1.com', + title: 'new title', + }); + + await featureLinkService.deleteLink( + { projectId: 'default', linkId: link.id }, + {} as IAuditUser, + ); + expect(await featureLinkStore.getAll()).toMatchObject([]); +}); + +test('cannot delete/update non existent link', async () => { + const { featureLinkStore, featureLinkService } = + createFakeFeatureLinkService({ + getLogger, + } as unknown as IUnleashConfig); + + await expect( + featureLinkService.updateLink( + { projectId: 'default', linkId: 'nonexitent' }, + { title: 'new title', url: 'example1.com', featureName: 'feature' }, + {} as IAuditUser, + ), + ).rejects.toThrow(NotFoundError); + await expect( + featureLinkService.deleteLink( + { projectId: 'default', linkId: 'nonexitent' }, + {} as IAuditUser, + ), + ).rejects.toThrow(NotFoundError); +}); diff --git a/src/lib/features/feature-links/feature-link-service.ts b/src/lib/features/feature-links/feature-link-service.ts new file mode 100644 index 0000000000..9b5e65dfbb --- /dev/null +++ b/src/lib/features/feature-links/feature-link-service.ts @@ -0,0 +1,108 @@ +import type { Logger } from '../../logger'; +import { + type IUnleashConfig, + type IAuditUser, + FeatureLinkAddedEvent, + FeatureLinkUpdatedEvent, + FeatureLinkRemovedEvent, +} from '../../types'; +import type { + IFeatureLink, + IFeatureLinkStore, +} from './feature-link-store-type'; +import type EventService from '../events/event-service'; +import { NotFoundError } from '../../error'; + +interface IFeatureLinkStoreObj { + featureLinkStore: IFeatureLinkStore; +} + +export default class FeatureLinkService { + private logger: Logger; + private featureLinkStore: IFeatureLinkStore; + private eventService: EventService; + + constructor( + stores: IFeatureLinkStoreObj, + { getLogger }: Pick, + eventService: EventService, + ) { + this.logger = getLogger('feature-links/feature-link-service.ts'); + this.featureLinkStore = stores.featureLinkStore; + this.eventService = eventService; + } + + async getAll(): Promise { + return this.featureLinkStore.getAll(); + } + + async createLink( + projectId: string, + newLink: Omit, + auditUser: IAuditUser, + ): Promise { + const link = await this.featureLinkStore.create(newLink); + + await this.eventService.storeEvent( + new FeatureLinkAddedEvent({ + featureName: newLink.featureName, + project: projectId, + data: { url: newLink.url, title: newLink.title }, + auditUser, + }), + ); + + return link; + } + + async updateLink( + { projectId, linkId }: { projectId: string; linkId: string }, + updatedLink: Omit, + auditUser: IAuditUser, + ): Promise { + const preData = await this.featureLinkStore.get(linkId); + + if (!preData) { + throw new NotFoundError(`Could not find link with id ${linkId}`); + } + + const link = await this.featureLinkStore.update({ + ...updatedLink, + id: linkId, + }); + + await this.eventService.storeEvent( + new FeatureLinkUpdatedEvent({ + featureName: updatedLink.featureName, + project: projectId, + data: { url: link.url, title: link.title }, + preData: { url: preData.url, title: preData.title }, + auditUser, + }), + ); + + return link; + } + + async deleteLink( + { projectId, linkId }: { projectId: string; linkId: string }, + auditUser: IAuditUser, + ): Promise { + const link = await this.featureLinkStore.get(linkId); + + if (!link) { + throw new NotFoundError(`Could not find link with id ${linkId}`); + } + + await this.featureLinkStore.delete(linkId); + + await this.eventService.storeEvent( + new FeatureLinkRemovedEvent({ + featureName: link.featureName, + project: projectId, + preData: { url: link.url, title: link.title }, + auditUser, + }), + ); + } +} diff --git a/src/lib/features/feature-links/feature-link-store-type.ts b/src/lib/features/feature-links/feature-link-store-type.ts new file mode 100644 index 0000000000..ccc8141c5f --- /dev/null +++ b/src/lib/features/feature-links/feature-link-store-type.ts @@ -0,0 +1,13 @@ +import type { Store } from '../../types/stores/store'; + +export interface IFeatureLink { + id: string; + featureName: string; + url: string; + title?: string; +} + +export interface IFeatureLinkStore extends Store { + create(link: Omit): Promise; + update(link: IFeatureLink): Promise; +} diff --git a/src/lib/features/feature-links/feature-link.e2e.test.ts b/src/lib/features/feature-links/feature-link.e2e.test.ts new file mode 100644 index 0000000000..7e791b1cd8 --- /dev/null +++ b/src/lib/features/feature-links/feature-link.e2e.test.ts @@ -0,0 +1,63 @@ +import { + type IUnleashTest, + setupAppWithAuth, +} from '../../../test/e2e/helpers/test-helper'; +import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; +import type { IEventStore, IFeatureLinkStore } from '../../types'; +import getLogger from '../../../test/fixtures/no-logger'; +import type { FeatureLinkSchema } from '../../openapi/spec/feature-link-schema'; + +let app: IUnleashTest; +let db: ITestDb; +let featureLinkStore: IFeatureLinkStore; +let eventStore: IEventStore; + +beforeAll(async () => { + db = await dbInit('feature_link', getLogger, { + dbInitMethod: 'legacy' as const, + }); + app = await setupAppWithAuth( + db.stores, + { + experimental: { + flags: {}, + }, + }, + db.rawDatabase, + ); + eventStore = db.stores.eventStore; + featureLinkStore = db.stores.featureLinkStore; + + await app.request + .post(`/auth/demo/login`) + .send({ + email: 'user@getunleash.io', + }) + .expect(200); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +beforeEach(async () => { + await featureLinkStore.deleteAll(); +}); + +const addLink = async ( + featureName: string, + link: FeatureLinkSchema, + expectedCode = 204, +) => { + return app.request + .post(`/api/admin/projects/default/features/${featureName}/link`) + .send(link) + .expect(expectedCode); +}; + +test('should add feature links', async () => { + await app.createFeature('my_feature'); + + await addLink('my_feature', { url: 'example.com', title: 'feature link' }); +}); diff --git a/src/lib/features/index.ts b/src/lib/features/index.ts index 1f5ddc2e3d..1977beae92 100644 --- a/src/lib/features/index.ts +++ b/src/lib/features/index.ts @@ -14,3 +14,4 @@ export * from './playground/createPlaygroundService'; export * from './personal-dashboard/createPersonalDashboardService'; export * from './user-subscriptions/createUserSubscriptionsService'; export * from './context/createContextService'; +export * from './feature-links/createFeatureLinkService'; diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index 525b50a060..bfdecf14cf 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -49,6 +49,7 @@ import { type ProjectFlagCreatorsSchema, } from '../../openapi/spec/project-flag-creators-schema'; import ProjectStatusController from '../project-status/project-status-controller'; +import FeatureLinkController from '../feature-links/feature-link-controller'; export default class ProjectController extends Controller { private projectService: ProjectService; @@ -248,6 +249,7 @@ export default class ProjectController extends Controller { this.use('/', new ProjectInsightsController(config, services).router); this.use('/', new ProjectStatusController(config, services).router); this.use('/', new FeatureLifecycleController(config, services).router); + this.use('/', new FeatureLinkController(config, services).router); } async getProjects( diff --git a/src/lib/openapi/spec/feature-link-schema.ts b/src/lib/openapi/spec/feature-link-schema.ts new file mode 100644 index 0000000000..8b06a3e7fb --- /dev/null +++ b/src/lib/openapi/spec/feature-link-schema.ts @@ -0,0 +1,26 @@ +import type { FromSchema } from 'json-schema-to-ts'; + +export const featureLinkSchema = { + $id: '#/components/schemas/featureLinkSchema', + type: 'object', + required: ['url'], + properties: { + url: { + type: 'string', + example: 'https://github.com/search?q=cleanupReminder&type=code', + description: 'The URL the feature is linked to', + }, + title: { + type: 'string', + example: 'Github cleanup', + description: 'The description of the link', + nullable: true, + }, + }, + description: 'The link to any URL related to the feature', + components: { + schemas: {}, + }, +} as const; + +export type FeatureLinkSchema = FromSchema; diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index 8f5f6ec7d8..0ec79acafd 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -85,6 +85,7 @@ export * from './feature-events-schema'; export * from './feature-lifecycle-completed-schema'; export * from './feature-lifecycle-count-schema'; export * from './feature-lifecycle-schema'; +export * from './feature-link-schema'; export * from './feature-metrics-schema'; export * from './feature-schema'; export * from './feature-search-environment-schema'; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index b541e3954a..0cc8793be4 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -158,6 +158,7 @@ import { createFakeContextService, } from '../features/context/createContextService'; import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; +import { createFakeFeatureLinkService } from '../features/feature-links/createFeatureLinkService'; export const createServices = ( stores: IUnleashStores, @@ -424,6 +425,10 @@ export const createServices = ( ? withTransactional(createUserSubscriptionsService(config), db) : withFakeTransactional(createFakeUserSubscriptionsService(config)); + const transactionalFeatureLinkService = withFakeTransactional( + createFakeFeatureLinkService(config).featureLinkService, + ); + return { transactionalAccessService, accessService, @@ -493,6 +498,7 @@ export const createServices = ( transactionalUserSubscriptionsService, uniqueConnectionService, featureLifecycleReadModel, + transactionalFeatureLinkService, }; }; diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index ac8d181126..ebdcce803d 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -18,6 +18,9 @@ export const APPLICATION_CREATED = 'application-created' as const; export const FEATURE_CREATED = 'feature-created' as const; export const FEATURE_DELETED = 'feature-deleted' as const; export const FEATURE_UPDATED = 'feature-updated' as const; +export const FEATURE_LINK_ADDED = 'feature-link-added' as const; +export const FEATURE_LINK_REMOVED = 'feature-link-removed' as const; +export const FEATURE_LINK_UPDATED = 'feature-link-updated' as const; export const FEATURE_DEPENDENCY_ADDED = 'feature-dependency-added' as const; export const FEATURE_DEPENDENCY_REMOVED = 'feature-dependency-removed' as const; export const FEATURE_DEPENDENCIES_REMOVED = @@ -242,6 +245,9 @@ export const IEventTypes = [ FEATURE_TYPE_UPDATED, FEATURE_COMPLETED, FEATURE_UNCOMPLETED, + FEATURE_LINK_ADDED, + FEATURE_LINK_REMOVED, + FEATURE_LINK_UPDATED, STRATEGY_ORDER_CHANGED, DROP_FEATURE_TAGS, FEATURE_UNTAGGED, @@ -1023,6 +1029,77 @@ export class FeatureMetadataUpdateEvent extends BaseEvent { } } +export class FeatureLinkAddedEvent extends BaseEvent { + readonly project: string; + + readonly featureName: string; + + readonly data: { url: string; title?: string }; + readonly preData: null; + + constructor(p: { + featureName: string; + project: string; + data: { url: string; title?: string }; + auditUser: IAuditUser; + }) { + super(FEATURE_LINK_ADDED, p.auditUser); + const { project, featureName, data } = p; + this.project = project; + this.featureName = featureName; + this.data = data; + this.preData = null; + } +} + +export class FeatureLinkUpdatedEvent extends BaseEvent { + readonly project: string; + + readonly featureName: string; + + readonly data: { url: string; title?: string }; + + readonly preData: { url: string; title?: string }; + + constructor(p: { + featureName: string; + project: string; + data: { url: string; title?: string }; + preData: { url: string; title?: string }; + auditUser: IAuditUser; + }) { + super(FEATURE_LINK_UPDATED, p.auditUser); + const { project, featureName, data, preData } = p; + this.project = project; + this.featureName = featureName; + this.data = data; + this.preData = preData; + } +} + +export class FeatureLinkRemovedEvent extends BaseEvent { + readonly project: string; + + readonly featureName: string; + + readonly preData: { url: string; title?: string }; + readonly data: null; + + constructor(p: { + featureName: string; + project: string; + preData: { url: string; title?: string }; + auditUser: IAuditUser; + }) { + super(FEATURE_LINK_REMOVED, p.auditUser); + const { project, featureName, preData } = p; + this.project = project; + this.featureName = featureName; + this.data = null; + this.preData = preData; + } +} + export class FeatureStrategyAddEvent extends BaseEvent { readonly project: string; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index e7f06e1ba3..559ada92b7 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -61,6 +61,7 @@ import type { ProjectStatusService } from '../features/project-status/project-st import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service'; import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; import type { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type'; +import type FeatureLinkService from '../features/feature-links/feature-link-service'; export interface IUnleashServices { transactionalAccessService: WithTransactional; @@ -133,4 +134,5 @@ export interface IUnleashServices { transactionalUserSubscriptionsService: WithTransactional; uniqueConnectionService: UniqueConnectionService; featureLifecycleReadModel: IFeatureLifecycleReadModel; + transactionalFeatureLinkService: WithTransactional; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 688c65def7..c31ece2cdb 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -59,6 +59,7 @@ import { ReleasePlanStore } from '../features/release-plans/release-plan-store'; import { ReleasePlanTemplateStore } from '../features/release-plans/release-plan-template-store'; import { ReleasePlanMilestoneStore } from '../features/release-plans/release-plan-milestone-store'; import { ReleasePlanMilestoneStrategyStore } from '../features/release-plans/release-plan-milestone-strategy-store'; +import type { IFeatureLinkStore } from '../features/feature-links/feature-link-store-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -122,6 +123,7 @@ export interface IUnleashStores { releasePlanTemplateStore: ReleasePlanTemplateStore; releasePlanMilestoneStore: ReleasePlanMilestoneStore; releasePlanMilestoneStrategyStore: ReleasePlanMilestoneStrategyStore; + featureLinkStore: IFeatureLinkStore; } export { @@ -183,4 +185,5 @@ export { ReleasePlanTemplateStore, ReleasePlanMilestoneStore, ReleasePlanMilestoneStrategyStore, + type IFeatureLinkStore, }; diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 8d57b1429c..c78521b248 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -62,6 +62,7 @@ import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/ import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model'; import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/fake-unique-connection-store'; import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/unique-connection-read-model'; +import FakeFeatureLinkStore from '../../lib/features/feature-links/fake-feature-link-store'; const db = { select: () => ({ @@ -138,6 +139,7 @@ const createStores: () => IUnleashStores = () => { releasePlanTemplateStore: {} as ReleasePlanTemplateStore, releasePlanMilestoneStrategyStore: {} as ReleasePlanMilestoneStrategyStore, + featureLinkStore: new FakeFeatureLinkStore(), }; };