mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: User subscriptions store and service (#8648)
Co-authored-by: kwasniew <kwasniewski.mateusz@gmail.com>
This commit is contained in:
		
							parent
							
								
									2e64df671a
								
							
						
					
					
						commit
						2bd2d74f83
					
				@ -54,6 +54,8 @@ import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/featur
 | 
				
			|||||||
import { createProjectReadModel } from '../features/project/createProjectReadModel';
 | 
					import { createProjectReadModel } from '../features/project/createProjectReadModel';
 | 
				
			||||||
import { OnboardingStore } from '../features/onboarding/onboarding-store';
 | 
					import { OnboardingStore } from '../features/onboarding/onboarding-store';
 | 
				
			||||||
import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';
 | 
					import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';
 | 
				
			||||||
 | 
					import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store';
 | 
				
			||||||
 | 
					import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const createStores = (
 | 
					export const createStores = (
 | 
				
			||||||
    config: IUnleashConfig,
 | 
					    config: IUnleashConfig,
 | 
				
			||||||
@ -187,6 +189,11 @@ export const createStores = (
 | 
				
			|||||||
            eventBus,
 | 
					            eventBus,
 | 
				
			||||||
            config.flagResolver,
 | 
					            config.flagResolver,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        userUnsubscribeStore: new UserUnsubscribeStore(db, getLogger),
 | 
				
			||||||
 | 
					        userSubscriptionsReadModel: new UserSubscriptionsReadModel(
 | 
				
			||||||
 | 
					            db,
 | 
				
			||||||
 | 
					            eventBus,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,13 @@
 | 
				
			|||||||
 | 
					import type { IUserSubscriptionsReadModel } from './user-subscriptions-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FakeUserSubscriptionsReadModel
 | 
				
			||||||
 | 
					    implements IUserSubscriptionsReadModel
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    async getSubscribedUsers(subscription: string) {
 | 
				
			||||||
 | 
					        return [];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getUserSubscriptions() {
 | 
				
			||||||
 | 
					        return ['productivity-report'];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					import type { IUserUnsubscribeStore } from './user-unsubscribe-store-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class FakeUserUnsubscribeStore implements IUserUnsubscribeStore {
 | 
				
			||||||
 | 
					    async insert() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async delete(): Promise<void> {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    destroy(): void {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					export type Subscriber = {
 | 
				
			||||||
 | 
					    name: string;
 | 
				
			||||||
 | 
					    email: string;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IUserSubscriptionsReadModel {
 | 
				
			||||||
 | 
					    getSubscribedUsers(subscription: string): Promise<Subscriber[]>;
 | 
				
			||||||
 | 
					    getUserSubscriptions(userId: number): Promise<string[]>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const SUBSCRIPTION_TYPES = ['productivity-report'] as const;
 | 
				
			||||||
@ -0,0 +1,56 @@
 | 
				
			|||||||
 | 
					import type { Db } from '../../db/db';
 | 
				
			||||||
 | 
					import type EventEmitter from 'events';
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					    SUBSCRIPTION_TYPES,
 | 
				
			||||||
 | 
					    type IUserSubscriptionsReadModel,
 | 
				
			||||||
 | 
					    type Subscriber,
 | 
				
			||||||
 | 
					} from './user-subscriptions-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const USERS_TABLE = 'users';
 | 
				
			||||||
 | 
					const USER_COLUMNS = [
 | 
				
			||||||
 | 
					    'id',
 | 
				
			||||||
 | 
					    'name',
 | 
				
			||||||
 | 
					    'username',
 | 
				
			||||||
 | 
					    'email',
 | 
				
			||||||
 | 
					    'image_url',
 | 
				
			||||||
 | 
					    'is_service',
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					const UNSUBSCRIPTION_TABLE = 'user_unsubscription';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mapRowToSubscriber = (row) =>
 | 
				
			||||||
 | 
					    ({
 | 
				
			||||||
 | 
					        name: row.name || row.username || '',
 | 
				
			||||||
 | 
					        email: row.email,
 | 
				
			||||||
 | 
					    }) as Subscriber;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UserSubscriptionsReadModel implements IUserSubscriptionsReadModel {
 | 
				
			||||||
 | 
					    private db: Db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(db: Db, eventBus: EventEmitter) {
 | 
				
			||||||
 | 
					        this.db = db;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getSubscribedUsers(subscription: string) {
 | 
				
			||||||
 | 
					        const unsubscribedUserIdsQuery = this.db(UNSUBSCRIPTION_TABLE)
 | 
				
			||||||
 | 
					            .select('user_id')
 | 
				
			||||||
 | 
					            .where('subscription', subscription);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const users = await this.db(USERS_TABLE)
 | 
				
			||||||
 | 
					            .select(USER_COLUMNS)
 | 
				
			||||||
 | 
					            .whereNotIn('id', unsubscribedUserIdsQuery)
 | 
				
			||||||
 | 
					            .andWhere('is_service', false)
 | 
				
			||||||
 | 
					            .andWhereNot('email', null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return users.map(mapRowToSubscriber);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async getUserSubscriptions(userId: number) {
 | 
				
			||||||
 | 
					        const unsubscriptions = await this.db(UNSUBSCRIPTION_TABLE)
 | 
				
			||||||
 | 
					            .select('subscription')
 | 
				
			||||||
 | 
					            .where('user_id', userId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return SUBSCRIPTION_TYPES.filter(
 | 
				
			||||||
 | 
					            (subscription) => !unsubscriptions.includes(subscription),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					import type { IUnleashConfig, IUnleashStores } from '../../types';
 | 
				
			||||||
 | 
					import type { Logger } from '../../logger';
 | 
				
			||||||
 | 
					import type { IAuditUser } from '../../types/user';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    IUserUnsubscribeStore,
 | 
				
			||||||
 | 
					    UnsubscribeEntry,
 | 
				
			||||||
 | 
					} from './user-unsubscribe-store-type';
 | 
				
			||||||
 | 
					import type EventService from '../events/event-service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class UserSubscriptionService {
 | 
				
			||||||
 | 
					    private userUnsubscribeStore: IUserUnsubscribeStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private eventService: EventService;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private logger: Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(
 | 
				
			||||||
 | 
					        { userUnsubscribeStore }: Pick<IUnleashStores, 'userUnsubscribeStore'>,
 | 
				
			||||||
 | 
					        { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
 | 
				
			||||||
 | 
					        eventService: EventService,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					        this.userUnsubscribeStore = userUnsubscribeStore;
 | 
				
			||||||
 | 
					        this.eventService = eventService;
 | 
				
			||||||
 | 
					        this.logger = getLogger('services/user-subscription-service.ts');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async subscribe(
 | 
				
			||||||
 | 
					        userId: number,
 | 
				
			||||||
 | 
					        subscription: string,
 | 
				
			||||||
 | 
					        auditUser: IAuditUser,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        const entry: UnsubscribeEntry = {
 | 
				
			||||||
 | 
					            userId,
 | 
				
			||||||
 | 
					            subscription,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.userUnsubscribeStore.delete(entry);
 | 
				
			||||||
 | 
					        // TODO: log an event
 | 
				
			||||||
 | 
					        // await this.eventService.storeEvent(
 | 
				
			||||||
 | 
					        //     new UserSubscriptionEvent({
 | 
				
			||||||
 | 
					        //         data: { ...entry, action: 'subscribed' },
 | 
				
			||||||
 | 
					        //         auditUser,
 | 
				
			||||||
 | 
					        //     }),
 | 
				
			||||||
 | 
					        // );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async unsubscribe(
 | 
				
			||||||
 | 
					        userId: number,
 | 
				
			||||||
 | 
					        subscription: string,
 | 
				
			||||||
 | 
					        auditUser: IAuditUser,
 | 
				
			||||||
 | 
					    ): Promise<void> {
 | 
				
			||||||
 | 
					        const entry: UnsubscribeEntry = {
 | 
				
			||||||
 | 
					            userId,
 | 
				
			||||||
 | 
					            subscription,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await this.userUnsubscribeStore.insert(entry);
 | 
				
			||||||
 | 
					        // TODO: log an event
 | 
				
			||||||
 | 
					        // await this.eventService.storeEvent(
 | 
				
			||||||
 | 
					        //     new UserSubscriptionEvent({
 | 
				
			||||||
 | 
					        //         data: { ...entry, action: 'unsubscribed' },
 | 
				
			||||||
 | 
					        //         auditUser,
 | 
				
			||||||
 | 
					        //     }),
 | 
				
			||||||
 | 
					        // );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					export type UnsubscribeEntry = {
 | 
				
			||||||
 | 
					    userId: number;
 | 
				
			||||||
 | 
					    subscription: string;
 | 
				
			||||||
 | 
					    createdAt?: Date;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface IUserUnsubscribeStore {
 | 
				
			||||||
 | 
					    insert(item: UnsubscribeEntry): Promise<void>;
 | 
				
			||||||
 | 
					    delete(item: UnsubscribeEntry): Promise<void>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,50 @@
 | 
				
			|||||||
 | 
					import type { Logger, LogProvider } from '../../logger';
 | 
				
			||||||
 | 
					import type { Db } from '../../db/db';
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					    UnsubscribeEntry,
 | 
				
			||||||
 | 
					    IUserUnsubscribeStore,
 | 
				
			||||||
 | 
					} from './user-unsubscribe-store-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const COLUMNS = ['user_id', 'subscription', 'created_at'];
 | 
				
			||||||
 | 
					export const TABLE = 'user_unsubscription';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface IUserUnsubscribeTable {
 | 
				
			||||||
 | 
					    user_id: number;
 | 
				
			||||||
 | 
					    subscription: string;
 | 
				
			||||||
 | 
					    created_at?: Date;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rowToField = (row: IUserUnsubscribeTable): UnsubscribeEntry => ({
 | 
				
			||||||
 | 
					    userId: row.user_id,
 | 
				
			||||||
 | 
					    subscription: row.subscription,
 | 
				
			||||||
 | 
					    createdAt: row.created_at,
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class UserUnsubscribeStore implements IUserUnsubscribeStore {
 | 
				
			||||||
 | 
					    private db: Db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private logger: Logger;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor(db: Db, getLogger: LogProvider) {
 | 
				
			||||||
 | 
					        this.db = db;
 | 
				
			||||||
 | 
					        this.logger = getLogger('user-unsubscribe-store.ts');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async insert({ userId, subscription }) {
 | 
				
			||||||
 | 
					        await this.db
 | 
				
			||||||
 | 
					            .table<IUserUnsubscribeTable>(TABLE)
 | 
				
			||||||
 | 
					            .insert({ user_id: userId, subscription: subscription })
 | 
				
			||||||
 | 
					            .onConflict(['user_id', 'subscription'])
 | 
				
			||||||
 | 
					            .ignore()
 | 
				
			||||||
 | 
					            .returning(COLUMNS);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async delete({ userId, subscription }): Promise<void> {
 | 
				
			||||||
 | 
					        await this.db
 | 
				
			||||||
 | 
					            .table<IUserUnsubscribeTable>(TABLE)
 | 
				
			||||||
 | 
					            .where({ user_id: userId, subscription: subscription })
 | 
				
			||||||
 | 
					            .del();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    destroy(): void {}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -51,6 +51,8 @@ import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types
 | 
				
			|||||||
import type { IProjectReadModel } from '../features/project/project-read-model-type';
 | 
					import type { IProjectReadModel } from '../features/project/project-read-model-type';
 | 
				
			||||||
import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-type';
 | 
					import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-type';
 | 
				
			||||||
import { IOnboardingStore } from '../features/onboarding/onboarding-store-type';
 | 
					import { IOnboardingStore } from '../features/onboarding/onboarding-store-type';
 | 
				
			||||||
 | 
					import type { IUserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store-type';
 | 
				
			||||||
 | 
					import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IUnleashStores {
 | 
					export interface IUnleashStores {
 | 
				
			||||||
    accessStore: IAccessStore;
 | 
					    accessStore: IAccessStore;
 | 
				
			||||||
@ -106,6 +108,8 @@ export interface IUnleashStores {
 | 
				
			|||||||
    projectReadModel: IProjectReadModel;
 | 
					    projectReadModel: IProjectReadModel;
 | 
				
			||||||
    onboardingReadModel: IOnboardingReadModel;
 | 
					    onboardingReadModel: IOnboardingReadModel;
 | 
				
			||||||
    onboardingStore: IOnboardingStore;
 | 
					    onboardingStore: IOnboardingStore;
 | 
				
			||||||
 | 
					    userUnsubscribeStore: IUserUnsubscribeStore;
 | 
				
			||||||
 | 
					    userSubscriptionsReadModel: IUserSubscriptionsReadModel;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										4
									
								
								src/test/fixtures/store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								src/test/fixtures/store.ts
									
									
									
									
										vendored
									
									
								
							@ -54,6 +54,8 @@ import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-to
 | 
				
			|||||||
import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
 | 
					import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
 | 
				
			||||||
import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store';
 | 
					import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store';
 | 
				
			||||||
import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel';
 | 
					import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel';
 | 
				
			||||||
 | 
					import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/fake-user-unsubscribe-store';
 | 
				
			||||||
 | 
					import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const db = {
 | 
					const db = {
 | 
				
			||||||
    select: () => ({
 | 
					    select: () => ({
 | 
				
			||||||
@ -117,6 +119,8 @@ const createStores: () => IUnleashStores = () => {
 | 
				
			|||||||
        featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
 | 
					        featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
 | 
				
			||||||
        projectReadModel: createFakeProjectReadModel(),
 | 
					        projectReadModel: createFakeProjectReadModel(),
 | 
				
			||||||
        onboardingStore: new FakeOnboardingStore(),
 | 
					        onboardingStore: new FakeOnboardingStore(),
 | 
				
			||||||
 | 
					        userUnsubscribeStore: new FakeUserUnsubscribeStore(),
 | 
				
			||||||
 | 
					        userSubscriptionsReadModel: new FakeUserSubscriptionsReadModel(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user