From d3fbaa65874749a0d00c88006dc7eeef7b4dfa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 19 Aug 2021 13:25:36 +0200 Subject: [PATCH] fix: add member and toggle count to project list (#918) --- src/lib/db/project-store.ts | 5 +- .../routes/admin-api/bootstrap-controller.ts | 2 +- src/lib/services/index.ts | 7 ++- src/lib/services/project-health-service.ts | 3 +- src/lib/services/project-service.ts | 60 ++++++++++++++----- src/lib/services/state-service.ts | 3 +- src/lib/types/model.ts | 13 ++++ src/lib/types/stores/project-store.ts | 9 +-- .../project-health-service.e2e.test.ts | 10 +++- .../e2e/services/project-service.e2e.test.ts | 11 +++- src/test/fixtures/fake-project-store.ts | 3 +- 11 files changed, 93 insertions(+), 33 deletions(-) diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 33a77a5793..b596a35cca 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -2,9 +2,12 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; -import { IEnvironmentOverview, IFeatureOverview } from '../types/model'; import { + IEnvironmentOverview, + IFeatureOverview, IProject, +} from '../types/model'; +import { IProjectHealthUpdate, IProjectInsert, IProjectStore, diff --git a/src/lib/routes/admin-api/bootstrap-controller.ts b/src/lib/routes/admin-api/bootstrap-controller.ts index be40fd1b67..0f27d9d31a 100644 --- a/src/lib/routes/admin-api/bootstrap-controller.ts +++ b/src/lib/routes/admin-api/bootstrap-controller.ts @@ -17,7 +17,7 @@ import { IContextField } from '../../types/stores/context-field-store'; import { IFeatureType } from '../../types/stores/feature-type-store'; import { ITagType } from '../../types/stores/tag-type-store'; import { IStrategy } from '../../types/stores/strategy-store'; -import { IProject } from '../../types/stores/project-store'; +import { IProject } from '../../types/model'; import { IUserPermission } from '../../types/stores/access-store'; class BootstrapController extends Controller { diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index d26ff09938..7952074492 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -38,7 +38,6 @@ export const createServices = ( const emailService = new EmailService(config.email, config.getLogger); const eventService = new EventService(stores, config); const featureTypeService = new FeatureTypeService(stores, config); - const projectService = new ProjectService(stores, config, accessService); const resetTokenService = new ResetTokenService(stores, config); const stateService = new StateService(stores, config); const strategyService = new StrategyService(stores, config); @@ -60,6 +59,12 @@ export const createServices = ( const environmentService = new EnvironmentService(stores, config); const featureTagService = new FeatureTagService(stores, config); const projectHealthService = new ProjectHealthService(stores, config); + const projectService = new ProjectService( + stores, + config, + accessService, + featureToggleServiceV2, + ); return { accessService, diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index 5f63eb2270..ae2f3b7d7b 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -4,6 +4,7 @@ import { Logger } from '../logger'; import { FeatureToggle, IFeatureOverview, + IProject, IProjectHealthReport, IProjectOverview, } from '../types/model'; @@ -13,7 +14,7 @@ import { } from '../util/constants'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store'; -import { IProject, IProjectStore } from '../types/stores/project-store'; +import { IProjectStore } from '../types/stores/project-store'; import Timer = NodeJS.Timer; export default class ProjectHealthService { diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 5d4626c57b..469947a414 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -12,14 +12,21 @@ import { } from '../types/events'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; -import { IProjectOverview, IUserWithRole, RoleName } from '../types/model'; +import { + IProject, + IProjectOverview, + IProjectWithCount, + IUserWithRole, + RoleName, +} from '../types/model'; import { GLOBAL_ENV } from '../types/environment'; import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; -import { IProject, IProjectStore } from '../types/stores/project-store'; +import { IProjectStore } from '../types/stores/project-store'; import { IRole } from '../types/stores/access-store'; import { IEventStore } from '../types/stores/event-store'; +import FeatureToggleServiceV2 from './feature-toggle-service-v2'; const getCreatedBy = (user: User) => user.email || user.username; @@ -31,7 +38,7 @@ export interface UsersWithRoles { } export default class ProjectService { - private projectStore: IProjectStore; + private store: IProjectStore; private accessService: AccessService; @@ -45,6 +52,8 @@ export default class ProjectService { private logger: any; + private featureToggleService: FeatureToggleServiceV2; + constructor( { projectStore, @@ -62,29 +71,48 @@ export default class ProjectService { >, config: IUnleashConfig, accessService: AccessService, + featureToggleService: FeatureToggleServiceV2, ) { - this.projectStore = projectStore; + this.store = projectStore; this.environmentStore = environmentStore; this.accessService = accessService; this.eventStore = eventStore; this.featureToggleStore = featureToggleStore; this.featureTypeStore = featureTypeStore; + this.featureToggleService = featureToggleService; this.logger = config.getLogger('services/project-service.js'); } - async getProjects(): Promise { - return this.projectStore.getAll(); + async getProjects(): Promise { + const projects = await this.store.getAll(); + const projectsWithCount = await Promise.all( + projects.map(async (p) => { + let featureCount = 0; + let memberCount = 0; + try { + featureCount = + await this.featureToggleService.getFeatureCountForProject( + p.id, + ); + memberCount = await this.getMembers(p.id); + } catch (e) { + this.logger.warn('Error fetching project counts', e); + } + return { ...p, featureCount, memberCount }; + }), + ); + return projectsWithCount; } async getProject(id: string): Promise { - return this.projectStore.get(id); + return this.store.get(id); } async createProject(newProject: IProject, user: User): Promise { const data = await schema.validateAsync(newProject); await this.validateUniqueId(data.id); - await this.projectStore.create(data); + await this.store.create(data); await this.environmentStore.connectProject(GLOBAL_ENV, data.id); @@ -100,10 +128,10 @@ export default class ProjectService { } async updateProject(updatedProject: IProject, user: User): Promise { - await this.projectStore.get(updatedProject.id); + await this.store.get(updatedProject.id); const project = await schema.validateAsync(updatedProject); - await this.projectStore.update(project); + await this.store.update(project); await this.eventStore.store({ type: PROJECT_UPDATED, @@ -130,7 +158,7 @@ export default class ProjectService { ); } - await this.projectStore.delete(id); + await this.store.delete(id); await this.eventStore.store({ type: PROJECT_DELETED, @@ -148,7 +176,7 @@ export default class ProjectService { } async validateUniqueId(id: string): Promise { - const exists = await this.projectStore.hasProject(id); + const exists = await this.store.hasProject(id); if (exists) { throw new NameExistsError('A project with this id already exists.'); } @@ -214,19 +242,19 @@ export default class ProjectService { } async getMembers(projectId: string): Promise { - return this.projectStore.getMembers(projectId); + return this.store.getMembers(projectId); } async getProjectOverview( projectId: string, archived: boolean = false, ): Promise { - const project = await this.projectStore.get(projectId); - const features = await this.projectStore.getProjectOverview( + const project = await this.store.get(projectId); + const features = await this.store.getProjectOverview( projectId, archived, ); - const members = await this.projectStore.getMembers(projectId); + const members = await this.store.getMembers(projectId); return { name: project.name, description: project.description, diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index debab70c42..329e64738a 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -25,6 +25,7 @@ import { IFeatureStrategy, ITag, IImportData, + IProject, } from '../types/model'; import { GLOBAL_ENV } from '../types/environment'; import { Logger } from '../logger'; @@ -32,7 +33,7 @@ import { IFeatureTag, IFeatureTagStore, } from '../types/stores/feature-tag-store'; -import { IProject, IProjectStore } from '../types/stores/project-store'; +import { IProjectStore } from '../types/stores/project-store'; import { ITagType, ITagTypeStore } from '../types/stores/tag-type-store'; import { ITagStore } from '../types/stores/tag-store'; import { IEventStore } from '../types/stores/event-store'; diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index e9a58b1586..99b3d19055 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -285,3 +285,16 @@ interface ImportCommon { export interface IImportData extends ImportCommon { data: any; } + +export interface IProject { + id: string; + name: string; + description: string; + health: number; + createdAt: Date; +} + +export interface IProjectWithCount extends IProject { + featureCount: number; + memberCount: number; +} diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index 5dc205e9b3..49102b1f99 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -1,13 +1,6 @@ -import { IFeatureOverview } from '../model'; +import { IFeatureOverview, IProject } from '../model'; import { Store } from './store'; -export interface IProject { - id: string; - name: string; - description: string; - health: number; - createdAt: Date; -} export interface IProjectInsert { id: string; name: string; diff --git a/src/test/e2e/services/project-health-service.e2e.test.ts b/src/test/e2e/services/project-health-service.e2e.test.ts index ea539d7f24..30b332f663 100644 --- a/src/test/e2e/services/project-health-service.e2e.test.ts +++ b/src/test/e2e/services/project-health-service.e2e.test.ts @@ -1,5 +1,6 @@ import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; +import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import { AccessService } from '../../../lib/services/access-service'; import ProjectService from '../../../lib/services/project-service'; import ProjectHealthService from '../../../lib/services/project-health-service'; @@ -10,6 +11,7 @@ let db; let projectService; let accessService; let projectHealthService; +let featureToggleService; let user; beforeAll(async () => { @@ -21,7 +23,13 @@ beforeAll(async () => { email: 'test@getunleash.io', }); accessService = new AccessService(stores, config); - projectService = new ProjectService(stores, config, accessService); + featureToggleService = new FeatureToggleServiceV2(stores, config); + projectService = new ProjectService( + stores, + config, + accessService, + featureToggleService, + ); projectHealthService = new ProjectHealthService(stores, config); }); diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index d69c5782ca..d26103c879 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -1,5 +1,6 @@ import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; +import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; import ProjectService from '../../../lib/services/project-service'; import { AccessService } from '../../../lib/services/access-service'; import { UPDATE_PROJECT } from '../../../lib/types/permissions'; @@ -12,6 +13,7 @@ let db; let projectService; let accessService; +let featureToggleService; let user; beforeAll(async () => { @@ -27,7 +29,13 @@ beforeAll(async () => { experimental: { rbac: true }, }); accessService = new AccessService(stores, config); - projectService = new ProjectService(stores, config, accessService); + featureToggleService = new FeatureToggleServiceV2(stores, config); + projectService = new ProjectService( + stores, + config, + accessService, + featureToggleService, + ); }); afterAll(async () => { @@ -50,6 +58,7 @@ test('should list all projects', async () => { await projectService.createProject(project, user); const projects = await projectService.getProjects(); expect(projects).toHaveLength(2); + expect(projects.find((p) => p.name === project.name)?.memberCount).toBe(1); }); test('should create new project', async () => { diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index bda4789324..7bfc9b9b8c 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -1,10 +1,9 @@ import { - IProject, IProjectHealthUpdate, IProjectInsert, IProjectStore, } from '../../lib/types/stores/project-store'; -import { IFeatureOverview } from '../../lib/types/model'; +import { IFeatureOverview, IProject } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; export default class FakeProjectStore implements IProjectStore {