mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-02 01:17:58 +02:00
feat: feature link backend stub (#9893)
This commit is contained in:
parent
1166d00e6d
commit
002233e7f6
@ -65,6 +65,7 @@ import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubs
|
|||||||
import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model';
|
import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model';
|
||||||
import { UniqueConnectionStore } from '../features/unique-connection/unique-connection-store';
|
import { UniqueConnectionStore } from '../features/unique-connection/unique-connection-store';
|
||||||
import { UniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model';
|
import { UniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model';
|
||||||
|
import FakeFeatureLinkStore from '../features/feature-links/fake-feature-link-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -201,6 +202,7 @@ export const createStores = (
|
|||||||
releasePlanMilestoneStore: new ReleasePlanMilestoneStore(db, config),
|
releasePlanMilestoneStore: new ReleasePlanMilestoneStore(db, config),
|
||||||
releasePlanMilestoneStrategyStore:
|
releasePlanMilestoneStrategyStore:
|
||||||
new ReleasePlanMilestoneStrategyStore(db, config),
|
new ReleasePlanMilestoneStrategyStore(db, config),
|
||||||
|
featureLinkStore: new FakeFeatureLinkStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
17
src/lib/features/feature-links/createFeatureLinkService.ts
Normal file
17
src/lib/features/feature-links/createFeatureLinkService.ts
Normal file
@ -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 };
|
||||||
|
};
|
53
src/lib/features/feature-links/fake-feature-link-store.ts
Normal file
53
src/lib/features/feature-links/fake-feature-link-store.ts
Normal file
@ -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<IFeatureLink, 'id'>): Promise<IFeatureLink> {
|
||||||
|
const newLink: IFeatureLink = {
|
||||||
|
...link,
|
||||||
|
id: String(Math.random()),
|
||||||
|
};
|
||||||
|
this.links.push(newLink);
|
||||||
|
return newLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(id: string): Promise<void> {
|
||||||
|
const index = this.links.findIndex((link) => link.id === id);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.links.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
this.links = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
|
||||||
|
async exists(id: string): Promise<boolean> {
|
||||||
|
return this.links.some((link) => link.id === id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id: string): Promise<IFeatureLink> {
|
||||||
|
const link = this.links.find((link) => link.id === id);
|
||||||
|
if (link) {
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
throw new NotFoundError('Could not find feature link');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IFeatureLink[]> {
|
||||||
|
return this.links;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(link: IFeatureLink): Promise<IFeatureLink> {
|
||||||
|
await this.delete(link.id);
|
||||||
|
this.links.push(link);
|
||||||
|
return link;
|
||||||
|
}
|
||||||
|
}
|
158
src/lib/features/feature-links/feature-link-controller.ts
Normal file
158
src/lib/features/feature-links/feature-link-controller.ts
Normal file
@ -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<FeatureLinkService>;
|
||||||
|
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<FeatureLinkService>;
|
||||||
|
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<IFeatureLink, 'id' | 'createdAt'>
|
||||||
|
>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
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<IFeatureLink, 'id'>
|
||||||
|
>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
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<IFeatureLink, 'id'>
|
||||||
|
>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { projectId, linkId } = req.params;
|
||||||
|
|
||||||
|
await this.transactionalFeatureLinkService.transactional((service) =>
|
||||||
|
service.deleteLink({ projectId, linkId }, req.audit),
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(204).end();
|
||||||
|
}
|
||||||
|
}
|
60
src/lib/features/feature-links/feature-link-service.test.ts
Normal file
60
src/lib/features/feature-links/feature-link-service.test.ts
Normal file
@ -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);
|
||||||
|
});
|
108
src/lib/features/feature-links/feature-link-service.ts
Normal file
108
src/lib/features/feature-links/feature-link-service.ts
Normal file
@ -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<IUnleashConfig, 'getLogger'>,
|
||||||
|
eventService: EventService,
|
||||||
|
) {
|
||||||
|
this.logger = getLogger('feature-links/feature-link-service.ts');
|
||||||
|
this.featureLinkStore = stores.featureLinkStore;
|
||||||
|
this.eventService = eventService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IFeatureLink[]> {
|
||||||
|
return this.featureLinkStore.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async createLink(
|
||||||
|
projectId: string,
|
||||||
|
newLink: Omit<IFeatureLink, 'id'>,
|
||||||
|
auditUser: IAuditUser,
|
||||||
|
): Promise<IFeatureLink> {
|
||||||
|
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<IFeatureLink, 'id'>,
|
||||||
|
auditUser: IAuditUser,
|
||||||
|
): Promise<IFeatureLink> {
|
||||||
|
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<void> {
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
13
src/lib/features/feature-links/feature-link-store-type.ts
Normal file
13
src/lib/features/feature-links/feature-link-store-type.ts
Normal file
@ -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<IFeatureLink, string> {
|
||||||
|
create(link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink>;
|
||||||
|
update(link: IFeatureLink): Promise<IFeatureLink>;
|
||||||
|
}
|
63
src/lib/features/feature-links/feature-link.e2e.test.ts
Normal file
63
src/lib/features/feature-links/feature-link.e2e.test.ts
Normal file
@ -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' });
|
||||||
|
});
|
@ -14,3 +14,4 @@ export * from './playground/createPlaygroundService';
|
|||||||
export * from './personal-dashboard/createPersonalDashboardService';
|
export * from './personal-dashboard/createPersonalDashboardService';
|
||||||
export * from './user-subscriptions/createUserSubscriptionsService';
|
export * from './user-subscriptions/createUserSubscriptionsService';
|
||||||
export * from './context/createContextService';
|
export * from './context/createContextService';
|
||||||
|
export * from './feature-links/createFeatureLinkService';
|
||||||
|
@ -49,6 +49,7 @@ import {
|
|||||||
type ProjectFlagCreatorsSchema,
|
type ProjectFlagCreatorsSchema,
|
||||||
} from '../../openapi/spec/project-flag-creators-schema';
|
} from '../../openapi/spec/project-flag-creators-schema';
|
||||||
import ProjectStatusController from '../project-status/project-status-controller';
|
import ProjectStatusController from '../project-status/project-status-controller';
|
||||||
|
import FeatureLinkController from '../feature-links/feature-link-controller';
|
||||||
|
|
||||||
export default class ProjectController extends Controller {
|
export default class ProjectController extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -248,6 +249,7 @@ export default class ProjectController extends Controller {
|
|||||||
this.use('/', new ProjectInsightsController(config, services).router);
|
this.use('/', new ProjectInsightsController(config, services).router);
|
||||||
this.use('/', new ProjectStatusController(config, services).router);
|
this.use('/', new ProjectStatusController(config, services).router);
|
||||||
this.use('/', new FeatureLifecycleController(config, services).router);
|
this.use('/', new FeatureLifecycleController(config, services).router);
|
||||||
|
this.use('/', new FeatureLinkController(config, services).router);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(
|
async getProjects(
|
||||||
|
26
src/lib/openapi/spec/feature-link-schema.ts
Normal file
26
src/lib/openapi/spec/feature-link-schema.ts
Normal file
@ -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<typeof featureLinkSchema>;
|
@ -85,6 +85,7 @@ export * from './feature-events-schema';
|
|||||||
export * from './feature-lifecycle-completed-schema';
|
export * from './feature-lifecycle-completed-schema';
|
||||||
export * from './feature-lifecycle-count-schema';
|
export * from './feature-lifecycle-count-schema';
|
||||||
export * from './feature-lifecycle-schema';
|
export * from './feature-lifecycle-schema';
|
||||||
|
export * from './feature-link-schema';
|
||||||
export * from './feature-metrics-schema';
|
export * from './feature-metrics-schema';
|
||||||
export * from './feature-schema';
|
export * from './feature-schema';
|
||||||
export * from './feature-search-environment-schema';
|
export * from './feature-search-environment-schema';
|
||||||
|
@ -158,6 +158,7 @@ import {
|
|||||||
createFakeContextService,
|
createFakeContextService,
|
||||||
} 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';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -424,6 +425,10 @@ export const createServices = (
|
|||||||
? withTransactional(createUserSubscriptionsService(config), db)
|
? withTransactional(createUserSubscriptionsService(config), db)
|
||||||
: withFakeTransactional(createFakeUserSubscriptionsService(config));
|
: withFakeTransactional(createFakeUserSubscriptionsService(config));
|
||||||
|
|
||||||
|
const transactionalFeatureLinkService = withFakeTransactional(
|
||||||
|
createFakeFeatureLinkService(config).featureLinkService,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
transactionalAccessService,
|
transactionalAccessService,
|
||||||
accessService,
|
accessService,
|
||||||
@ -493,6 +498,7 @@ export const createServices = (
|
|||||||
transactionalUserSubscriptionsService,
|
transactionalUserSubscriptionsService,
|
||||||
uniqueConnectionService,
|
uniqueConnectionService,
|
||||||
featureLifecycleReadModel,
|
featureLifecycleReadModel,
|
||||||
|
transactionalFeatureLinkService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ export const APPLICATION_CREATED = 'application-created' as const;
|
|||||||
export const FEATURE_CREATED = 'feature-created' as const;
|
export const FEATURE_CREATED = 'feature-created' as const;
|
||||||
export const FEATURE_DELETED = 'feature-deleted' as const;
|
export const FEATURE_DELETED = 'feature-deleted' as const;
|
||||||
export const FEATURE_UPDATED = 'feature-updated' 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_ADDED = 'feature-dependency-added' as const;
|
||||||
export const FEATURE_DEPENDENCY_REMOVED = 'feature-dependency-removed' as const;
|
export const FEATURE_DEPENDENCY_REMOVED = 'feature-dependency-removed' as const;
|
||||||
export const FEATURE_DEPENDENCIES_REMOVED =
|
export const FEATURE_DEPENDENCIES_REMOVED =
|
||||||
@ -242,6 +245,9 @@ export const IEventTypes = [
|
|||||||
FEATURE_TYPE_UPDATED,
|
FEATURE_TYPE_UPDATED,
|
||||||
FEATURE_COMPLETED,
|
FEATURE_COMPLETED,
|
||||||
FEATURE_UNCOMPLETED,
|
FEATURE_UNCOMPLETED,
|
||||||
|
FEATURE_LINK_ADDED,
|
||||||
|
FEATURE_LINK_REMOVED,
|
||||||
|
FEATURE_LINK_UPDATED,
|
||||||
STRATEGY_ORDER_CHANGED,
|
STRATEGY_ORDER_CHANGED,
|
||||||
DROP_FEATURE_TAGS,
|
DROP_FEATURE_TAGS,
|
||||||
FEATURE_UNTAGGED,
|
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 {
|
export class FeatureStrategyAddEvent extends BaseEvent {
|
||||||
readonly project: string;
|
readonly project: string;
|
||||||
|
|
||||||
|
@ -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 { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service';
|
||||||
import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-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 { IFeatureLifecycleReadModel } from '../features/feature-lifecycle/feature-lifecycle-read-model-type';
|
||||||
|
import type FeatureLinkService from '../features/feature-links/feature-link-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
transactionalAccessService: WithTransactional<AccessService>;
|
transactionalAccessService: WithTransactional<AccessService>;
|
||||||
@ -133,4 +134,5 @@ export interface IUnleashServices {
|
|||||||
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
|
transactionalUserSubscriptionsService: WithTransactional<UserSubscriptionsService>;
|
||||||
uniqueConnectionService: UniqueConnectionService;
|
uniqueConnectionService: UniqueConnectionService;
|
||||||
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
featureLifecycleReadModel: IFeatureLifecycleReadModel;
|
||||||
|
transactionalFeatureLinkService: WithTransactional<FeatureLinkService>;
|
||||||
}
|
}
|
||||||
|
@ -59,6 +59,7 @@ import { ReleasePlanStore } from '../features/release-plans/release-plan-store';
|
|||||||
import { ReleasePlanTemplateStore } from '../features/release-plans/release-plan-template-store';
|
import { ReleasePlanTemplateStore } from '../features/release-plans/release-plan-template-store';
|
||||||
import { ReleasePlanMilestoneStore } from '../features/release-plans/release-plan-milestone-store';
|
import { ReleasePlanMilestoneStore } from '../features/release-plans/release-plan-milestone-store';
|
||||||
import { ReleasePlanMilestoneStrategyStore } from '../features/release-plans/release-plan-milestone-strategy-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 {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -122,6 +123,7 @@ export interface IUnleashStores {
|
|||||||
releasePlanTemplateStore: ReleasePlanTemplateStore;
|
releasePlanTemplateStore: ReleasePlanTemplateStore;
|
||||||
releasePlanMilestoneStore: ReleasePlanMilestoneStore;
|
releasePlanMilestoneStore: ReleasePlanMilestoneStore;
|
||||||
releasePlanMilestoneStrategyStore: ReleasePlanMilestoneStrategyStore;
|
releasePlanMilestoneStrategyStore: ReleasePlanMilestoneStrategyStore;
|
||||||
|
featureLinkStore: IFeatureLinkStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -183,4 +185,5 @@ export {
|
|||||||
ReleasePlanTemplateStore,
|
ReleasePlanTemplateStore,
|
||||||
ReleasePlanMilestoneStore,
|
ReleasePlanMilestoneStore,
|
||||||
ReleasePlanMilestoneStrategyStore,
|
ReleasePlanMilestoneStrategyStore,
|
||||||
|
type IFeatureLinkStore,
|
||||||
};
|
};
|
||||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -62,6 +62,7 @@ import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/
|
|||||||
import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model';
|
import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model';
|
||||||
import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/fake-unique-connection-store';
|
import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/fake-unique-connection-store';
|
||||||
import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/unique-connection-read-model';
|
import { UniqueConnectionReadModel } from '../../lib/features/unique-connection/unique-connection-read-model';
|
||||||
|
import FakeFeatureLinkStore from '../../lib/features/feature-links/fake-feature-link-store';
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -138,6 +139,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
releasePlanTemplateStore: {} as ReleasePlanTemplateStore,
|
releasePlanTemplateStore: {} as ReleasePlanTemplateStore,
|
||||||
releasePlanMilestoneStrategyStore:
|
releasePlanMilestoneStrategyStore:
|
||||||
{} as ReleasePlanMilestoneStrategyStore,
|
{} as ReleasePlanMilestoneStrategyStore,
|
||||||
|
featureLinkStore: new FakeFeatureLinkStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user