1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: add links to feature read model (#9905)

This commit is contained in:
Mateusz Kwasniewski 2025-05-06 14:57:52 +02:00 committed by GitHub
parent 8e05c92440
commit b9f1d8414c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 119 additions and 8 deletions

View File

@ -0,0 +1,10 @@
import type {
IFeatureLink,
IFeatureLinksReadModel,
} from './feature-links-read-model-type';
export class FakeFeatureLinksReadModel implements IFeatureLinksReadModel {
async getLinks(feature: string): Promise<IFeatureLink[]> {
return [];
}
}

View File

@ -97,6 +97,10 @@ test('should manage feature links', async () => {
featureName: 'my_feature',
},
]);
const { body } = await app.getProjectFeatures('default', 'my_feature');
expect(body.links).toMatchObject([
{ id: links[0].id, title: 'feature link', url: 'example.com' },
]);
await updatedLink('my_feature', links[0].id, {
url: 'example_updated.com',

View File

@ -0,0 +1,9 @@
export interface IFeatureLink {
id: string;
url: string;
title: string | null;
}
export interface IFeatureLinksReadModel {
getLinks(feature: string): Promise<IFeatureLink[]>;
}

View File

@ -0,0 +1,25 @@
import type { Db } from '../../db/db';
import type {
IFeatureLink,
IFeatureLinksReadModel,
} from './feature-links-read-model-type';
export class FeatureLinksReadModel implements IFeatureLinksReadModel {
private db: Db;
constructor(db: Db) {
this.db = db;
}
async getLinks(feature: string): Promise<IFeatureLink[]> {
const links = await this.db
.from('feature_link')
.where('feature_name', feature);
return links.map((link) => ({
id: link.id,
url: link.url,
title: link.title,
}));
}
}

View File

@ -59,6 +59,8 @@ import { FeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycl
import { FakeFeatureLifecycleReadModel } from '../feature-lifecycle/fake-feature-lifecycle-read-model';
import { FakeFeatureCollaboratorsReadModel } from './fake-feature-collaborators-read-model';
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';
export const createFeatureToggleService = (
db: Db,
@ -128,6 +130,8 @@ export const createFeatureToggleService = (
const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db);
const featureLinksReadModel = new FeatureLinksReadModel(db);
const featureToggleService = new FeatureToggleService(
{
featureStrategiesStore,
@ -149,6 +153,7 @@ export const createFeatureToggleService = (
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
featureLinksReadModel,
);
return featureToggleService;
};
@ -189,6 +194,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
const featureCollaboratorsReadModel =
new FakeFeatureCollaboratorsReadModel();
const featureLinksReadModel = new FakeFeatureLinksReadModel();
const featureToggleService = new FeatureToggleService(
{
@ -216,6 +222,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
featureLinksReadModel,
);
return {
featureToggleService,

View File

@ -113,6 +113,10 @@ import type { ResourceLimitsSchema } from '../../openapi';
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
import type { Collaborator } from './types/feature-collaborators-read-model-type';
import { sortStrategies } from '../../util/sortStrategies';
import type {
IFeatureLink,
IFeatureLinksReadModel,
} from '../feature-links/feature-links-read-model-type';
interface IFeatureContext {
featureName: string;
@ -182,6 +186,8 @@ class FeatureToggleService {
private featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
private featureLinksReadModel: IFeatureLinksReadModel;
private dependentFeaturesService: DependentFeaturesService;
private eventBus: EventEmitter;
@ -227,6 +233,7 @@ class FeatureToggleService {
dependentFeaturesService: DependentFeaturesService,
featureLifecycleReadModel: IFeatureLifecycleReadModel,
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel,
featureLinksReadModel: IFeatureLinksReadModel,
) {
this.logger = getLogger('services/feature-toggle-service.ts');
this.featureStrategiesStore = featureStrategiesStore;
@ -247,6 +254,7 @@ class FeatureToggleService {
this.dependentFeaturesService = dependentFeaturesService;
this.featureLifecycleReadModel = featureLifecycleReadModel;
this.featureCollaboratorsReadModel = featureCollaboratorsReadModel;
this.featureLinksReadModel = featureLinksReadModel;
this.eventBus = eventBus;
this.resourceLimits = resourceLimits;
}
@ -442,6 +450,7 @@ class FeatureToggleService {
});
}
}
async validateStrategyType(
strategyName: string | undefined,
): Promise<void> {
@ -1074,14 +1083,19 @@ class FeatureToggleService {
let children: string[] = [];
let lifecycle: IFeatureLifecycleStage | undefined = undefined;
let collaborators: Collaborator[] = [];
[dependencies, children, lifecycle, collaborators] = await Promise.all([
this.dependentFeaturesReadModel.getParents(featureName),
this.dependentFeaturesReadModel.getChildren([featureName]),
this.featureLifecycleReadModel.findCurrentStage(featureName),
this.featureCollaboratorsReadModel.getFeatureCollaborators(
featureName,
),
]);
let links: IFeatureLink[] = [];
[dependencies, children, lifecycle, collaborators, links] =
await Promise.all([
this.dependentFeaturesReadModel.getParents(featureName),
this.dependentFeaturesReadModel.getChildren([featureName]),
this.featureLifecycleReadModel.findCurrentStage(featureName),
this.featureCollaboratorsReadModel.getFeatureCollaborators(
featureName,
),
this.flagResolver.isEnabled('featureLinks')
? this.featureLinksReadModel.getLinks(featureName)
: Promise.resolve([]),
]);
if (environmentVariants) {
const result =
@ -1095,6 +1109,7 @@ class FeatureToggleService {
dependencies,
children,
lifecycle,
links,
collaborators: { users: collaborators },
};
} else {
@ -1110,6 +1125,7 @@ class FeatureToggleService {
dependencies,
children,
lifecycle,
links,
collaborators: { users: collaborators },
};
}

View File

@ -248,6 +248,35 @@ export const featureSchema = {
},
},
},
links: {
type: 'array',
items: {
type: 'object',
additionalProperties: false,
required: ['id', 'url'],
properties: {
id: {
type: 'string',
example: '01JTJNCJ5XVP2KPJFA03YRBZCA',
description: 'The id of the link',
},
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 list of links. This is an experimental field and may change.',
},
},
components: {
schemas: {

View File

@ -16,6 +16,7 @@ import EventService from '../features/events/event-service';
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
import type { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
import type { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
import type { IFeatureLinksReadModel } from '../features/feature-links/feature-links-read-model-type';
test('Should only store events for potentially stale on', async () => {
expect.assertions(2);
@ -72,6 +73,7 @@ test('Should only store events for potentially stale on', async () => {
{} as DependentFeaturesService,
{} as IFeatureLifecycleReadModel,
{} as IFeatureCollaboratorsReadModel,
{} as IFeatureLinksReadModel,
);
await featureToggleService.updatePotentiallyStaleFeatures();

View File

@ -160,6 +160,8 @@ 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';
export const createServices = (
stores: IUnleashStores,
@ -284,6 +286,10 @@ export const createServices = (
? new FeatureCollaboratorsReadModel(db)
: new FakeFeatureCollaboratorsReadModel();
const featureLinkReadModel = db
? new FeatureLinksReadModel(db)
: new FakeFeatureLinksReadModel();
const featureToggleService = new FeatureToggleService(
stores,
config,
@ -296,6 +302,7 @@ export const createServices = (
dependentFeaturesService,
featureLifecycleReadModel,
featureCollaboratorsReadModel,
featureLinkReadModel,
);
const transactionalEnvironmentService = db
? withTransactional(createEnvironmentService(config), db)

View File

@ -14,6 +14,7 @@ import type { IntegrationEventsService } from '../features/integration-events/in
import type { IFlagResolver } from './experimental';
import type { Collaborator } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
import type { EventEmitter } from 'events';
import type { IFeatureLink } from '../features/feature-links/feature-links-read-model-type';
export type Operator = (typeof ALL_OPERATORS)[number];
@ -123,6 +124,7 @@ export interface FeatureToggleView extends FeatureToggleWithEnvironment {
children: string[];
lifecycle: IFeatureLifecycleStage | undefined;
collaborators?: { users: Collaborator[] };
links: IFeatureLink[];
}
export interface IEnvironmentDetail extends IEnvironmentBase {