1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: User subscriptions store and service (#8648)

Co-authored-by: kwasniew <kwasniewski.mateusz@gmail.com>
This commit is contained in:
Tymoteusz Czech 2024-11-05 10:14:47 +01:00 committed by GitHub
parent 2e64df671a
commit 2bd2d74f83
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 230 additions and 0 deletions

View File

@ -54,6 +54,8 @@ import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/featur
import { createProjectReadModel } from '../features/project/createProjectReadModel';
import { OnboardingStore } from '../features/onboarding/onboarding-store';
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 = (
config: IUnleashConfig,
@ -187,6 +189,11 @@ export const createStores = (
eventBus,
config.flagResolver,
),
userUnsubscribeStore: new UserUnsubscribeStore(db, getLogger),
userSubscriptionsReadModel: new UserSubscriptionsReadModel(
db,
eventBus,
),
};
};

View File

@ -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'];
}
}

View File

@ -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 {}
}

View File

@ -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;

View File

@ -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),
);
}
}

View File

@ -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,
// }),
// );
}
}

View File

@ -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>;
}

View File

@ -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 {}
}

View File

@ -51,6 +51,8 @@ import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types
import type { IProjectReadModel } from '../features/project/project-read-model-type';
import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-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 {
accessStore: IAccessStore;
@ -106,6 +108,8 @@ export interface IUnleashStores {
projectReadModel: IProjectReadModel;
onboardingReadModel: IOnboardingReadModel;
onboardingStore: IOnboardingStore;
userUnsubscribeStore: IUserUnsubscribeStore;
userSubscriptionsReadModel: IUserSubscriptionsReadModel;
}
export {

View File

@ -54,6 +54,8 @@ import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-to
import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel';
import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store';
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 = {
select: () => ({
@ -117,6 +119,8 @@ const createStores: () => IUnleashStores = () => {
featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(),
projectReadModel: createFakeProjectReadModel(),
onboardingStore: new FakeOnboardingStore(),
userUnsubscribeStore: new FakeUserUnsubscribeStore(),
userSubscriptionsReadModel: new FakeUserSubscriptionsReadModel(),
};
};