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:
parent
2e64df671a
commit
2bd2d74f83
@ -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,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 { 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 {
|
||||
|
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 { 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(),
|
||||
};
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user