mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +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