From 4972b9686c7510a514c9218fafec211298e5335f Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Fri, 9 Feb 2024 13:18:26 +0200 Subject: [PATCH] feat: project applications controller/service layer (#6184) Just adding controller/service layer, connecting with schema. Next PR will implement store and e2e tests. --- .../project/project-controller.ts} | 77 ++++++++++++++----- .../features}/project/projects.e2e.test.ts | 35 ++++++--- src/lib/openapi/spec/index.ts | 2 + src/lib/routes/admin-api/index.ts | 7 +- src/lib/services/project-service.ts | 35 +++++++-- 5 files changed, 122 insertions(+), 34 deletions(-) rename src/lib/{routes/admin-api/project/project-api.ts => features/project/project-controller.ts} (73%) rename src/{test/e2e/api/admin => lib/features}/project/projects.e2e.test.ts (90%) diff --git a/src/lib/routes/admin-api/project/project-api.ts b/src/lib/features/project/project-controller.ts similarity index 73% rename from src/lib/routes/admin-api/project/project-api.ts rename to src/lib/features/project/project-controller.ts index 2525575019..dc0b6b203b 100644 --- a/src/lib/routes/admin-api/project/project-api.ts +++ b/src/lib/features/project/project-controller.ts @@ -1,5 +1,5 @@ import { Response } from 'express'; -import Controller from '../../controller'; +import Controller from '../../routes/controller'; import { IArchivedQuery, IProjectParam, @@ -7,12 +7,12 @@ import { IUnleashServices, NONE, serializeDates, -} from '../../../types'; -import ProjectFeaturesController from '../../../features/feature-toggle/feature-toggle-controller'; -import EnvironmentsController from '../../../features/project-environments/environments'; -import ProjectHealthReport from './health-report'; -import ProjectService from '../../../services/project-service'; -import VariantsController from './variants'; +} from '../../types'; +import ProjectFeaturesController from '../feature-toggle/feature-toggle-controller'; +import EnvironmentsController from '../project-environments/environments'; +import ProjectHealthReport from '../../routes/admin-api/project/health-report'; +import ProjectService from '../../services/project-service'; +import VariantsController from '../../routes/admin-api/project/variants'; import { createResponseSchema, ProjectDoraMetricsSchema, @@ -22,18 +22,22 @@ import { projectsSchema, ProjectsSchema, projectOverviewSchema, -} from '../../../openapi'; -import { getStandardResponses } from '../../../openapi/util/standard-responses'; -import { OpenApiService, SettingService } from '../../../services'; -import { IAuthRequest } from '../../unleash-types'; -import { ProjectApiTokenController } from './api-token'; -import ProjectArchiveController from './project-archive'; -import { createKnexTransactionStarter } from '../../../db/transaction'; -import { Db } from '../../../db/db'; -import DependentFeaturesController from '../../../features/dependent-features/dependent-features-controller'; -import { ProjectOverviewSchema } from '../../../openapi/spec/project-overview-schema'; +} from '../../openapi'; +import { getStandardResponses } from '../../openapi/util/standard-responses'; +import { OpenApiService, SettingService } from '../../services'; +import { IAuthRequest } from '../../routes/unleash-types'; +import { ProjectApiTokenController } from '../../routes/admin-api/project/api-token'; +import ProjectArchiveController from '../../routes/admin-api/project/project-archive'; +import { createKnexTransactionStarter } from '../../db/transaction'; +import { Db } from '../../db/db'; +import DependentFeaturesController from '../dependent-features/dependent-features-controller'; +import { ProjectOverviewSchema } from '../../openapi/spec/project-overview-schema'; +import { + projectApplicationsSchema, + ProjectApplicationsSchema, +} from '../../openapi/spec/project-applications-schema'; -export default class ProjectApi extends Controller { +export default class ProjectController extends Controller { private projectService: ProjectService; private settingService: SettingService; @@ -129,6 +133,26 @@ export default class ProjectApi extends Controller { ], }); + this.route({ + method: 'get', + path: '/:projectId/applications', + handler: this.getProjectApplications, + permission: NONE, + middleware: [ + services.openApiService.validPath({ + tags: ['Unstable'], + operationId: 'getProjectApplications', + summary: 'Get a list of all applications for a project.', + description: + 'This endpoint returns an list of all the applications for a project.', + responses: { + 200: createResponseSchema('projectApplicationsSchema'), + ...getStandardResponses(401, 403, 404), + }, + }), + ], + }); + this.use( '/', new ProjectFeaturesController( @@ -229,4 +253,21 @@ export default class ProjectApi extends Controller { dora, ); } + + async getProjectApplications( + req: IAuthRequest, + res: Response, + ): Promise { + const { projectId } = req.params; + + const applications = + await this.projectService.getApplications(projectId); + + this.openApiService.respondWithValidation( + 200, + res, + projectApplicationsSchema.$id, + applications, + ); + } } diff --git a/src/test/e2e/api/admin/project/projects.e2e.test.ts b/src/lib/features/project/projects.e2e.test.ts similarity index 90% rename from src/test/e2e/api/admin/project/projects.e2e.test.ts rename to src/lib/features/project/projects.e2e.test.ts index 7f68ea1e7a..1903625ea7 100644 --- a/src/test/e2e/api/admin/project/projects.e2e.test.ts +++ b/src/lib/features/project/projects.e2e.test.ts @@ -1,14 +1,14 @@ -import dbInit, { ITestDb } from '../../../helpers/database-init'; +import dbInit, { ITestDb } from '../../../test/e2e/helpers/database-init'; import { IUnleashTest, insertFeatureEnvironmentsLastSeen, insertLastSeenAt, setupAppWithCustomConfig, -} from '../../../helpers/test-helper'; -import getLogger from '../../../../fixtures/no-logger'; +} from '../../../test/e2e/helpers/test-helper'; +import getLogger from '../../../test/fixtures/no-logger'; -import { IProjectStore } from '../../../../../lib/types'; -import { DEFAULT_ENV } from '../../../../../lib/util'; +import { IProjectStore } from '../../types'; +import { DEFAULT_ENV } from '../../util'; let app: IUnleashTest; let db: ITestDb; @@ -143,9 +143,11 @@ test('response for default project should include created_at', async () => { .expect(200); expect(body.createdAt).toBeDefined(); }); - test('response for project overview should include feature type counts', async () => { - await app.createFeature({ name: 'my-new-release-toggle', type: 'release' }); + await app.createFeature({ + name: 'my-new-release-toggle', + type: 'release', + }); await app.createFeature({ name: 'my-new-development-toggle', type: 'development', @@ -156,8 +158,14 @@ test('response for project overview should include feature type counts', async ( .expect(200); expect(body).toMatchObject({ featureTypeCounts: [ - { type: 'development', count: 1 }, - { type: 'release', count: 1 }, + { + type: 'development', + count: 1, + }, + { + type: 'release', + count: 1, + }, ], }); }); @@ -278,3 +286,12 @@ test('response should include last seen at per environment for multiple environm expect(body.features[1].lastSeenAt).toBe('2023-10-01T12:34:56.000Z'); }); + +test('should return empty list of applications', async () => { + const { body } = await app.request + .get('/api/admin/projects/default/applications') + .expect('Content-Type', /json/) + .expect(200); + + expect(body).toMatchObject([]); +}); diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index a80a018cfd..b06d736eb5 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -175,3 +175,5 @@ export * from './feature-type-count-schema'; export * from './feature-search-response-schema'; export * from './inactive-user-schema'; export * from './inactive-users-schema'; +export * from './project-application-schema'; +export * from './project-applications-schema'; diff --git a/src/lib/routes/admin-api/index.ts b/src/lib/routes/admin-api/index.ts index 12fe75f270..8e3b7fea12 100644 --- a/src/lib/routes/admin-api/index.ts +++ b/src/lib/routes/admin-api/index.ts @@ -20,7 +20,7 @@ import UserAdminController from './user-admin'; import EmailController from './email'; import UserFeedbackController from './user-feedback'; import UserSplashController from './user-splash'; -import ProjectApi from './project/project-api'; +import ProjectController from '../../features/project/project-controller'; import { EnvironmentsController } from './environments'; import ConstraintsController from './constraints'; import PatController from './user/pat'; @@ -117,7 +117,10 @@ class AdminApi extends Controller { '/feedback', new UserFeedbackController(config, services).router, ); - this.app.use('/projects', new ProjectApi(config, services, db).router); + this.app.use( + '/projects', + new ProjectController(config, services, db).router, + ); this.app.use( '/environments', new EnvironmentsController(config, services).router, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index c8eb50f5e9..b53babc286 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -63,7 +63,10 @@ import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-p import { IProjectStatsStore } from '../types/stores/project-stats-store-type'; import { uniqueByKey } from '../util/unique'; import { BadDataError, PermissionError } from '../error'; -import { ProjectDoraMetricsSchema } from '../openapi'; +import { + ProjectDoraMetricsSchema, + ProjectApplicationsSchema, +} from '../openapi'; import { checkFeatureNamingData } from '../features/feature-naming-pattern/feature-naming-validation'; import { IPrivateProjectChecker } from '../features/private-project/privateProjectCheckerType'; import EventService from '../features/events/event-service'; @@ -89,7 +92,14 @@ interface ICalculateStatus { updates: IProjectStats; } -function includes(list: number[], { id }: { id: number }): boolean { +function includes( + list: number[], + { + id, + }: { + id: number; + }, +): boolean { return list.some((l) => l === id); } @@ -887,7 +897,16 @@ export default class ProjectService { featureToggleNames, ); - return { features: toggleAverage, projectAverage: projectAverage }; + return { + features: toggleAverage, + projectAverage: projectAverage, + }; + } + + async getApplications( + projectId: string, + ): Promise { + return []; } async changeRole( @@ -1091,7 +1110,10 @@ export default class ProjectService { const [projectActivityCurrentWindow, projectActivityPastWindow] = await Promise.all([ this.eventStore.queryCount([ - { op: 'where', parameters: { project: projectId } }, + { + op: 'where', + parameters: { project: projectId }, + }, { op: 'beforeDate', parameters: { @@ -1101,7 +1123,10 @@ export default class ProjectService { }, ]), this.eventStore.queryCount([ - { op: 'where', parameters: { project: projectId } }, + { + op: 'where', + parameters: { project: projectId }, + }, { op: 'betweenDate', parameters: {