1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-14 00:19:16 +01:00

refactor/clean-up-get-project (#2925)

This PR moves the getProjectOverview method out from the project health
controller. It doesn't make sense that this method lives here anymore,
as over time it has grown into method that relays all information about
a single project. It makes more sense that this now lives on the root of
the project api. Also removes unwanted duplication of getProjectOverview
from the project-service and the project-health-service.
This commit is contained in:
Fredrik Strand Oseberg 2023-01-18 13:22:58 +01:00 committed by GitHub
parent d63b3c69fe
commit 89163b8719
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 87 additions and 101 deletions

View File

@ -4,20 +4,15 @@ import { IUnleashServices } from '../../../types/services';
import { IUnleashConfig } from '../../../types/option'; import { IUnleashConfig } from '../../../types/option';
import ProjectHealthService from '../../../services/project-health-service'; import ProjectHealthService from '../../../services/project-health-service';
import { Logger } from '../../../logger'; import { Logger } from '../../../logger';
import { IArchivedQuery, IProjectParam } from '../../../types/model'; import { IProjectParam } from '../../../types/model';
import { NONE } from '../../../types/permissions'; import { NONE } from '../../../types/permissions';
import { OpenApiService } from '../../../services/openapi-service'; import { OpenApiService } from '../../../services/openapi-service';
import { createResponseSchema } from '../../../openapi/util/create-response-schema'; import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import {
healthOverviewSchema,
HealthOverviewSchema,
} from '../../../openapi/spec/health-overview-schema';
import { serializeDates } from '../../../types/serialize-dates'; import { serializeDates } from '../../../types/serialize-dates';
import { import {
healthReportSchema, healthReportSchema,
HealthReportSchema, HealthReportSchema,
} from '../../../openapi/spec/health-report-schema'; } from '../../../openapi/spec/health-report-schema';
import { IAuthRequest } from '../../unleash-types';
export default class ProjectHealthReport extends Controller { export default class ProjectHealthReport extends Controller {
private projectHealthService: ProjectHealthService; private projectHealthService: ProjectHealthService;
@ -38,22 +33,6 @@ export default class ProjectHealthReport extends Controller {
this.projectHealthService = projectHealthService; this.projectHealthService = projectHealthService;
this.openApiService = openApiService; this.openApiService = openApiService;
this.route({
method: 'get',
path: '/:projectId',
handler: this.getProjectHealthOverview,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjectHealthOverview',
responses: {
200: createResponseSchema('healthOverviewSchema'),
},
}),
],
});
this.route({ this.route({
method: 'get', method: 'get',
path: '/:projectId/health-report', path: '/:projectId/health-report',
@ -71,26 +50,6 @@ export default class ProjectHealthReport extends Controller {
}); });
} }
async getProjectHealthOverview(
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
res: Response<HealthOverviewSchema>,
): Promise<void> {
const { projectId } = req.params;
const { archived } = req.query;
const { user } = req;
const overview = await this.projectHealthService.getProjectOverview(
projectId,
archived,
user.id,
);
this.openApiService.respondWithValidation(
200,
res,
healthOverviewSchema.$id,
serializeDates(overview),
);
}
async getProjectHealthReport( async getProjectHealthReport(
req: Request<IProjectParam>, req: Request<IProjectParam>,
res: Response<HealthReportSchema>, res: Response<HealthReportSchema>,

View File

@ -16,6 +16,11 @@ import { OpenApiService } from '../../../services/openapi-service';
import { serializeDates } from '../../../types/serialize-dates'; import { serializeDates } from '../../../types/serialize-dates';
import { createResponseSchema } from '../../../openapi/util/create-response-schema'; import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { IAuthRequest } from '../../unleash-types'; import { IAuthRequest } from '../../unleash-types';
import {
HealthOverviewSchema,
healthOverviewSchema,
} from '../../../../lib/openapi';
import { IArchivedQuery, IProjectParam } from '../../../types/model';
export default class ProjectApi extends Controller { export default class ProjectApi extends Controller {
private projectService: ProjectService; private projectService: ProjectService;
@ -43,6 +48,22 @@ export default class ProjectApi extends Controller {
], ],
}); });
this.route({
method: 'get',
path: '/:projectId',
handler: this.getProjectOverview,
permission: NONE,
middleware: [
services.openApiService.validPath({
tags: ['Projects'],
operationId: 'getProjectOverview',
responses: {
200: createResponseSchema('healthOverviewSchema'),
},
}),
],
});
this.use('/', new ProjectFeaturesController(config, services).router); this.use('/', new ProjectFeaturesController(config, services).router);
this.use('/', new EnvironmentsController(config, services).router); this.use('/', new EnvironmentsController(config, services).router);
this.use('/', new ProjectHealthReport(config, services).router); this.use('/', new ProjectHealthReport(config, services).router);
@ -68,4 +89,24 @@ export default class ProjectApi extends Controller {
{ version: 1, projects: serializeDates(projects) }, { version: 1, projects: serializeDates(projects) },
); );
} }
async getProjectOverview(
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
res: Response<HealthOverviewSchema>,
): Promise<void> {
const { projectId } = req.params;
const { archived } = req.query;
const { user } = req;
const overview = await this.projectService.getProjectOverview(
projectId,
archived,
user.id,
);
this.openApiService.respondWithValidation(
200,
res,
healthOverviewSchema.$id,
serializeDates(overview),
);
}
} }

