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