diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index b9a3cb00b9..4811d1bf5d 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -29,6 +29,11 @@ export interface IEnvironmentProjectLink { projectId: string; } +export interface IProjectMembersCount { + count: number; + project: string; +} + class ProjectStore implements IProjectStore { private db: Knex; @@ -89,12 +94,11 @@ class ProjectStore implements IProjectStore { ); projectTimer(); const memberTimer = this.timer('getMemberCount'); - const memberCount = await this.db.raw( - `SELECT count(role_id) as member_count, project FROM role_user GROUP BY project`, - ); + + const memberCount = await this.getMembersCount(); memberTimer(); const memberMap = new Map( - memberCount.rows.map((c) => [c.project, Number(c.member_count)]), + memberCount.map((c) => [c.project, Number(c.count)]), ); return projectsWithFeatureCount.map((r) => { return { ...r, memberCount: memberMap.get(r.id) }; @@ -247,23 +251,75 @@ class ProjectStore implements IProjectStore { .pluck('project_environments.environment_name'); } - async getMembers(projectId: string): Promise { - const rolesFromProject = this.db('role_permission') - .select('role_id') - .distinct(); - - const numbers = await this.db('role_user') - .countDistinct('user_id as members') - .where('project', projectId) - .whereIn('role_id', rolesFromProject) - .first(); - const { members } = numbers; - if (typeof members === 'string') { - return parseInt(members, 10); - } + async getMembersCount(): Promise { + const members = await this.db + .select('project') + .from((db) => { + db.select('user_id', 'project') + .from('role_user') + .leftJoin('roles', 'role_user.role_id', 'roles.id') + .where((builder) => builder.whereNot('type', 'root')) + .union((queryBuilder) => { + queryBuilder + .select('user_id', 'project') + .from('group_role') + .leftJoin( + 'group_user', + 'group_user.group_id', + 'group_role.group_id', + ); + }) + .union((queryBuilder) => { + queryBuilder + .select('user_id', 'projects.name as project') + .from('role_user') + .leftJoin('roles', 'role_user.role_id', 'roles.id') + .crossJoin('projects') + .where((builder) => + builder + .where('type', 'root') + .where('roles.name', 'Editor'), + ); + }) + .as('query'); + }) + .groupBy('project') + .count('user_id'); return members; } + async getMembersCountByProject(projectId?: string): Promise { + const members = await this.db + .from((db) => { + db.select('user_id') + .from('role_user') + .leftJoin('roles', 'role_user.role_id', 'roles.id') + .where((builder) => + builder + .where('project', projectId) + .whereNot('type', 'root'), + ) + .orWhere((builder) => + builder.where('type', 'root').where('name', 'Editor'), + ) + .union((queryBuilder) => { + queryBuilder + .select('user_id') + .from('group_role') + .leftJoin( + 'group_user', + 'group_user.group_id', + 'group_role.group_id', + ) + .where('project', projectId); + }) + .as('query'); + }) + .count() + .first(); + return Number(members.count); + } + async count(): Promise { return this.db .from(TABLE) diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index 0eabecb2d9..df5d371305 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -67,7 +67,9 @@ export default class ProjectHealthService { projectId, archived, ); - const members = await this.projectStore.getMembers(projectId); + const members = await this.projectStore.getMembersCountByProject( + projectId, + ); return { name: project.name, description: project.description, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 14ea9e8487..35fd9e29e9 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -573,7 +573,7 @@ export default class ProjectService { } async getMembers(projectId: string): Promise { - return this.store.getMembers(projectId); + return this.store.getMembersCountByProject(projectId); } async getProjectOverview( @@ -588,7 +588,7 @@ export default class ProjectService { projectId, archived, ); - const members = await this.store.getMembers(projectId); + const members = await this.store.getMembersCountByProject(projectId); return { name: project.name, environments, diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index d8263a0959..8cce795f6c 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -1,4 +1,7 @@ -import { IEnvironmentProjectLink } from '../../db/project-store'; +import { + IEnvironmentProjectLink, + IProjectMembersCount, +} from '../../db/project-store'; import { IProject, IProjectWithCount } from '../model'; import { Store } from './store'; @@ -32,7 +35,8 @@ export interface IProjectStore extends Store { addEnvironmentToProject(id: string, environment: string): Promise; deleteEnvironmentForProject(id: string, environment: string): Promise; getEnvironmentsForProject(id: string): Promise; - getMembers(projectId: string): Promise; + getMembersCountByProject(projectId: string): Promise; + getMembersCount(): Promise; getProjectsWithCounts(query?: IProjectQuery): Promise; count(): Promise; getAll(query?: IProjectQuery): Promise; diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 9ae835fbfd..0b9bbabb4f 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -5,7 +5,10 @@ import { } from '../../lib/types/stores/project-store'; import { IProject, IProjectWithCount } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; -import { IEnvironmentProjectLink } from 'lib/db/project-store'; +import { + IEnvironmentProjectLink, + IProjectMembersCount, +} from 'lib/db/project-store'; export default class FakeProjectStore implements IProjectStore { projects: IProject[] = []; @@ -99,7 +102,7 @@ export default class FakeProjectStore implements IProjectStore { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getMembers(projectId: string): Promise { + async getMembersCountByProject(projectId: string): Promise { return Promise.resolve(0); } @@ -120,4 +123,8 @@ export default class FakeProjectStore implements IProjectStore { this.projects.find((p) => p.id === healthUpdate.id).health = healthUpdate.health; } + + getMembersCount(): Promise { + throw new Error('Method not implemented.'); + } }