View File

@ -92,18 +92,18 @@ 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 favoritesService = new FavoritesService(stores, config); const favoritesService = new FavoritesService(stores, config);
const projectHealthService = new ProjectHealthService(
stores,
config,
featureToggleServiceV2,
favoritesService,
);
const projectService = new ProjectService( const projectService = new ProjectService(
stores, stores,
config, config,
accessService, accessService,
featureToggleServiceV2, featureToggleServiceV2,
groupService, groupService,
favoritesService,
);
const projectHealthService = new ProjectHealthService(
stores,
config,
projectService,
); );
const userSplashService = new UserSplashService(stores, config); const userSplashService = new UserSplashService(stores, config);
const openApiService = new OpenApiService(config); const openApiService = new OpenApiService(config);

View File

@ -6,15 +6,13 @@ import {
IFeatureOverview, IFeatureOverview,
IProject, IProject,
IProjectHealthReport, IProjectHealthReport,
IProjectOverview,
} from '../types/model'; } from '../types/model';
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 { IProjectStore } from '../types/stores/project-store'; import { IProjectStore } from '../types/stores/project-store';
import FeatureToggleService from './feature-toggle-service';
import { hoursToMilliseconds } from 'date-fns'; import { hoursToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer; import Timer = NodeJS.Timer;
import { FavoritesService } from './favorites-service'; import ProjectService from './project-service';
export default class ProjectHealthService { export default class ProjectHealthService {
private logger: Logger; private logger: Logger;
@ -29,9 +27,7 @@ export default class ProjectHealthService {
private healthRatingTimer: Timer; private healthRatingTimer: Timer;
private featureToggleService: FeatureToggleService; private projectService: ProjectService;
private favoritesService: FavoritesService;
constructor( constructor(
{ {
@ -43,8 +39,7 @@ export default class ProjectHealthService {
'projectStore' | 'featureTypeStore' | 'featureToggleStore' 'projectStore' | 'featureTypeStore' | 'featureToggleStore'
>, >,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>, { getLogger }: Pick<IUnleashConfig, 'getLogger'>,
featureToggleService: FeatureToggleService, projectService: ProjectService,
favoritesService: FavoritesService,
) { ) {
this.logger = getLogger('services/project-health-service.ts'); this.logger = getLogger('services/project-health-service.ts');
this.projectStore = projectStore; this.projectStore = projectStore;
@ -55,50 +50,14 @@ export default class ProjectHealthService {
() => this.setHealthRating(), () => this.setHealthRating(),
hoursToMilliseconds(1), hoursToMilliseconds(1),
).unref(); ).unref();
this.featureToggleService = featureToggleService;
this.favoritesService = favoritesService;
}
// TODO: duplicate from project-service. this.projectService = projectService;
async getProjectOverview(
projectId: string,
archived: boolean = false,
userId?: number,
): Promise<IProjectOverview> {
const project = await this.projectStore.get(projectId);
const environments = await this.projectStore.getEnvironmentsForProject(
projectId,
);
const features = await this.featureToggleService.getFeatureOverview({
projectId,
archived,
userId,
});
const members = await this.projectStore.getMembersCountByProject(
projectId,
);
const favorite = await this.favoritesService.isFavoriteProject({
project: projectId,
userId,
});
return {
name: project.name,
description: project.description,
health: project.health,
favorite: favorite,
updatedAt: project.updatedAt,
environments,
features,
members,
version: 1,
};
} }
async getProjectHealthReport( async getProjectHealthReport(
projectId: string, projectId: string,
): Promise<IProjectHealthReport> { ): Promise<IProjectHealthReport> {
const overview = await this.getProjectOverview( const overview = await this.projectService.getProjectOverview(
projectId, projectId,
false, false,
undefined, undefined,

View File

@ -46,6 +46,7 @@ import { IUserStore } from 'lib/types/stores/user-store';
import { arraysHaveSameItems } from '../util/arraysHaveSameItems'; import { arraysHaveSameItems } from '../util/arraysHaveSameItems';
import { GroupService } from './group-service'; import { GroupService } from './group-service';
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group'; import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
import { FavoritesService } from './favorites-service';
const getCreatedBy = (user: IUser) => user.email || user.username; const getCreatedBy = (user: IUser) => user.email || user.username;
@ -80,6 +81,8 @@ export default class ProjectService {
private userStore: IUserStore; private userStore: IUserStore;
private favoritesService: FavoritesService;
constructor( constructor(
{ {
projectStore, projectStore,
@ -105,6 +108,7 @@ export default class ProjectService {
accessService: AccessService, accessService: AccessService,
featureToggleService: FeatureToggleService, featureToggleService: FeatureToggleService,
groupService: GroupService, groupService: GroupService,
favoriteService: FavoritesService,
) { ) {
this.store = projectStore; this.store = projectStore;
this.environmentStore = environmentStore; this.environmentStore = environmentStore;
@ -114,6 +118,7 @@ export default class ProjectService {
this.featureToggleStore = featureToggleStore; this.featureToggleStore = featureToggleStore;
this.featureTypeStore = featureTypeStore; this.featureTypeStore = featureTypeStore;
this.featureToggleService = featureToggleService; this.featureToggleService = featureToggleService;
this.favoritesService = favoriteService;
this.tagStore = featureTagStore; this.tagStore = featureTagStore;
this.userStore = userStore; this.userStore = userStore;
this.groupService = groupService; this.groupService = groupService;
@ -590,6 +595,7 @@ export default class ProjectService {
async getProjectOverview( async getProjectOverview(
projectId: string, projectId: string,
archived: boolean = false, archived: boolean = false,
userId?: number,
): Promise<IProjectOverview> { ): Promise<IProjectOverview> {
const project = await this.store.get(projectId); const project = await this.store.get(projectId);
const environments = await this.store.getEnvironmentsForProject( const environments = await this.store.getEnvironmentsForProject(
@ -598,13 +604,21 @@ export default class ProjectService {
const features = await this.featureToggleService.getFeatureOverview({ const features = await this.featureToggleService.getFeatureOverview({
projectId, projectId,
archived, archived,
userId,
}); });
const members = await this.store.getMembersCountByProject(projectId); const members = await this.store.getMembersCountByProject(projectId);
const favorite = await this.favoritesService.isFavoriteProject({
project: projectId,
userId,
});
return { return {
name: project.name, name: project.name,
environments,
description: project.description, description: project.description,
health: project.health, health: project.health,
favorite: favorite,
updatedAt: project.updatedAt,
environments,
features, features,
members, members,
version: 1, version: 1,

View File

@ -23,7 +23,7 @@ test('should require authenticated user', async () => {
const preHook = (app) => { const preHook = (app) => {
app.use('/api/admin/', (req, res) => app.use('/api/admin/', (req, res) =>
res res
.status('401') .status(401)
.json( .json(
new AuthenticationRequired({ new AuthenticationRequired({
path: '/auth/demo/login', path: '/auth/demo/login',

View File

@ -5291,7 +5291,7 @@ If the provided project does not exist, the list of events will be empty.",
}, },
"/api/admin/projects/{projectId}": { "/api/admin/projects/{projectId}": {
"get": { "get": {
"operationId": "getProjectHealthOverview", "operationId": "getProjectOverview",
"parameters": [ "parameters": [
{ {
"in": "path", "in": "path",

View File

@ -14,12 +14,14 @@ import { DEFAULT_PROJECT } from '../../../lib/types/project';
import { ALL_PROJECTS } from '../../../lib/util/constants'; import { ALL_PROJECTS } from '../../../lib/util/constants';
import { SegmentService } from '../../../lib/services/segment-service'; import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service'; import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
let db: ITestDb; let db: ITestDb;
let stores: IUnleashStores; let stores: IUnleashStores;
let accessService; let accessService;
let groupService; let groupService;
let featureToggleService; let featureToggleService;
let favoritesService;
let projectService; let projectService;
let editorUser; let editorUser;
let superUser; let superUser;
@ -220,12 +222,14 @@ beforeAll(async () => {
new SegmentService(stores, config), new SegmentService(stores, config),
accessService, accessService,
); );
favoritesService = new FavoritesService(stores, config);
projectService = new ProjectService( projectService = new ProjectService(
stores, stores,
config, config,
accessService, accessService,
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService,
); );
editorUser = await createUserEditorAccess('Bob Test', 'bob@getunleash.io'); editorUser = await createUserEditorAccess('Bob Test', 'bob@getunleash.io');

View File

@ -10,11 +10,13 @@ import FeatureToggleService from '../../../lib/services/feature-toggle-service';
import { AccessService } from '../../../lib/services/access-service'; import { AccessService } from '../../../lib/services/access-service';
import { SegmentService } from '../../../lib/services/segment-service'; import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service'; import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
let db; let db;
let stores; let stores;
let apiTokenService: ApiTokenService; let apiTokenService: ApiTokenService;
let projectService: ProjectService; let projectService: ProjectService;
let favoritesService: FavoritesService;
beforeAll(async () => { beforeAll(async () => {
const config = createTestConfig({ const config = createTestConfig({
@ -39,13 +41,14 @@ beforeAll(async () => {
name: 'Some Name', name: 'Some Name',
email: 'test@getunleash.io', email: 'test@getunleash.io',
}); });
favoritesService = new FavoritesService(stores, config);
projectService = new ProjectService( projectService = new ProjectService(
stores, stores,
config, config,
accessService, accessService,
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService,
); );
await projectService.createProject(project, user); await projectService.createProject(project, user);

View File

@ -37,19 +37,20 @@ beforeAll(async () => {
new SegmentService(stores, config), new SegmentService(stores, config),
accessService, accessService,
); );
favoritesService = new FavoritesService(stores, config);
projectService = new ProjectService( projectService = new ProjectService(
stores, stores,
config, config,
accessService, accessService,
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService,
); );
favoritesService = new FavoritesService(stores, config);
projectHealthService = new ProjectHealthService( projectHealthService = new ProjectHealthService(
stores, stores,
config, config,
featureToggleService, projectService,
favoritesService,
); );
}); });

View File

@ -11,6 +11,7 @@ import EnvironmentService from '../../../lib/services/environment-service';
import IncompatibleProjectError from '../../../lib/error/incompatible-project-error'; import IncompatibleProjectError from '../../../lib/error/incompatible-project-error';
import { SegmentService } from '../../../lib/services/segment-service'; import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service'; import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
let stores; let stores;
let db: ITestDb; let db: ITestDb;
@ -20,6 +21,7 @@ let groupService: GroupService;
let accessService: AccessService; let accessService: AccessService;
let environmentService: EnvironmentService; let environmentService: EnvironmentService;
let featureToggleService: FeatureToggleService; let featureToggleService: FeatureToggleService;
let favoritesService: FavoritesService;
let user; let user;
beforeAll(async () => { beforeAll(async () => {
@ -42,6 +44,8 @@ beforeAll(async () => {
new SegmentService(stores, config), new SegmentService(stores, config),
accessService, accessService,
); );
favoritesService = new FavoritesService(stores, config);
environmentService = new EnvironmentService(stores, config); environmentService = new EnvironmentService(stores, config);
projectService = new ProjectService( projectService = new ProjectService(
stores, stores,
@ -49,6 +53,7 @@ beforeAll(async () => {
accessService, accessService,
featureToggleService, featureToggleService,
groupService, groupService,
favoritesService,
); );
}); });