Skip to content

Commit

Permalink
feat: project status backend structure (#8630)
Browse files Browse the repository at this point in the history
Adding project status schema definition, controller, service, e2e test.

Next PR will add functionality for activity object.

---------

Co-authored-by: Thomas Heartman <[email protected]>
  • Loading branch information
sjaanus and thomasheartman authored Nov 1, 2024
1 parent c9a564a commit c9dc526
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/lib/features/project-status/createProjectStatusService.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
71 changes: 71 additions & 0 deletions src/lib/features/project-status/project-status-controller.ts
Original file line number Diff line number Diff line change
@@ -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<IProjectParam, unknown, unknown, unknown>,
res: Response<ProjectStatusSchema>,
): Promise<void> {
const { projectId } = req.params;
const status: ProjectStatusSchema =
await this.projectStatusService.getProjectStatus(projectId);

this.openApiService.respondWithValidation(
200,
res,
projectStatusSchema.$id,
serializeDates(status),
);
}
}
9 changes: 9 additions & 0 deletions src/lib/features/project-status/project-status-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { ProjectStatusSchema } from '../../openapi';

export class ProjectStatusService {
constructor() {}

async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
return { activityCountByDate: [{ date: '2024-09-11', count: 0 }] };
}
}
40 changes: 40 additions & 0 deletions src/lib/features/project-status/projects-status.e2e.test.ts
Original file line number Diff line number Diff line change
@@ -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 }],
});
});
2 changes: 2 additions & 0 deletions src/lib/features/project/project-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
2 changes: 2 additions & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down
30 changes: 30 additions & 0 deletions src/lib/openapi/spec/project-activity-schema.ts
Original file line number Diff line number Diff line change
@@ -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<typeof projectActivitySchema>;
15 changes: 15 additions & 0 deletions src/lib/openapi/spec/project-status-schema.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
25 changes: 25 additions & 0 deletions src/lib/openapi/spec/project-status-schema.ts
Original file line number Diff line number Diff line change
@@ -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<typeof projectStatusSchema>;
11 changes: 11 additions & 0 deletions src/lib/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -482,6 +491,7 @@ export const createServices = (
integrationEventsService,
onboardingService,
personalDashboardService,
projectStatusService,
};
};

Expand Down Expand Up @@ -533,4 +543,5 @@ export {
IntegrationEventsService,
OnboardingService,
PersonalDashboardService,
ProjectStatusService,
};
2 changes: 2 additions & 0 deletions src/lib/types/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AccessService>;
Expand Down Expand Up @@ -126,4 +127,5 @@ export interface IUnleashServices {
integrationEventsService: IntegrationEventsService;
onboardingService: OnboardingService;
personalDashboardService: PersonalDashboardService;
projectStatusService: ProjectStatusService;
}

0 comments on commit c9dc526

Please sign in to comment.