mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	fix: add member and toggle count to project list (#918)
This commit is contained in:
		
							parent
							
								
									d7011dacf4
								
							
						
					
					
						commit
						d3fbaa6587
					
				| @ -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, | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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, | ||||
|  | ||||
| @ -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 { | ||||
|  | ||||
| @ -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<IProject[]> { | ||||
|         return this.projectStore.getAll(); | ||||
|     async getProjects(): Promise<IProjectWithCount[]> { | ||||
|         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<IProject> { | ||||
|         return this.projectStore.get(id); | ||||
|         return this.store.get(id); | ||||
|     } | ||||
| 
 | ||||
|     async createProject(newProject: IProject, user: User): Promise<IProject> { | ||||
|         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<void> { | ||||
|         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<void> { | ||||
|         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<number> { | ||||
|         return this.projectStore.getMembers(projectId); | ||||
|         return this.store.getMembers(projectId); | ||||
|     } | ||||
| 
 | ||||
|     async getProjectOverview( | ||||
|         projectId: string, | ||||
|         archived: boolean = false, | ||||
|     ): Promise<IProjectOverview> { | ||||
|         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, | ||||
|  | ||||
| @ -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'; | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -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 () => { | ||||
|  | ||||
							
								
								
									
										3
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								src/test/fixtures/fake-project-store.ts
									
									
									
									
										vendored
									
									
								
							| @ -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 { | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user