1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

fix: add member and toggle count to project list (#918)

This commit is contained in:
Ivar Conradi Østhus 2021-08-19 13:25:36 +02:00 committed by GitHub
parent d7011dacf4
commit d3fbaa6587
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 93 additions and 33 deletions

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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 {

View File

@ -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,

View File

@ -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';

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
}); });

View File

@ -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 () => {

View File

@ -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 {