1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-21 13:47:39 +02:00

feat: sql feature link persistence (#9901)

This commit is contained in:
Mateusz Kwasniewski 2025-05-06 11:46:15 +02:00 committed by GitHub
parent bb82b6920b
commit c6ab2a1cf7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 155 additions and 19 deletions

View File

@ -65,7 +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'; import { FeatureLinkStore } from '../features/feature-links/feature-link-store';
export const createStores = ( export const createStores = (
config: IUnleashConfig, config: IUnleashConfig,
@ -202,7 +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(), featureLinkStore: new FeatureLinkStore(db, config),
}; };
}; };

View File

@ -1,7 +1,24 @@
import type { IUnleashConfig } from '../../types'; import type { IUnleashConfig } from '../../types';
import FeatureLinkService from './feature-link-service'; import FeatureLinkService from './feature-link-service';
import FakeFeatureLinkStore from './fake-feature-link-store'; import FakeFeatureLinkStore from './fake-feature-link-store';
import { createFakeEventsService } from '../events/createEventsService'; import {
createEventsService,
createFakeEventsService,
} from '../events/createEventsService';
import type { Db } from '../../db/db';
import { FeatureLinkStore } from './feature-link-store';
export const createFeatureLinkService =
(config: IUnleashConfig) => (db: Db) => {
const eventService = createEventsService(db, config);
const featureLinkStore = new FeatureLinkStore(db, config);
return new FeatureLinkService(
{ featureLinkStore },
config,
eventService,
);
};
export const createFakeFeatureLinkService = (config: IUnleashConfig) => { export const createFakeFeatureLinkService = (config: IUnleashConfig) => {
const eventService = createFakeEventsService(config); const eventService = createFakeEventsService(config);

View File

@ -7,7 +7,7 @@ import type {
export default class FakeFeatureLinkStore implements IFeatureLinkStore { export default class FakeFeatureLinkStore implements IFeatureLinkStore {
private links: IFeatureLink[] = []; private links: IFeatureLink[] = [];
async create(link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink> { async insert(link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink> {
const newLink: IFeatureLink = { const newLink: IFeatureLink = {
...link, ...link,
id: String(Math.random()), id: String(Math.random()),
@ -45,9 +45,13 @@ export default class FakeFeatureLinkStore implements IFeatureLinkStore {
return this.links; return this.links;
} }
async update(link: IFeatureLink): Promise<IFeatureLink> { async update(
await this.delete(link.id); id: string,
this.links.push(link); link: Omit<IFeatureLink, 'id'>,
return link; ): Promise<IFeatureLink> {
await this.delete(id);
const fullLink = { ...link, id };
this.links.push(fullLink);
return fullLink;
} }
} }

View File

@ -41,7 +41,7 @@ export default class FeatureLinkService {
newLink: Omit<IFeatureLink, 'id'>, newLink: Omit<IFeatureLink, 'id'>,
auditUser: IAuditUser, auditUser: IAuditUser,
): Promise<IFeatureLink> { ): Promise<IFeatureLink> {
const link = await this.featureLinkStore.create(newLink); const link = await this.featureLinkStore.insert(newLink);
await this.eventService.storeEvent( await this.eventService.storeEvent(
new FeatureLinkAddedEvent({ new FeatureLinkAddedEvent({
@ -66,10 +66,7 @@ export default class FeatureLinkService {
throw new NotFoundError(`Could not find link with id ${linkId}`); throw new NotFoundError(`Could not find link with id ${linkId}`);
} }
const link = await this.featureLinkStore.update({ const link = await this.featureLinkStore.update(linkId, updatedLink);
...updatedLink,
id: linkId,
});
await this.eventService.storeEvent( await this.eventService.storeEvent(
new FeatureLinkUpdatedEvent({ new FeatureLinkUpdatedEvent({

View File

@ -8,6 +8,6 @@ export interface IFeatureLink {
} }
export interface IFeatureLinkStore extends Store<IFeatureLink, string> { export interface IFeatureLinkStore extends Store<IFeatureLink, string> {
create(link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink>; insert(link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink>;
update(link: IFeatureLink): Promise<IFeatureLink>; update(id: string, link: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink>;
} }

View File

@ -0,0 +1,33 @@
import type { Db } from '../../db/db';
import type { IFeatureLinkStore } from '../../types';
import type { IFeatureLink } from './feature-link-store-type';
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
import type { Row } from '../../db/crud/row-type';
import { ulid } from 'ulidx';
export class FeatureLinkStore
extends CRUDStore<
IFeatureLink,
Omit<IFeatureLink, 'id'>,
Row<IFeatureLink>,
Row<Omit<IFeatureLink, 'id'>>,
string
>
implements IFeatureLinkStore
{
constructor(db: Db, config: CrudStoreConfig) {
super('feature_link', db, config);
}
async insert(item: Omit<IFeatureLink, 'id'>): Promise<IFeatureLink> {
const id = ulid();
const featureLink = {
id: ulid(),
feature_name: item.featureName,
url: item.url,
title: item.title,
};
await this.db('feature_link').insert(featureLink);
return { ...item, id };
}
}

View File

@ -58,8 +58,90 @@ const addLink = async (
.expect(expectedCode); .expect(expectedCode);
}; };
test('should add feature links', async () => { const updatedLink = async (
featureName: string,
linkId: string,
link: FeatureLinkSchema,
expectedCode = 204,
) => {
return app.request
.put(
`/api/admin/projects/default/features/${featureName}/link/${linkId}`,
)
.send(link)
.expect(expectedCode);
};
const deleteLink = async (
featureName: string,
linkId: string,
expectedCode = 204,
) => {
return app.request
.delete(
`/api/admin/projects/default/features/${featureName}/link/${linkId}`,
)
.expect(expectedCode);
};
test('should manage feature links', async () => {
await app.createFeature('my_feature'); await app.createFeature('my_feature');
await addLink('my_feature', { url: 'example.com', title: 'feature link' }); await addLink('my_feature', { url: 'example.com', title: 'feature link' });
const links = await featureLinkStore.getAll();
expect(links).toMatchObject([
{
url: 'example.com',
title: 'feature link',
featureName: 'my_feature',
},
]);
await updatedLink('my_feature', links[0].id, {
url: 'example_updated.com',
title: 'feature link updated',
});
const updatedLinks = await featureLinkStore.getAll();
expect(updatedLinks).toMatchObject([
{
url: 'example_updated.com',
title: 'feature link updated',
featureName: 'my_feature',
},
]);
await deleteLink('my_feature', links[0].id);
const deletedLinks = await featureLinkStore.getAll();
expect(deletedLinks).toMatchObject([]);
const [event1, event2, event3] = await eventStore.getEvents();
expect([event1, event2, event3]).toMatchObject([
{
type: 'feature-link-removed',
data: null,
preData: {
url: 'example_updated.com',
title: 'feature link updated',
},
featureName: 'my_feature',
project: 'default',
},
{
type: 'feature-link-updated',
data: { url: 'example_updated.com', title: 'feature link updated' },
preData: { url: 'example.com', title: 'feature link' },
featureName: 'my_feature',
project: 'default',
},
{
type: 'feature-link-added',
data: { url: 'example.com', title: 'feature link' },
preData: null,
featureName: 'my_feature',
project: 'default',
},
]);
}); });

View File

@ -72,6 +72,7 @@ import {
createFakeProjectService, createFakeProjectService,
createFakeUserSubscriptionsService, createFakeUserSubscriptionsService,
createFeatureLifecycleService, createFeatureLifecycleService,
createFeatureLinkService,
createFeatureToggleService, createFeatureToggleService,
createProjectService, createProjectService,
createUserSubscriptionsService, createUserSubscriptionsService,
@ -425,9 +426,11 @@ export const createServices = (
? withTransactional(createUserSubscriptionsService(config), db) ? withTransactional(createUserSubscriptionsService(config), db)
: withFakeTransactional(createFakeUserSubscriptionsService(config)); : withFakeTransactional(createFakeUserSubscriptionsService(config));
const transactionalFeatureLinkService = withFakeTransactional( const transactionalFeatureLinkService = db
createFakeFeatureLinkService(config).featureLinkService, ? withTransactional(createFeatureLinkService(config), db)
); : withFakeTransactional(
createFakeFeatureLinkService(config).featureLinkService,
);
return { return {
transactionalAccessService, transactionalAccessService,