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:
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