From 44bf6615a32909f4decdb24d5c524e03c21c3dc3 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 25 Sep 2024 13:32:33 +0200 Subject: [PATCH] feat: add project owners to personal dashboard project payload (#8248) This PR adds project owner information to the personal dashboard's project payload. To do so, it uses the existing project owners read model. I've had to make a few changes to the project owners read model to accomodate this: - make the input type to `addOwners` more lenient. We only need the project ids, so we can make that the only required property - fall back to using email as the name if the user has no name or username (such as if you sign up with the demo auth) --- .../personal-dashboard-controller.e2e.test.ts | 41 +++++++++++++++++++ .../personal-dashboard-read-model-type.ts | 5 +++ .../personal-dashboard-service.ts | 23 +++++++++-- .../project/fake-project-owners-read-model.ts | 9 ++-- .../project/project-owners-read-model.ts | 20 +++++---- .../project/project-owners-read-model.type.ts | 12 ++++-- .../openapi/spec/personal-dashboard-schema.ts | 2 + src/lib/services/index.ts | 4 ++ 8 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts index 23701039caab..179c793ea691 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts @@ -110,6 +110,11 @@ test('should return personal dashboard with membered projects', async () => { type: 'root', }, ], + owners: [ + { + ownerType: 'system', + }, + ], }, { name: projectA.name, @@ -121,6 +126,15 @@ test('should return personal dashboard with membered projects', async () => { type: 'project', }, ], + owners: [ + { + email: 'user1@test.com', + imageUrl: + 'https://gravatar.com/avatar/a8cc79d8407a64b0d8982df34e3525afd298a479fe68f300651380730dbf23e9?s=42&d=retro&r=g', + name: 'user1@test.com', + ownerType: 'user', + }, + ], }, { name: projectC.name, @@ -132,6 +146,15 @@ test('should return personal dashboard with membered projects', async () => { type: 'project', }, ], + owners: [ + { + email: 'user2@test.com', + imageUrl: + 'https://gravatar.com/avatar/706150f3ef810ea66acb30c6d55f1a7e545338747072609e47df71c7c7ccc6a4?s=42&d=retro&r=g', + name: 'user2@test.com', + ownerType: 'user', + }, + ], }, ], }); @@ -181,6 +204,11 @@ test('should return projects where users are part of a group', async () => { type: 'root', }, ], + owners: [ + { + ownerType: 'system', + }, + ], }, { name: projectA.name, @@ -197,6 +225,19 @@ test('should return projects where users are part of a group', async () => { type: 'project', }, ], + owners: [ + { + email: 'user1@test.com', + imageUrl: + 'https://gravatar.com/avatar/a8cc79d8407a64b0d8982df34e3525afd298a479fe68f300651380730dbf23e9?s=42&d=retro&r=g', + name: 'user1@test.com', + ownerType: 'user', + }, + { + name: 'groupA', + ownerType: 'group', + }, + ], }, ], }); diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts index d928aabddd08..b3db032ff8a7 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts @@ -1,3 +1,5 @@ +import type { ProjectOwners } from '../project/project-owners-read-model.type'; + export type PersonalFeature = { name: string; type: string; project: string }; export type PersonalProject = { name: string; @@ -8,6 +10,9 @@ export type PersonalProject = { type: 'custom' | 'project' | 'root' | 'custom-root'; }[]; }; +export type PersonalProjectWithOwners = PersonalProject & { + owners: ProjectOwners; +}; export interface IPersonalDashboardReadModel { getPersonalFeatures(userId: number): Promise; diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index d25a7e86856c..f4aa5fccba79 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -1,21 +1,36 @@ +import type { IProjectOwnersReadModel } from '../project/project-owners-read-model.type'; import type { IPersonalDashboardReadModel, PersonalFeature, - PersonalProject, + PersonalProjectWithOwners, } from './personal-dashboard-read-model-type'; export class PersonalDashboardService { private personalDashboardReadModel: IPersonalDashboardReadModel; - constructor(personalDashboardReadModel: IPersonalDashboardReadModel) { + private projectOwnersReadModel: IProjectOwnersReadModel; + + constructor( + personalDashboardReadModel: IPersonalDashboardReadModel, + projectOwnersReadModel: IProjectOwnersReadModel, + ) { this.personalDashboardReadModel = personalDashboardReadModel; + this.projectOwnersReadModel = projectOwnersReadModel; } getPersonalFeatures(userId: number): Promise { return this.personalDashboardReadModel.getPersonalFeatures(userId); } - getPersonalProjects(userId: number): Promise { - return this.personalDashboardReadModel.getPersonalProjects(userId); + async getPersonalProjects( + userId: number, + ): Promise { + const projects = + await this.personalDashboardReadModel.getPersonalProjects(userId); + + const withOwners = + await this.projectOwnersReadModel.addOwners(projects); + + return withOwners; } } diff --git a/src/lib/features/project/fake-project-owners-read-model.ts b/src/lib/features/project/fake-project-owners-read-model.ts index dc99ecb83793..cab7bd59ad8b 100644 --- a/src/lib/features/project/fake-project-owners-read-model.ts +++ b/src/lib/features/project/fake-project-owners-read-model.ts @@ -1,13 +1,12 @@ import type { IProjectOwnersReadModel, - IProjectForUiWithOwners, + WithProjectOwners, } from './project-owners-read-model.type'; -import type { ProjectForUi } from './project-read-model-type'; export class FakeProjectOwnersReadModel implements IProjectOwnersReadModel { - async addOwners( - projects: ProjectForUi[], - ): Promise { + async addOwners( + projects: T[], + ): Promise> { return projects.map((project) => ({ ...project, owners: [{ ownerType: 'system' }], diff --git a/src/lib/features/project/project-owners-read-model.ts b/src/lib/features/project/project-owners-read-model.ts index c4c39dbfbd2a..7f282231f0ac 100644 --- a/src/lib/features/project/project-owners-read-model.ts +++ b/src/lib/features/project/project-owners-read-model.ts @@ -4,11 +4,10 @@ import { anonymise, generateImageUrl } from '../../util'; import type { GroupProjectOwner, IProjectOwnersReadModel, - IProjectForUiWithOwners, ProjectOwnersDictionary, UserProjectOwner, + WithProjectOwners, } from './project-owners-read-model.type'; -import type { ProjectForUi } from './project-read-model-type'; const T = { ROLE_USER: 'role_user', @@ -24,10 +23,10 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { this.db = db; } - static addOwnerData( - projects: ProjectForUi[], + static addOwnerData( + projects: T[], owners: ProjectOwnersDictionary, - ): IProjectForUiWithOwners[] { + ): WithProjectOwners { return projects.map((project) => ({ ...project, owners: owners[project.id] || [{ ownerType: 'system' }], @@ -64,7 +63,10 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { const data: UserProjectOwner = { ownerType: 'user', - name: user?.name || user?.username, + name: + user?.name || + user?.username || + processSensitiveData(user?.email), email: processSensitiveData(user?.email), imageUrl: generateImageUrl(user), }; @@ -138,10 +140,10 @@ export class ProjectOwnersReadModel implements IProjectOwnersReadModel { return dict; } - async addOwners( - projects: ProjectForUi[], + async addOwners( + projects: T[], anonymizeProjectOwners: boolean = false, - ): Promise { + ): Promise> { const owners = await this.getAllProjectOwners(anonymizeProjectOwners); return ProjectOwnersReadModel.addOwnerData(projects, owners); diff --git a/src/lib/features/project/project-owners-read-model.type.ts b/src/lib/features/project/project-owners-read-model.type.ts index 843f2208c234..b8a8faaa0adc 100644 --- a/src/lib/features/project/project-owners-read-model.type.ts +++ b/src/lib/features/project/project-owners-read-model.type.ts @@ -11,7 +11,7 @@ export type GroupProjectOwner = { ownerType: 'group'; name: string; }; -type ProjectOwners = +export type ProjectOwners = | [SystemOwner] | Array; @@ -21,9 +21,13 @@ export type IProjectForUiWithOwners = ProjectForUi & { owners: ProjectOwners; }; +export type WithProjectOwners = (T & { + owners: ProjectOwners; +})[]; + export interface IProjectOwnersReadModel { - addOwners( - projects: ProjectForUi[], + addOwners( + projects: T[], anonymizeProjectOwners?: boolean, - ): Promise; + ): Promise>; } diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index 0fd85c82480c..69339aca6f6d 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -1,4 +1,5 @@ import type { FromSchema } from 'json-schema-to-ts'; +import { projectSchema } from './project-schema'; export const personalDashboardSchema = { $id: '#/components/schemas/personalDashboardSchema', @@ -24,6 +25,7 @@ export const personalDashboardSchema = { example: 'My Project', description: 'The name of the project', }, + owners: projectSchema.properties.owners, roles: { type: 'array', description: diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 1be5769f1579..40c517171b06 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -149,6 +149,8 @@ import { OnboardingService } from '../features/onboarding/onboarding-service'; import { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service'; import { PersonalDashboardReadModel } from '../features/personal-dashboard/personal-dashboard-read-model'; import { FakePersonalDashboardReadModel } from '../features/personal-dashboard/fake-personal-dashboard-read-model'; +import { ProjectOwnersReadModel } from '../features/project/project-owners-read-model'; +import { FakeProjectOwnersReadModel } from '../features/project/fake-project-owners-read-model'; export const createServices = ( stores: IUnleashStores, @@ -408,6 +410,8 @@ export const createServices = ( db ? new PersonalDashboardReadModel(db) : new FakePersonalDashboardReadModel(), + + db ? new ProjectOwnersReadModel(db) : new FakeProjectOwnersReadModel(), ); return {