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 { Logger, LogProvider } from '../logger'; | ||||||
| 
 | 
 | ||||||
| import NotFoundError from '../error/notfound-error'; | import NotFoundError from '../error/notfound-error'; | ||||||
| import { IEnvironmentOverview, IFeatureOverview } from '../types/model'; |  | ||||||
| import { | import { | ||||||
|  |     IEnvironmentOverview, | ||||||
|  |     IFeatureOverview, | ||||||
|     IProject, |     IProject, | ||||||
|  | } from '../types/model'; | ||||||
|  | import { | ||||||
|     IProjectHealthUpdate, |     IProjectHealthUpdate, | ||||||
|     IProjectInsert, |     IProjectInsert, | ||||||
|     IProjectStore, |     IProjectStore, | ||||||
|  | |||||||
| @ -17,7 +17,7 @@ import { IContextField } from '../../types/stores/context-field-store'; | |||||||
| import { IFeatureType } from '../../types/stores/feature-type-store'; | import { IFeatureType } from '../../types/stores/feature-type-store'; | ||||||
| import { ITagType } from '../../types/stores/tag-type-store'; | import { ITagType } from '../../types/stores/tag-type-store'; | ||||||
| import { IStrategy } from '../../types/stores/strategy-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'; | import { IUserPermission } from '../../types/stores/access-store'; | ||||||
| 
 | 
 | ||||||
| class BootstrapController extends Controller { | class BootstrapController extends Controller { | ||||||
|  | |||||||
| @ -38,7 +38,6 @@ export const createServices = ( | |||||||
|     const emailService = new EmailService(config.email, config.getLogger); |     const emailService = new EmailService(config.email, config.getLogger); | ||||||
|     const eventService = new EventService(stores, config); |     const eventService = new EventService(stores, config); | ||||||
|     const featureTypeService = new FeatureTypeService(stores, config); |     const featureTypeService = new FeatureTypeService(stores, config); | ||||||
|     const projectService = new ProjectService(stores, config, accessService); |  | ||||||
|     const resetTokenService = new ResetTokenService(stores, config); |     const resetTokenService = new ResetTokenService(stores, config); | ||||||
|     const stateService = new StateService(stores, config); |     const stateService = new StateService(stores, config); | ||||||
|     const strategyService = new StrategyService(stores, config); |     const strategyService = new StrategyService(stores, config); | ||||||
| @ -60,6 +59,12 @@ export const createServices = ( | |||||||
|     const environmentService = new EnvironmentService(stores, config); |     const environmentService = new EnvironmentService(stores, config); | ||||||
|     const featureTagService = new FeatureTagService(stores, config); |     const featureTagService = new FeatureTagService(stores, config); | ||||||
|     const projectHealthService = new ProjectHealthService(stores, config); |     const projectHealthService = new ProjectHealthService(stores, config); | ||||||
|  |     const projectService = new ProjectService( | ||||||
|  |         stores, | ||||||
|  |         config, | ||||||
|  |         accessService, | ||||||
|  |         featureToggleServiceV2, | ||||||
|  |     ); | ||||||
| 
 | 
 | ||||||
|     return { |     return { | ||||||
|         accessService, |         accessService, | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ import { Logger } from '../logger'; | |||||||
| import { | import { | ||||||
|     FeatureToggle, |     FeatureToggle, | ||||||
|     IFeatureOverview, |     IFeatureOverview, | ||||||
|  |     IProject, | ||||||
|     IProjectHealthReport, |     IProjectHealthReport, | ||||||
|     IProjectOverview, |     IProjectOverview, | ||||||
| } from '../types/model'; | } from '../types/model'; | ||||||
| @ -13,7 +14,7 @@ import { | |||||||
| } from '../util/constants'; | } from '../util/constants'; | ||||||
| import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; | import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; | ||||||
| import { IFeatureTypeStore } from '../types/stores/feature-type-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; | import Timer = NodeJS.Timer; | ||||||
| 
 | 
 | ||||||
| export default class ProjectHealthService { | export default class ProjectHealthService { | ||||||
|  | |||||||
| @ -12,14 +12,21 @@ import { | |||||||
| } from '../types/events'; | } from '../types/events'; | ||||||
| import { IUnleashStores } from '../types/stores'; | import { IUnleashStores } from '../types/stores'; | ||||||
| import { IUnleashConfig } from '../types/option'; | 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 { GLOBAL_ENV } from '../types/environment'; | ||||||
| import { IEnvironmentStore } from '../types/stores/environment-store'; | import { IEnvironmentStore } from '../types/stores/environment-store'; | ||||||
| import { IFeatureTypeStore } from '../types/stores/feature-type-store'; | import { IFeatureTypeStore } from '../types/stores/feature-type-store'; | ||||||
| import { IFeatureToggleStore } from '../types/stores/feature-toggle-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 { IRole } from '../types/stores/access-store'; | ||||||
| import { IEventStore } from '../types/stores/event-store'; | import { IEventStore } from '../types/stores/event-store'; | ||||||
|  | import FeatureToggleServiceV2 from './feature-toggle-service-v2'; | ||||||
| 
 | 
 | ||||||
| const getCreatedBy = (user: User) => user.email || user.username; | const getCreatedBy = (user: User) => user.email || user.username; | ||||||
| 
 | 
 | ||||||
| @ -31,7 +38,7 @@ export interface UsersWithRoles { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class ProjectService { | export default class ProjectService { | ||||||
|     private projectStore: IProjectStore; |     private store: IProjectStore; | ||||||
| 
 | 
 | ||||||
|     private accessService: AccessService; |     private accessService: AccessService; | ||||||
| 
 | 
 | ||||||
| @ -45,6 +52,8 @@ export default class ProjectService { | |||||||
| 
 | 
 | ||||||
|     private logger: any; |     private logger: any; | ||||||
| 
 | 
 | ||||||
|  |     private featureToggleService: FeatureToggleServiceV2; | ||||||
|  | 
 | ||||||
|     constructor( |     constructor( | ||||||
|         { |         { | ||||||
|             projectStore, |             projectStore, | ||||||
| @ -62,29 +71,48 @@ export default class ProjectService { | |||||||
|         >, |         >, | ||||||
|         config: IUnleashConfig, |         config: IUnleashConfig, | ||||||
|         accessService: AccessService, |         accessService: AccessService, | ||||||
|  |         featureToggleService: FeatureToggleServiceV2, | ||||||
|     ) { |     ) { | ||||||
|         this.projectStore = projectStore; |         this.store = projectStore; | ||||||
|         this.environmentStore = environmentStore; |         this.environmentStore = environmentStore; | ||||||
|         this.accessService = accessService; |         this.accessService = accessService; | ||||||
|         this.eventStore = eventStore; |         this.eventStore = eventStore; | ||||||
|         this.featureToggleStore = featureToggleStore; |         this.featureToggleStore = featureToggleStore; | ||||||
|         this.featureTypeStore = featureTypeStore; |         this.featureTypeStore = featureTypeStore; | ||||||
|  |         this.featureToggleService = featureToggleService; | ||||||
|         this.logger = config.getLogger('services/project-service.js'); |         this.logger = config.getLogger('services/project-service.js'); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getProjects(): Promise<IProject[]> { |     async getProjects(): Promise<IProjectWithCount[]> { | ||||||
|         return this.projectStore.getAll(); |         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> { |     async getProject(id: string): Promise<IProject> { | ||||||
|         return this.projectStore.get(id); |         return this.store.get(id); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async createProject(newProject: IProject, user: User): Promise<IProject> { |     async createProject(newProject: IProject, user: User): Promise<IProject> { | ||||||
|         const data = await schema.validateAsync(newProject); |         const data = await schema.validateAsync(newProject); | ||||||
|         await this.validateUniqueId(data.id); |         await this.validateUniqueId(data.id); | ||||||
| 
 | 
 | ||||||
|         await this.projectStore.create(data); |         await this.store.create(data); | ||||||
| 
 | 
 | ||||||
|         await this.environmentStore.connectProject(GLOBAL_ENV, data.id); |         await this.environmentStore.connectProject(GLOBAL_ENV, data.id); | ||||||
| 
 | 
 | ||||||
| @ -100,10 +128,10 @@ export default class ProjectService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async updateProject(updatedProject: IProject, user: User): Promise<void> { |     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); |         const project = await schema.validateAsync(updatedProject); | ||||||
| 
 | 
 | ||||||
|         await this.projectStore.update(project); |         await this.store.update(project); | ||||||
| 
 | 
 | ||||||
|         await this.eventStore.store({ |         await this.eventStore.store({ | ||||||
|             type: PROJECT_UPDATED, |             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({ |         await this.eventStore.store({ | ||||||
|             type: PROJECT_DELETED, |             type: PROJECT_DELETED, | ||||||
| @ -148,7 +176,7 @@ export default class ProjectService { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async validateUniqueId(id: string): Promise<void> { |     async validateUniqueId(id: string): Promise<void> { | ||||||
|         const exists = await this.projectStore.hasProject(id); |         const exists = await this.store.hasProject(id); | ||||||
|         if (exists) { |         if (exists) { | ||||||
|             throw new NameExistsError('A project with this id already 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> { |     async getMembers(projectId: string): Promise<number> { | ||||||
|         return this.projectStore.getMembers(projectId); |         return this.store.getMembers(projectId); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     async getProjectOverview( |     async getProjectOverview( | ||||||
|         projectId: string, |         projectId: string, | ||||||
|         archived: boolean = false, |         archived: boolean = false, | ||||||
|     ): Promise<IProjectOverview> { |     ): Promise<IProjectOverview> { | ||||||
|         const project = await this.projectStore.get(projectId); |         const project = await this.store.get(projectId); | ||||||
|         const features = await this.projectStore.getProjectOverview( |         const features = await this.store.getProjectOverview( | ||||||
|             projectId, |             projectId, | ||||||
|             archived, |             archived, | ||||||
|         ); |         ); | ||||||
|         const members = await this.projectStore.getMembers(projectId); |         const members = await this.store.getMembers(projectId); | ||||||
|         return { |         return { | ||||||
|             name: project.name, |             name: project.name, | ||||||
|             description: project.description, |             description: project.description, | ||||||
|  | |||||||
| @ -25,6 +25,7 @@ import { | |||||||
|     IFeatureStrategy, |     IFeatureStrategy, | ||||||
|     ITag, |     ITag, | ||||||
|     IImportData, |     IImportData, | ||||||
|  |     IProject, | ||||||
| } from '../types/model'; | } from '../types/model'; | ||||||
| import { GLOBAL_ENV } from '../types/environment'; | import { GLOBAL_ENV } from '../types/environment'; | ||||||
| import { Logger } from '../logger'; | import { Logger } from '../logger'; | ||||||
| @ -32,7 +33,7 @@ import { | |||||||
|     IFeatureTag, |     IFeatureTag, | ||||||
|     IFeatureTagStore, |     IFeatureTagStore, | ||||||
| } from '../types/stores/feature-tag-store'; | } 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 { ITagType, ITagTypeStore } from '../types/stores/tag-type-store'; | ||||||
| import { ITagStore } from '../types/stores/tag-store'; | import { ITagStore } from '../types/stores/tag-store'; | ||||||
| import { IEventStore } from '../types/stores/event-store'; | import { IEventStore } from '../types/stores/event-store'; | ||||||
|  | |||||||
| @ -285,3 +285,16 @@ interface ImportCommon { | |||||||
| export interface IImportData extends ImportCommon { | export interface IImportData extends ImportCommon { | ||||||
|     data: any; |     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'; | import { Store } from './store'; | ||||||
| 
 | 
 | ||||||
| export interface IProject { |  | ||||||
|     id: string; |  | ||||||
|     name: string; |  | ||||||
|     description: string; |  | ||||||
|     health: number; |  | ||||||
|     createdAt: Date; |  | ||||||
| } |  | ||||||
| export interface IProjectInsert { | export interface IProjectInsert { | ||||||
|     id: string; |     id: string; | ||||||
|     name: string; |     name: string; | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import dbInit from '../helpers/database-init'; | import dbInit from '../helpers/database-init'; | ||||||
| import getLogger from '../../fixtures/no-logger'; | import getLogger from '../../fixtures/no-logger'; | ||||||
|  | import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; | ||||||
| import { AccessService } from '../../../lib/services/access-service'; | import { AccessService } from '../../../lib/services/access-service'; | ||||||
| import ProjectService from '../../../lib/services/project-service'; | import ProjectService from '../../../lib/services/project-service'; | ||||||
| import ProjectHealthService from '../../../lib/services/project-health-service'; | import ProjectHealthService from '../../../lib/services/project-health-service'; | ||||||
| @ -10,6 +11,7 @@ let db; | |||||||
| let projectService; | let projectService; | ||||||
| let accessService; | let accessService; | ||||||
| let projectHealthService; | let projectHealthService; | ||||||
|  | let featureToggleService; | ||||||
| let user; | let user; | ||||||
| 
 | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
| @ -21,7 +23,13 @@ beforeAll(async () => { | |||||||
|         email: 'test@getunleash.io', |         email: 'test@getunleash.io', | ||||||
|     }); |     }); | ||||||
|     accessService = new AccessService(stores, config); |     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); |     projectHealthService = new ProjectHealthService(stores, config); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,5 +1,6 @@ | |||||||
| import dbInit from '../helpers/database-init'; | import dbInit from '../helpers/database-init'; | ||||||
| import getLogger from '../../fixtures/no-logger'; | import getLogger from '../../fixtures/no-logger'; | ||||||
|  | import FeatureToggleServiceV2 from '../../../lib/services/feature-toggle-service-v2'; | ||||||
| import ProjectService from '../../../lib/services/project-service'; | import ProjectService from '../../../lib/services/project-service'; | ||||||
| import { AccessService } from '../../../lib/services/access-service'; | import { AccessService } from '../../../lib/services/access-service'; | ||||||
| import { UPDATE_PROJECT } from '../../../lib/types/permissions'; | import { UPDATE_PROJECT } from '../../../lib/types/permissions'; | ||||||
| @ -12,6 +13,7 @@ let db; | |||||||
| 
 | 
 | ||||||
| let projectService; | let projectService; | ||||||
| let accessService; | let accessService; | ||||||
|  | let featureToggleService; | ||||||
| let user; | let user; | ||||||
| 
 | 
 | ||||||
| beforeAll(async () => { | beforeAll(async () => { | ||||||
| @ -27,7 +29,13 @@ beforeAll(async () => { | |||||||
|         experimental: { rbac: true }, |         experimental: { rbac: true }, | ||||||
|     }); |     }); | ||||||
|     accessService = new AccessService(stores, config); |     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 () => { | afterAll(async () => { | ||||||
| @ -50,6 +58,7 @@ test('should list all projects', async () => { | |||||||
|     await projectService.createProject(project, user); |     await projectService.createProject(project, user); | ||||||
|     const projects = await projectService.getProjects(); |     const projects = await projectService.getProjects(); | ||||||
|     expect(projects).toHaveLength(2); |     expect(projects).toHaveLength(2); | ||||||
|  |     expect(projects.find((p) => p.name === project.name)?.memberCount).toBe(1); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test('should create new project', async () => { | 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 { | import { | ||||||
|     IProject, |  | ||||||
|     IProjectHealthUpdate, |     IProjectHealthUpdate, | ||||||
|     IProjectInsert, |     IProjectInsert, | ||||||
|     IProjectStore, |     IProjectStore, | ||||||
| } from '../../lib/types/stores/project-store'; | } 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'; | import NotFoundError from '../../lib/error/notfound-error'; | ||||||
| 
 | 
 | ||||||
| export default class FakeProjectStore implements IProjectStore { | export default class FakeProjectStore implements IProjectStore { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user