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:
parent
bb82b6920b
commit
c6ab2a1cf7
@ -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),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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({
|
||||||
|
@ -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>;
|
||||||
}
|
}
|
||||||
|
33
src/lib/features/feature-links/feature-link-store.ts
Normal file
33
src/lib/features/feature-links/feature-link-store.ts
Normal 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 };
|
||||||
|
}
|
||||||
|
}
|
@ -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',
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user