mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +02:00
feat: add links to feature read model (#9905)
This commit is contained in:
parent
8e05c92440
commit
b9f1d8414c
@ -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 [];
|
||||||
|
}
|
||||||
|
}
|
@ -97,6 +97,10 @@ test('should manage feature links', async () => {
|
|||||||
featureName: 'my_feature',
|
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, {
|
await updatedLink('my_feature', links[0].id, {
|
||||||
url: 'example_updated.com',
|
url: 'example_updated.com',
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
export interface IFeatureLink {
|
||||||
|
id: string;
|
||||||
|
url: string;
|
||||||
|
title: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFeatureLinksReadModel {
|
||||||
|
getLinks(feature: string): Promise<IFeatureLink[]>;
|
||||||
|
}
|
25
src/lib/features/feature-links/feature-links-read-model.ts
Normal file
25
src/lib/features/feature-links/feature-links-read-model.ts
Normal 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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
@ -59,6 +59,8 @@ import { FeatureLifecycleReadModel } from '../feature-lifecycle/feature-lifecycl
|
|||||||
import { FakeFeatureLifecycleReadModel } from '../feature-lifecycle/fake-feature-lifecycle-read-model';
|
import { FakeFeatureLifecycleReadModel } from '../feature-lifecycle/fake-feature-lifecycle-read-model';
|
||||||
import { FakeFeatureCollaboratorsReadModel } from './fake-feature-collaborators-read-model';
|
import { FakeFeatureCollaboratorsReadModel } from './fake-feature-collaborators-read-model';
|
||||||
import { FeatureCollaboratorsReadModel } from './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 = (
|
export const createFeatureToggleService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -128,6 +130,8 @@ export const createFeatureToggleService = (
|
|||||||
|
|
||||||
const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db);
|
const featureCollaboratorsReadModel = new FeatureCollaboratorsReadModel(db);
|
||||||
|
|
||||||
|
const featureLinksReadModel = new FeatureLinksReadModel(db);
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
{
|
{
|
||||||
featureStrategiesStore,
|
featureStrategiesStore,
|
||||||
@ -149,6 +153,7 @@ export const createFeatureToggleService = (
|
|||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
featureCollaboratorsReadModel,
|
featureCollaboratorsReadModel,
|
||||||
|
featureLinksReadModel,
|
||||||
);
|
);
|
||||||
return featureToggleService;
|
return featureToggleService;
|
||||||
};
|
};
|
||||||
@ -189,6 +194,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
|
const featureLifecycleReadModel = new FakeFeatureLifecycleReadModel();
|
||||||
const featureCollaboratorsReadModel =
|
const featureCollaboratorsReadModel =
|
||||||
new FakeFeatureCollaboratorsReadModel();
|
new FakeFeatureCollaboratorsReadModel();
|
||||||
|
const featureLinksReadModel = new FakeFeatureLinksReadModel();
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
{
|
{
|
||||||
@ -216,6 +222,7 @@ export const createFakeFeatureToggleService = (config: IUnleashConfig) => {
|
|||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
featureCollaboratorsReadModel,
|
featureCollaboratorsReadModel,
|
||||||
|
featureLinksReadModel,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
|
@ -113,6 +113,10 @@ import type { ResourceLimitsSchema } from '../../openapi';
|
|||||||
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||||
import type { Collaborator } from './types/feature-collaborators-read-model-type';
|
import type { Collaborator } from './types/feature-collaborators-read-model-type';
|
||||||
import { sortStrategies } from '../../util/sortStrategies';
|
import { sortStrategies } from '../../util/sortStrategies';
|
||||||
|
import type {
|
||||||
|
IFeatureLink,
|
||||||
|
IFeatureLinksReadModel,
|
||||||
|
} from '../feature-links/feature-links-read-model-type';
|
||||||
|
|
||||||
interface IFeatureContext {
|
interface IFeatureContext {
|
||||||
featureName: string;
|
featureName: string;
|
||||||
@ -182,6 +186,8 @@ class FeatureToggleService {
|
|||||||
|
|
||||||
private featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
private featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel;
|
||||||
|
|
||||||
|
private featureLinksReadModel: IFeatureLinksReadModel;
|
||||||
|
|
||||||
private dependentFeaturesService: DependentFeaturesService;
|
private dependentFeaturesService: DependentFeaturesService;
|
||||||
|
|
||||||
private eventBus: EventEmitter;
|
private eventBus: EventEmitter;
|
||||||
@ -227,6 +233,7 @@ class FeatureToggleService {
|
|||||||
dependentFeaturesService: DependentFeaturesService,
|
dependentFeaturesService: DependentFeaturesService,
|
||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel,
|
featureLifecycleReadModel: IFeatureLifecycleReadModel,
|
||||||
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel,
|
featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel,
|
||||||
|
featureLinksReadModel: IFeatureLinksReadModel,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/feature-toggle-service.ts');
|
this.logger = getLogger('services/feature-toggle-service.ts');
|
||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureStrategiesStore = featureStrategiesStore;
|
||||||
@ -247,6 +254,7 @@ class FeatureToggleService {
|
|||||||
this.dependentFeaturesService = dependentFeaturesService;
|
this.dependentFeaturesService = dependentFeaturesService;
|
||||||
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
this.featureLifecycleReadModel = featureLifecycleReadModel;
|
||||||
this.featureCollaboratorsReadModel = featureCollaboratorsReadModel;
|
this.featureCollaboratorsReadModel = featureCollaboratorsReadModel;
|
||||||
|
this.featureLinksReadModel = featureLinksReadModel;
|
||||||
this.eventBus = eventBus;
|
this.eventBus = eventBus;
|
||||||
this.resourceLimits = resourceLimits;
|
this.resourceLimits = resourceLimits;
|
||||||
}
|
}
|
||||||
@ -442,6 +450,7 @@ class FeatureToggleService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateStrategyType(
|
async validateStrategyType(
|
||||||
strategyName: string | undefined,
|
strategyName: string | undefined,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -1074,14 +1083,19 @@ class FeatureToggleService {
|
|||||||
let children: string[] = [];
|
let children: string[] = [];
|
||||||
let lifecycle: IFeatureLifecycleStage | undefined = undefined;
|
let lifecycle: IFeatureLifecycleStage | undefined = undefined;
|
||||||
let collaborators: Collaborator[] = [];
|
let collaborators: Collaborator[] = [];
|
||||||
[dependencies, children, lifecycle, collaborators] = await Promise.all([
|
let links: IFeatureLink[] = [];
|
||||||
this.dependentFeaturesReadModel.getParents(featureName),
|
[dependencies, children, lifecycle, collaborators, links] =
|
||||||
this.dependentFeaturesReadModel.getChildren([featureName]),
|
await Promise.all([
|
||||||
this.featureLifecycleReadModel.findCurrentStage(featureName),
|
this.dependentFeaturesReadModel.getParents(featureName),
|
||||||
this.featureCollaboratorsReadModel.getFeatureCollaborators(
|
this.dependentFeaturesReadModel.getChildren([featureName]),
|
||||||
featureName,
|
this.featureLifecycleReadModel.findCurrentStage(featureName),
|
||||||
),
|
this.featureCollaboratorsReadModel.getFeatureCollaborators(
|
||||||
]);
|
featureName,
|
||||||
|
),
|
||||||
|
this.flagResolver.isEnabled('featureLinks')
|
||||||
|
? this.featureLinksReadModel.getLinks(featureName)
|
||||||
|
: Promise.resolve([]),
|
||||||
|
]);
|
||||||
|
|
||||||
if (environmentVariants) {
|
if (environmentVariants) {
|
||||||
const result =
|
const result =
|
||||||
@ -1095,6 +1109,7 @@ class FeatureToggleService {
|
|||||||
dependencies,
|
dependencies,
|
||||||
children,
|
children,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
|
links,
|
||||||
collaborators: { users: collaborators },
|
collaborators: { users: collaborators },
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@ -1110,6 +1125,7 @@ class FeatureToggleService {
|
|||||||
dependencies,
|
dependencies,
|
||||||
children,
|
children,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
|
links,
|
||||||
collaborators: { users: collaborators },
|
collaborators: { users: collaborators },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -16,6 +16,7 @@ import EventService from '../features/events/event-service';
|
|||||||
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
import FakeFeatureTagStore from '../../test/fixtures/fake-feature-tag-store';
|
||||||
import type { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
import type { DependentFeaturesService } from '../features/dependent-features/dependent-features-service';
|
||||||
import type { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
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 () => {
|
test('Should only store events for potentially stale on', async () => {
|
||||||
expect.assertions(2);
|
expect.assertions(2);
|
||||||
@ -72,6 +73,7 @@ test('Should only store events for potentially stale on', async () => {
|
|||||||
{} as DependentFeaturesService,
|
{} as DependentFeaturesService,
|
||||||
{} as IFeatureLifecycleReadModel,
|
{} as IFeatureLifecycleReadModel,
|
||||||
{} as IFeatureCollaboratorsReadModel,
|
{} as IFeatureCollaboratorsReadModel,
|
||||||
|
{} as IFeatureLinksReadModel,
|
||||||
);
|
);
|
||||||
|
|
||||||
await featureToggleService.updatePotentiallyStaleFeatures();
|
await featureToggleService.updatePotentiallyStaleFeatures();
|
||||||
|
@ -160,6 +160,8 @@ import {
|
|||||||
} from '../features/context/createContextService';
|
} from '../features/context/createContextService';
|
||||||
import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
|
import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service';
|
||||||
import { createFakeFeatureLinkService } from '../features/feature-links/createFeatureLinkService';
|
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 = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -284,6 +286,10 @@ export const createServices = (
|
|||||||
? new FeatureCollaboratorsReadModel(db)
|
? new FeatureCollaboratorsReadModel(db)
|
||||||
: new FakeFeatureCollaboratorsReadModel();
|
: new FakeFeatureCollaboratorsReadModel();
|
||||||
|
|
||||||
|
const featureLinkReadModel = db
|
||||||
|
? new FeatureLinksReadModel(db)
|
||||||
|
: new FakeFeatureLinksReadModel();
|
||||||
|
|
||||||
const featureToggleService = new FeatureToggleService(
|
const featureToggleService = new FeatureToggleService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
@ -296,6 +302,7 @@ export const createServices = (
|
|||||||
dependentFeaturesService,
|
dependentFeaturesService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
featureCollaboratorsReadModel,
|
featureCollaboratorsReadModel,
|
||||||
|
featureLinkReadModel,
|
||||||
);
|
);
|
||||||
const transactionalEnvironmentService = db
|
const transactionalEnvironmentService = db
|
||||||
? withTransactional(createEnvironmentService(config), db)
|
? withTransactional(createEnvironmentService(config), db)
|
||||||
|
@ -14,6 +14,7 @@ import type { IntegrationEventsService } from '../features/integration-events/in
|
|||||||
import type { IFlagResolver } from './experimental';
|
import type { IFlagResolver } from './experimental';
|
||||||
import type { Collaborator } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
|
import type { Collaborator } from '../features/feature-toggle/types/feature-collaborators-read-model-type';
|
||||||
import type { EventEmitter } from 'events';
|
import type { EventEmitter } from 'events';
|
||||||
|
import type { IFeatureLink } from '../features/feature-links/feature-links-read-model-type';
|
||||||
|
|
||||||
export type Operator = (typeof ALL_OPERATORS)[number];
|
export type Operator = (typeof ALL_OPERATORS)[number];
|
||||||
|
|
||||||
@ -123,6 +124,7 @@ export interface FeatureToggleView extends FeatureToggleWithEnvironment {
|
|||||||
children: string[];
|
children: string[];
|
||||||
lifecycle: IFeatureLifecycleStage | undefined;
|
lifecycle: IFeatureLifecycleStage | undefined;
|
||||||
collaborators?: { users: Collaborator[] };
|
collaborators?: { users: Collaborator[] };
|
||||||
|
links: IFeatureLink[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnvironmentDetail extends IEnvironmentBase {
|
export interface IEnvironmentDetail extends IEnvironmentBase {
|
||||||
|
Loading…
Reference in New Issue
Block a user