From fe4f99c156618440263c5bce1c931930a1a082aa Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 10:33:32 +0200 Subject: [PATCH 1/9] feat: add role information to projects --- .../openapi/spec/personal-dashboard-schema.ts | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index ec98f861e4c3..cc2d8e5c5542 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -12,13 +12,48 @@ export const personalDashboardSchema = { items: { type: 'object', additionalProperties: false, - required: ['id'], + required: ['id', 'name', 'roles'], properties: { id: { type: 'string', example: 'my-project-id', description: 'The id of the project', }, + name: { + type: 'string', + example: 'My Project', + description: 'The name of the project', + }, + roles: { + type: 'array', + description: + 'The list of roles that the user has in this project.', + minItems: 1, + items: { + type: 'object', + description: 'An Unleash role.', + additionalProperties: false, + required: ['name', 'id', 'type'], + properties: { + name: { + type: 'string', + example: 'Owner', + description: 'The name of the role', + }, + id: { + type: 'integer', + example: 4, + description: 'The id of the role', + }, + type: { + type: 'string', + enum: ['custom', 'project'], + example: 'project', + description: 'The type of the role', + }, + }, + }, + }, }, }, description: From f3f5162206bf4a996d859370f87860590c916f58 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 10:45:04 +0200 Subject: [PATCH 2/9] feat: add owners --- src/lib/openapi/spec/personal-dashboard-schema.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index cc2d8e5c5542..17e0e762cbd9 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: From 3db25e63a6510240d371db59cf8c3c6aca4d00b4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 13:53:12 +0200 Subject: [PATCH 3/9] feat: add rough test --- .../personal-dashboard-controller.e2e.test.ts | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) 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 830dc9629f72..73bcddc231a3 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 @@ -4,6 +4,7 @@ import { setupAppWithAuth, } from '../../../test/e2e/helpers/test-helper'; import getLogger from '../../../test/fixtures/no-logger'; +import type { IUser } from '../../types'; let app: IUnleashTest; let db: ITestDb; @@ -61,3 +62,75 @@ test('should return personal dashboard with own flags and favorited flags', asyn ], }); }); + +const createProject = async (name: string, user: IUser) => { + const auditUser = { + id: 1, + username: 'audit user', + ip: '127.0.0.1', + }; + const project = await app.services.projectService.createProject( + { + name, + }, + user, + auditUser, + ); + return project; +}; + +test('should return personal dashboard with membered projects', async () => { + // create project A with user 1 + // create project B with user 1 + const { body: user1 } = await loginUser('user1@test.com'); + const projectA = await createProject('Project A', user1); + await createProject('Project B', user1); + + // create project C with user 2 + const { body: user2 } = await loginUser('user2@test.com'); + const projectC = await createProject('Project C', user2); + + // Add user 2 as a member of project A + await app.services.projectService.addAccess( + projectA.id, + [5], + [], + [user2.id], + user1, + ); + + // Add user 1 as an owner of project C + await app.services.projectService.addAccess( + projectC.id, + [4], + [], + [user1.id], + user2, + ); + + const { body } = await app.request.get(`/api/admin/personal-dashboard`); + + expect(body).toMatchObject({ + projects: [ + { + name: projectA.name, + id: projectA.id, + owners: [{ id: user1.id, name: user1.email, imageUrl: '' }], + roles: ['member'], + }, + { + name: projectC.name, + id: projectC.id, + owners: [ + { id: user2.id, name: user2.email, imageUrl: '' }, + { id: user1.id, name: user1.email, imageUrl: '' }, + ], + roles: ['owner'], + }, + ], + }); +}); + +test('should return projects where users are part of a group', () => { + // TODO +}); From 09441fbea972f730ea4f3100a968701922e926e5 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 14:18:10 +0200 Subject: [PATCH 4/9] feat: make owners required --- src/lib/openapi/spec/personal-dashboard-schema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index 17e0e762cbd9..47b6550c7306 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -13,7 +13,7 @@ export const personalDashboardSchema = { items: { type: 'object', additionalProperties: false, - required: ['id', 'name', 'roles'], + required: ['id', 'name', 'roles', 'owners'], properties: { id: { type: 'string', From 7b17cfb5f27d5eac1847fb9574015249189259d7 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 14:19:23 +0200 Subject: [PATCH 5/9] feat: remove project owners for now --- src/lib/openapi/spec/personal-dashboard-schema.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index 47b6550c7306..cc2d8e5c5542 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -1,5 +1,4 @@ import type { FromSchema } from 'json-schema-to-ts'; -import { projectSchema } from './project-schema'; export const personalDashboardSchema = { $id: '#/components/schemas/personalDashboardSchema', @@ -13,7 +12,7 @@ export const personalDashboardSchema = { items: { type: 'object', additionalProperties: false, - required: ['id', 'name', 'roles', 'owners'], + required: ['id', 'name', 'roles'], properties: { id: { type: 'string', @@ -25,7 +24,6 @@ export const personalDashboardSchema = { example: 'My Project', description: 'The name of the project', }, - owners: projectSchema.properties.owners, roles: { type: 'array', description: From 6feb2812046320adf36aa7651a0ed1c6adf52de4 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 14:21:23 +0200 Subject: [PATCH 6/9] feat: sketch out first steps --- .../fake-personal-dashboard-read-model.ts | 5 ++ .../personal-dashboard-controller.ts | 5 +- .../personal-dashboard-read-model-type.ts | 6 +++ .../personal-dashboard-read-model.ts | 49 +++++++++++++++++++ .../personal-dashboard-service.ts | 5 ++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts index 682988dc4578..1df56844b030 100644 --- a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts @@ -1,6 +1,7 @@ import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class FakePersonalDashboardReadModel @@ -9,4 +10,8 @@ export class FakePersonalDashboardReadModel async getPersonalFeatures(userId: number): Promise { return []; } + + async getPersonalProjects(userId: number): Promise { + return []; + } } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts index 057002884c19..a7e1f3d21fd1 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.ts @@ -63,11 +63,14 @@ export default class PersonalDashboardController extends Controller { user.id, ); + const projects = + await this.personalDashboardService.getPersonalProjects(user.id); + this.openApiService.respondWithValidation( 200, res, personalDashboardSchema.$id, - { projects: [], flags }, + { projects, flags }, ); } } 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 f63e43ae080b..7d251d7c2da8 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,5 +1,11 @@ export type PersonalFeature = { name: string; type: string; project: string }; +export type PersonalProject = { + name: string; + id: string; + roles: { name: string; id: number; type: 'custom' | 'project' }[]; +}; export interface IPersonalDashboardReadModel { getPersonalFeatures(userId: number): Promise; + getPersonalProjects(userId: number): Promise; } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts index 317113cd755c..7f239e7c5603 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts @@ -2,6 +2,7 @@ import type { Db } from '../../db/db'; import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { @@ -11,6 +12,54 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { this.db = db; } + async getPersonalProjects(userId: number): Promise { + const result = await this.db<{ + name: string; + id: string; + roleId: number; + roleName: string; + roleType: string; + }>('projects') + .join('role_user', 'projects.id', 'role_user.project') + .join('roles', 'role_user.role_id', 'roles.id') + .where('role_user.user_id', userId) + .whereNull('project.archived_at') + .select( + 'projects.name', + 'projects.id', + 'roles.id as roleId', + 'roles.name as roleName', + 'roles.type as roleType', + ) + .orderBy('projects.name', 'desc') + .limit(100); + + console.log(result); + + return result.reduce((acc, row) => { + if (acc[row.id]) { + acc[row.id].roles.push({ + id: row.roleId, + name: row.roleName, + type: row.roleType, + }); + } else { + acc[row.id] = { + id: row.id, + name: row.name, + roles: [ + { + id: row.roleId, + name: row.roleName, + type: row.roleType, + }, + ], + }; + } + return acc; + }, {}); + } + async getPersonalFeatures(userId: number): Promise { const result = await this.db<{ name: string; diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index 4837d93324b5..d25a7e86856c 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -1,6 +1,7 @@ import type { IPersonalDashboardReadModel, PersonalFeature, + PersonalProject, } from './personal-dashboard-read-model-type'; export class PersonalDashboardService { @@ -13,4 +14,8 @@ export class PersonalDashboardService { getPersonalFeatures(userId: number): Promise { return this.personalDashboardReadModel.getPersonalFeatures(userId); } + + getPersonalProjects(userId: number): Promise { + return this.personalDashboardReadModel.getPersonalProjects(userId); + } } From 2a754aeb33a85ce1cb0f2ec5631177ed9eb91d53 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 24 Sep 2024 14:37:31 +0200 Subject: [PATCH 7/9] feat: return projects --- .../personal-dashboard-controller.e2e.test.ts | 39 ++++++++++++------- .../personal-dashboard-read-model-type.ts | 6 ++- .../personal-dashboard-read-model.ts | 11 +++--- .../openapi/spec/personal-dashboard-schema.ts | 7 +++- 4 files changed, 41 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 73bcddc231a3..f38277cf3d9f 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 @@ -99,33 +99,42 @@ test('should return personal dashboard with membered projects', async () => { user1, ); - // Add user 1 as an owner of project C - await app.services.projectService.addAccess( - projectC.id, - [4], - [], - [user1.id], - user2, - ); - const { body } = await app.request.get(`/api/admin/personal-dashboard`); expect(body).toMatchObject({ projects: [ + { + name: 'Default', + id: 'default', + roles: [ + { + name: 'Editor', + id: 2, + type: 'root', + }, + ], + }, { name: projectA.name, id: projectA.id, - owners: [{ id: user1.id, name: user1.email, imageUrl: '' }], - roles: ['member'], + roles: [ + { + name: 'Member', + id: 5, + type: 'project', + }, + ], }, { name: projectC.name, id: projectC.id, - owners: [ - { id: user2.id, name: user2.email, imageUrl: '' }, - { id: user1.id, name: user1.email, imageUrl: '' }, + roles: [ + { + name: 'Owner', + id: 4, + type: 'project', + }, ], - roles: ['owner'], }, ], }); 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 7d251d7c2da8..d928aabddd08 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 @@ -2,7 +2,11 @@ export type PersonalFeature = { name: string; type: string; project: string }; export type PersonalProject = { name: string; id: string; - roles: { name: string; id: number; type: 'custom' | 'project' }[]; + roles: { + name: string; + id: number; + type: 'custom' | 'project' | 'root' | 'custom-root'; + }[]; }; export interface IPersonalDashboardReadModel { diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts index 7f239e7c5603..dd9a856355e8 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts @@ -23,7 +23,7 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { .join('role_user', 'projects.id', 'role_user.project') .join('roles', 'role_user.role_id', 'roles.id') .where('role_user.user_id', userId) - .whereNull('project.archived_at') + .whereNull('projects.archived_at') .select( 'projects.name', 'projects.id', @@ -31,12 +31,9 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { 'roles.name as roleName', 'roles.type as roleType', ) - .orderBy('projects.name', 'desc') .limit(100); - console.log(result); - - return result.reduce((acc, row) => { + const dict = result.reduce((acc, row) => { if (acc[row.id]) { acc[row.id].roles.push({ id: row.roleId, @@ -58,6 +55,10 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { } return acc; }, {}); + + const projectList: PersonalProject[] = Object.values(dict); + projectList.sort((a, b) => a.name.localeCompare(b.name)); + return projectList; } async getPersonalFeatures(userId: number): Promise { diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index cc2d8e5c5542..0fd85c82480c 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -47,7 +47,12 @@ export const personalDashboardSchema = { }, type: { type: 'string', - enum: ['custom', 'project'], + enum: [ + 'custom', + 'project', + 'root', + 'custom-root', + ], example: 'project', description: 'The type of the role', }, From 08e29178391e577a7366cd539856c35822f90b80 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 25 Sep 2024 08:12:52 +0200 Subject: [PATCH 8/9] feat: fix other test --- .../personal-dashboard/personal-dashboard-controller.e2e.test.ts | 1 - 1 file changed, 1 deletion(-) 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 f38277cf3d9f..979ea23df84d 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 @@ -54,7 +54,6 @@ test('should return personal dashboard with own flags and favorited flags', asyn const { body } = await app.request.get(`/api/admin/personal-dashboard`); expect(body).toMatchObject({ - projects: [], flags: [ { name: 'my_feature_d', type: 'release', project: 'default' }, { name: 'my_feature_c', type: 'release', project: 'default' }, From 0af98f5465a539dbe1bdf38e1196d90cb6753d96 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 25 Sep 2024 08:13:28 +0200 Subject: [PATCH 9/9] feat: remove some comments, add others --- .../personal-dashboard-controller.e2e.test.ts | 6 +----- 1 file changed, 1 insertion(+), 5 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 979ea23df84d..2512d1db8e2a 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 @@ -79,20 +79,16 @@ const createProject = async (name: string, user: IUser) => { }; test('should return personal dashboard with membered projects', async () => { - // create project A with user 1 - // create project B with user 1 const { body: user1 } = await loginUser('user1@test.com'); const projectA = await createProject('Project A', user1); await createProject('Project B', user1); - // create project C with user 2 const { body: user2 } = await loginUser('user2@test.com'); const projectC = await createProject('Project C', user2); - // Add user 2 as a member of project A await app.services.projectService.addAccess( projectA.id, - [5], + [5], // member role [], [user2.id], user1,