diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts new file mode 100644 index 000000000000..00a30582ff94 --- /dev/null +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -0,0 +1,17 @@ +import type { Db, IUnleashConfig } from '../../server-impl'; +import { ProjectStatusService } from './project-status-service'; + +export const createProjectStatusService = ( + db: Db, + config: IUnleashConfig, +): ProjectStatusService => { + return new ProjectStatusService(); +}; + +export const createFakeProjectStatusService = () => { + const projectStatusService = new ProjectStatusService(); + + return { + projectStatusService, + }; +}; diff --git a/src/lib/features/project-status/project-status-controller.ts b/src/lib/features/project-status/project-status-controller.ts new file mode 100644 index 000000000000..3abd8aa0228c --- /dev/null +++ b/src/lib/features/project-status/project-status-controller.ts @@ -0,0 +1,71 @@ +import type { Response } from 'express'; +import Controller from '../../routes/controller'; +import { + type IFlagResolver, + type IProjectParam, + type IUnleashConfig, + type IUnleashServices, + NONE, + serializeDates, +} from '../../types'; + +import { getStandardResponses } from '../../openapi/util/standard-responses'; +import type { OpenApiService } from '../../services'; +import type { IAuthRequest } from '../../routes/unleash-types'; +import { + createResponseSchema, + projectStatusSchema, + type ProjectStatusSchema, +} from '../../openapi'; +import type { ProjectStatusService } from './project-status-service'; + +export default class ProjectStatusController extends Controller { + private projectStatusService: ProjectStatusService; + + private openApiService: OpenApiService; + + private flagResolver: IFlagResolver; + + constructor(config: IUnleashConfig, services: IUnleashServices) { + super(config); + this.projectStatusService = services.projectStatusService; + this.openApiService = services.openApiService; + this.flagResolver = config.flagResolver; + + this.route({ + method: 'get', + path: '/:projectId/status', + handler: this.getProjectStatus, + permission: NONE, + middleware: [ + this.openApiService.validPath({ + tags: ['Projects'], + operationId: 'getProjectStatus', + summary: 'Get project status', + description: + 'This endpoint returns information on the status the project, including activities, health, resources, and aggregated flag lifecycle data.', + responses: { + 200: createResponseSchema('projectStatusSchema'), + ...getStandardResponses(401, 403, 404), + }, + }), + ], + }); + } + + async getProjectStatus( + req: IAuthRequest, + res: Response, + ): Promise { + const { projectId } = req.params; + const status: ProjectStatusSchema = + await this.projectStatusService.getProjectStatus(projectId); + + this.openApiService.respondWithValidation( + 200, + res, + projectStatusSchema.$id, + serializeDates(status), + ); + } +} diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts new file mode 100644 index 000000000000..26a2c444e2d2 --- /dev/null +++ b/src/lib/features/project-status/project-status-service.ts @@ -0,0 +1,9 @@ +import type { ProjectStatusSchema } from '../../openapi'; + +export class ProjectStatusService { + constructor() {} + + async getProjectStatus(projectId: string): Promise { + return { activityCountByDate: [{ date: '2024-09-11', count: 0 }] }; + } +} diff --git a/src/lib/features/project-status/projects-status.e2e.test.ts b/src/lib/features/project-status/projects-status.e2e.test.ts new file mode 100644 index 000000000000..79e4992cfe36 --- /dev/null +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -0,0 +1,40 @@ +import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; +import { + type IUnleashTest, + setupAppWithCustomConfig, +} from '../../../test/e2e/helpers/test-helper'; +import getLogger from '../../../test/fixtures/no-logger'; + +let app: IUnleashTest; +let db: ITestDb; + +beforeAll(async () => { + db = await dbInit('projects_status', getLogger); + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, + }, + }, + db.rawDatabase, + ); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('project insights happy path', async () => { + const { body } = await app.request + .get('/api/admin/projects/default/status') + .expect('Content-Type', /json/) + .expect(200); + + expect(body).toMatchObject({ + activityCountByDate: [{ date: '2024-09-11', count: 0 }], + }); +}); diff --git a/src/lib/features/project/project-controller.ts b/src/lib/features/project/project-controller.ts index d795fc7d40a6..74f085236165 100644 --- a/src/lib/features/project/project-controller.ts +++ b/src/lib/features/project/project-controller.ts @@ -48,6 +48,7 @@ import { projectFlagCreatorsSchema, type ProjectFlagCreatorsSchema, } from '../../openapi/spec/project-flag-creators-schema'; +import ProjectStatusController from '../project-status/project-status-controller'; export default class ProjectController extends Controller { private projectService: ProjectService; @@ -242,6 +243,7 @@ export default class ProjectController extends Controller { ).router, ); this.use('/', new ProjectInsightsController(config, services).router); + this.use('/', new ProjectStatusController(config, services).router); this.use('/', new FeatureLifecycleController(config, services).router); } diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts index aea41ffcbfdb..3aa488d5f414 100644 --- a/src/lib/openapi/spec/index.ts +++ b/src/lib/openapi/spec/index.ts @@ -145,6 +145,7 @@ export * from './playground-response-schema'; export * from './playground-segment-schema'; export * from './playground-strategy-schema'; export * from './profile-schema'; +export * from './project-activity-schema'; export * from './project-application-schema'; export * from './project-application-sdk-schema'; export * from './project-applications-schema'; @@ -158,6 +159,7 @@ export * from './project-insights-schema'; export * from './project-overview-schema'; export * from './project-schema'; export * from './project-stats-schema'; +export * from './project-status-schema'; export * from './projects-schema'; export * from './public-signup-token-create-schema'; export * from './public-signup-token-schema'; diff --git a/src/lib/openapi/spec/project-activity-schema.ts b/src/lib/openapi/spec/project-activity-schema.ts new file mode 100644 index 000000000000..67c1c41abbd8 --- /dev/null +++ b/src/lib/openapi/spec/project-activity-schema.ts @@ -0,0 +1,30 @@ +import type { FromSchema } from 'json-schema-to-ts'; + +export const projectActivitySchema = { + $id: '#/components/schemas/projectActivitySchema', + type: 'array', + description: + 'An array of project activity information. Each item contains a date and the total number of activities for that date.', + items: { + type: 'object', + additionalProperties: false, + required: ['date', 'count'], + properties: { + date: { + type: 'string', + example: '2022-12-14', + description: 'Activity date', + }, + count: { + type: 'integer', + minimum: 0, + description: 'Activity count', + }, + }, + }, + components: { + schemas: {}, + }, +} as const; + +export type ProjectActivitySchema = FromSchema; diff --git a/src/lib/openapi/spec/project-status-schema.test.ts b/src/lib/openapi/spec/project-status-schema.test.ts new file mode 100644 index 000000000000..c3c37a44d13d --- /dev/null +++ b/src/lib/openapi/spec/project-status-schema.test.ts @@ -0,0 +1,15 @@ +import { validateSchema } from '../validate'; +import type { ProjectStatusSchema } from './project-status-schema'; + +test('projectStatusSchema', () => { + const data: ProjectStatusSchema = { + activityCountByDate: [ + { date: '2022-12-14', count: 2 }, + { date: '2022-12-15', count: 5 }, + ], + }; + + expect( + validateSchema('#/components/schemas/projectStatusSchema', data), + ).toBeUndefined(); +}); diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts new file mode 100644 index 000000000000..71085b2da4a3 --- /dev/null +++ b/src/lib/openapi/spec/project-status-schema.ts @@ -0,0 +1,25 @@ +import type { FromSchema } from 'json-schema-to-ts'; +import { projectActivitySchema } from './project-activity-schema'; + +export const projectStatusSchema = { + $id: '#/components/schemas/projectStatusSchema', + type: 'object', + additionalProperties: false, + required: ['activityCountByDate'], + description: + 'Schema representing the overall status of a project, including an array of activity records. Each record in the activity array contains a date and a count, providing a snapshot of the project’s activity level over time.', + properties: { + activityCountByDate: { + $ref: '#/components/schemas/projectActivitySchema', + description: + 'Array of activity records with date and count, representing the project’s daily activity statistics.', + }, + }, + components: { + schemas: { + projectActivitySchema, + }, + }, +} as const; + +export type ProjectStatusSchema = FromSchema; diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index c0c6fa78a944..b624b3070bb3 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -153,6 +153,11 @@ import { createFakePersonalDashboardService, createPersonalDashboardService, } from '../features/personal-dashboard/createPersonalDashboardService'; +import { + createFakeProjectStatusService, + createProjectStatusService, +} from '../features/project-status/createProjectStatusService'; +import { ProjectStatusService } from '../features/project-status/project-status-service'; export const createServices = ( stores: IUnleashStores, @@ -324,6 +329,10 @@ export const createServices = ( ? createProjectInsightsService(db, config) : createFakeProjectInsightsService().projectInsightsService; + const projectStatusService = db + ? createProjectStatusService(db, config) + : createFakeProjectStatusService().projectStatusService; + const projectHealthService = new ProjectHealthService( stores, config, @@ -482,6 +491,7 @@ export const createServices = ( integrationEventsService, onboardingService, personalDashboardService, + projectStatusService, }; }; @@ -533,4 +543,5 @@ export { IntegrationEventsService, OnboardingService, PersonalDashboardService, + ProjectStatusService, }; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 626553f6c1ce..e217abd570b8 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -57,6 +57,7 @@ import type { FeatureLifecycleService } from '../features/feature-lifecycle/feat import type { IntegrationEventsService } from '../features/integration-events/integration-events-service'; import type { OnboardingService } from '../features/onboarding/onboarding-service'; import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service'; +import type { ProjectStatusService } from '../features/project-status/project-status-service'; export interface IUnleashServices { transactionalAccessService: WithTransactional; @@ -126,4 +127,5 @@ export interface IUnleashServices { integrationEventsService: IntegrationEventsService; onboardingService: OnboardingService; personalDashboardService: PersonalDashboardService; + projectStatusService: ProjectStatusService; }