1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-11 00:08:30 +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 NotFoundError from '../error/notfound-error';
import { IEnvironmentOverview, IFeatureOverview } from '../types/model';
import {
IEnvironmentOverview,
IFeatureOverview,
IProject,
} from '../types/model';
import {
IProjectHealthUpdate,
IProjectInsert,
IProjectStore,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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