From a2a94dd011885cd670834740da9f2c12279f651a Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Tue, 5 Nov 2024 14:17:37 +0100 Subject: [PATCH] feat: user profile returns user subscriptions (#8656) --- src/lib/db/index.ts | 5 +---- .../createUserSubscriptionsService.ts | 8 ++++++-- .../user-subscriptions-read-model.test.ts | 7 +------ .../user-subscriptions-read-model.ts | 3 +-- .../user-subscriptions-service.ts | 16 +++++++++++++++- src/lib/openapi/spec/profile-schema.test.ts | 1 + src/lib/openapi/spec/profile-schema.ts | 10 +++++++++- src/lib/routes/admin-api/user/user.test.ts | 18 ++++++++++++++++++ src/lib/routes/admin-api/user/user.ts | 14 ++++++++++++-- 9 files changed, 64 insertions(+), 18 deletions(-) diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 4785a3fbbb..72ddcd4b49 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -190,10 +190,7 @@ export const createStores = ( config.flagResolver, ), userUnsubscribeStore: new UserUnsubscribeStore(db), - userSubscriptionsReadModel: new UserSubscriptionsReadModel( - db, - eventBus, - ), + userSubscriptionsReadModel: new UserSubscriptionsReadModel(db), }; }; diff --git a/src/lib/features/user-subscriptions/createUserSubscriptionsService.ts b/src/lib/features/user-subscriptions/createUserSubscriptionsService.ts index e0a9d5b722..c2fe97fe0b 100644 --- a/src/lib/features/user-subscriptions/createUserSubscriptionsService.ts +++ b/src/lib/features/user-subscriptions/createUserSubscriptionsService.ts @@ -6,15 +6,18 @@ import { createFakeEventsService, } from '../events/createEventsService'; import { FakeUserUnsubscribeStore } from './fake-user-unsubscribe-store'; +import { UserSubscriptionsReadModel } from './user-subscriptions-read-model'; +import { FakeUserSubscriptionsReadModel } from './fake-user-subscriptions-read-model'; export const createUserSubscriptionsService = (config: IUnleashConfig) => (db: Db): UserSubscriptionsService => { const userUnsubscribeStore = new UserUnsubscribeStore(db); + const userSubscriptionsReadModel = new UserSubscriptionsReadModel(db); const eventService = createEventsService(db, config); const userSubscriptionsService = new UserSubscriptionsService( - { userUnsubscribeStore }, + { userUnsubscribeStore, userSubscriptionsReadModel }, config, eventService, ); @@ -26,10 +29,11 @@ export const createFakeUserSubscriptionsService = ( config: IUnleashConfig, ): UserSubscriptionsService => { const userUnsubscribeStore = new FakeUserUnsubscribeStore(); + const userSubscriptionsReadModel = new FakeUserSubscriptionsReadModel(); const eventService = createFakeEventsService(config); const userSubscriptionsService = new UserSubscriptionsService( - { userUnsubscribeStore }, + { userUnsubscribeStore, userSubscriptionsReadModel }, config, eventService, ); diff --git a/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts b/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts index 9b8101fd55..30ba7e4302 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-read-model.test.ts @@ -2,7 +2,6 @@ import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import getLogger from '../../../test/fixtures/no-logger'; import { UserSubscriptionsReadModel } from './user-subscriptions-read-model'; import type { IUserSubscriptionsReadModel } from './user-subscriptions-read-model-type'; -import EventEmitter from 'events'; import { SUBSCRIPTION_TYPES } from './user-subscriptions-read-model-type'; import type { IUnleashStores, IUserStore } from '../../types'; import type { IUserUnsubscribeStore } from './user-unsubscribe-store-type'; @@ -21,11 +20,7 @@ beforeAll(async () => { stores = db.stores; userStore = stores.userStore; userUnsubscribeStore = stores.userUnsubscribeStore; - const eventBus = new EventEmitter(); - userSubscriptionsReadModel = new UserSubscriptionsReadModel( - db.rawDatabase, - eventBus, - ); + userSubscriptionsReadModel = new UserSubscriptionsReadModel(db.rawDatabase); }); beforeEach(async () => { diff --git a/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts b/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts index 88c981ebb1..c8176391ab 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-read-model.ts @@ -1,5 +1,4 @@ import type { Db } from '../../db/db'; -import type EventEmitter from 'events'; import { SUBSCRIPTION_TYPES, type IUserSubscriptionsReadModel, @@ -26,7 +25,7 @@ const mapRowToSubscriber = (row) => export class UserSubscriptionsReadModel implements IUserSubscriptionsReadModel { private db: Db; - constructor(db: Db, eventBus: EventEmitter) { + constructor(db: Db) { this.db = db; } diff --git a/src/lib/features/user-subscriptions/user-subscriptions-service.ts b/src/lib/features/user-subscriptions/user-subscriptions-service.ts index cebd781a93..9dbb8d9a53 100644 --- a/src/lib/features/user-subscriptions/user-subscriptions-service.ts +++ b/src/lib/features/user-subscriptions/user-subscriptions-service.ts @@ -6,24 +6,38 @@ import type { UnsubscribeEntry, } from './user-unsubscribe-store-type'; import type EventService from '../events/event-service'; +import type { IUserSubscriptionsReadModel } from './user-subscriptions-read-model-type'; export class UserSubscriptionsService { private userUnsubscribeStore: IUserUnsubscribeStore; + private userSubscriptionsReadModel: IUserSubscriptionsReadModel; + private eventService: EventService; private logger: Logger; constructor( - { userUnsubscribeStore }: Pick, + { + userUnsubscribeStore, + userSubscriptionsReadModel, + }: Pick< + IUnleashStores, + 'userUnsubscribeStore' | 'userSubscriptionsReadModel' + >, { getLogger }: Pick, eventService: EventService, ) { this.userUnsubscribeStore = userUnsubscribeStore; + this.userSubscriptionsReadModel = userSubscriptionsReadModel; this.eventService = eventService; this.logger = getLogger('services/user-subscription-service.ts'); } + async getUserSubscriptions(userId: number) { + return this.userSubscriptionsReadModel.getUserSubscriptions(userId); + } + async subscribe( userId: number, subscription: string, diff --git a/src/lib/openapi/spec/profile-schema.test.ts b/src/lib/openapi/spec/profile-schema.test.ts index de7d17bcc8..e168e8eb91 100644 --- a/src/lib/openapi/spec/profile-schema.test.ts +++ b/src/lib/openapi/spec/profile-schema.test.ts @@ -9,6 +9,7 @@ test('profileSchema', () => { name: 'Admin', }, projects: ['default', 'secretproject'], + subscriptions: ['productivity-report'], features: [ { name: 'firstFeature', project: 'default' }, { name: 'secondFeature', project: 'secretproject' }, diff --git a/src/lib/openapi/spec/profile-schema.ts b/src/lib/openapi/spec/profile-schema.ts index 08a5a3e41a..92ebba40b6 100644 --- a/src/lib/openapi/spec/profile-schema.ts +++ b/src/lib/openapi/spec/profile-schema.ts @@ -7,7 +7,7 @@ export const profileSchema = { type: 'object', additionalProperties: false, description: 'User profile overview', - required: ['rootRole', 'projects', 'features'], + required: ['rootRole', 'projects', 'features', 'subscriptions'], properties: { rootRole: { $ref: '#/components/schemas/roleSchema', @@ -20,6 +20,14 @@ export const profileSchema = { }, example: ['my-projectA', 'my-projectB'], }, + subscriptions: { + description: 'Which email subscriptions this user is subscribed to', + type: 'array', + items: { + type: 'string', + }, + example: ['productivity-report'], + }, features: { description: 'Deprecated, always returns empty array', type: 'array', diff --git a/src/lib/routes/admin-api/user/user.test.ts b/src/lib/routes/admin-api/user/user.test.ts index d92fac9113..5e759e8e0f 100644 --- a/src/lib/routes/admin-api/user/user.test.ts +++ b/src/lib/routes/admin-api/user/user.test.ts @@ -53,6 +53,24 @@ test('should return current user', async () => { }); const owaspPassword = 't7GTx&$Y9pcsnxRv6'; +test('should return current profile', async () => { + expect.assertions(1); + const { request, base } = await getSetup(); + + return request + .get(`${base}/api/admin/user/profile`) + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body).toMatchObject({ + projects: [], + rootRole: { id: -1, name: 'Viewer', type: 'root' }, + subscriptions: ['productivity-report'], + features: [], + }); + }); +}); + test('should allow user to change password', async () => { const { request, base, userStore } = await getSetup(); await request diff --git a/src/lib/routes/admin-api/user/user.ts b/src/lib/routes/admin-api/user/user.ts index f6c2d1b6d8..af68fe2133 100644 --- a/src/lib/routes/admin-api/user/user.ts +++ b/src/lib/routes/admin-api/user/user.ts @@ -32,6 +32,7 @@ import { type RolesSchema, } from '../../../openapi/spec/roles-schema'; import type { IFlagResolver } from '../../../types'; +import type { UserSubscriptionsService } from '../../../features/user-subscriptions/user-subscriptions-service'; class UserController extends Controller { private accessService: AccessService; @@ -48,6 +49,8 @@ class UserController extends Controller { private flagResolver: IFlagResolver; + private userSubscriptionsService: UserSubscriptionsService; + constructor( config: IUnleashConfig, { @@ -57,6 +60,7 @@ class UserController extends Controller { userSplashService, openApiService, projectService, + transactionalUserSubscriptionsService, }: Pick< IUnleashServices, | 'accessService' @@ -65,6 +69,7 @@ class UserController extends Controller { | 'userSplashService' | 'openApiService' | 'projectService' + | 'transactionalUserSubscriptionsService' >, ) { super(config); @@ -74,6 +79,7 @@ class UserController extends Controller { this.userSplashService = userSplashService; this.openApiService = openApiService; this.projectService = projectService; + this.userSubscriptionsService = transactionalUserSubscriptionsService; this.flagResolver = config.flagResolver; this.route({ @@ -237,12 +243,16 @@ class UserController extends Controller { ): Promise { const { user } = req; - const projects = await this.projectService.getProjectsByUser(user.id); + const [projects, rootRole, subscriptions] = await Promise.all([ + this.projectService.getProjectsByUser(user.id), + this.accessService.getRootRoleForUser(user.id), + this.userSubscriptionsService.getUserSubscriptions(user.id), + ]); - const rootRole = await this.accessService.getRootRoleForUser(user.id); const responseData: ProfileSchema = { projects, rootRole, + subscriptions, features: [], };