mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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