From 48f74eb5a93efbecf0dcbc3271199c05f6f4b9cd Mon Sep 17 00:00:00 2001 From: Lauest Date: Wed, 12 Feb 2025 12:42:05 +0100 Subject: [PATCH] added endpoint for application dashboard data --- .../application.controller.ts | 41 +++++++ .../dto/applications-dashboard-responses.ts | 10 ++ .../device-management/application.service.ts | 103 ++++++++++++------ 3 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 src/entities/dto/applications-dashboard-responses.ts diff --git a/src/controllers/admin-controller/application.controller.ts b/src/controllers/admin-controller/application.controller.ts index c84a368e..68045b24 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -30,6 +30,7 @@ import { ComposeAuthGuard } from "@auth/compose-auth.guard"; import { ApplicationAdmin, Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ApiAuth } from "@auth/swagger-auth-decorator"; +import { ApplicationDashboardResponseDto } from "@dto/applications-dashboard-responses"; import { CreateApplicationDto } from "@dto/create-application.dto"; import { DeleteResponseDto } from "@dto/delete-application-response.dto"; import { ListAllApplicationsResponseDto } from "@dto/list-all-applications-response.dto"; @@ -100,6 +101,22 @@ export class ApplicationController { } } + @Read() + @Get(":id/application-dashboard-data") + @ApiProduces("application/json") + @ApiOperation({ summary: "returns applications dashboard data" }) + @ApiNotFoundResponse() + async countApplicationWithError( + @Req() req: AuthenticatedRequest, + @Param("id", new ParseIntPipe()) id: number + ): Promise { + try { + return await this.getApplicationsWithError(req, id, req.user.permissions.isGlobalAdmin); + } catch (err) { + throw new NotFoundException(ErrorCodes.IdDoesNotExists); + } + } + @Read() @Get(":id") @ApiOperation({ summary: "Find one Application by id" }) @@ -295,4 +312,28 @@ export class ApplicationController { const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); return await this.applicationService.findFilterInformation(allowedApplications, organizationId); } + + private async getApplicationsWithError(req: AuthenticatedRequest, organizationId: number, isGlobalAdmin: boolean) { + if (isGlobalAdmin) { + return { + ...(await this.applicationService.countApplicationsWithError(organizationId)), + totalDevices: await this.applicationService.countAllDevices(organizationId), + }; + } + + const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); + + if (allFromOrg.some(x => x === organizationId)) { + return { + ...(await this.applicationService.countApplicationsWithError(organizationId)), + totalDevices: await this.applicationService.countAllDevices(organizationId), + }; + } + + const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + return { + ...(await this.applicationService.countApplicationsWithError(organizationId, allowedApplications)), + totalDevices: await this.applicationService.countAllDevices(organizationId, allowedApplications), + }; + } } diff --git a/src/entities/dto/applications-dashboard-responses.ts b/src/entities/dto/applications-dashboard-responses.ts new file mode 100644 index 00000000..d1e643d2 --- /dev/null +++ b/src/entities/dto/applications-dashboard-responses.ts @@ -0,0 +1,10 @@ +export type ApplicationsWithErrorsResponseDto = { + total: number; + withError: number; +}; + +export type ApplicationDashboardResponseDto = { + total: number; + withError: number; + totalDevices: number; +}; diff --git a/src/services/device-management/application.service.ts b/src/services/device-management/application.service.ts index 651911fc..87f43788 100644 --- a/src/services/device-management/application.service.ts +++ b/src/services/device-management/application.service.ts @@ -1,3 +1,4 @@ +import { ApplicationsWithErrorsResponseDto } from "@dto/applications-dashboard-responses"; import { CreateApplicationDto } from "@dto/create-application.dto"; import { ListAllApplicationsResponseDto, @@ -30,7 +31,7 @@ import { MulticastService } from "@services/chirpstack/multicast.service"; import { DataTargetService } from "@services/data-targets/data-target.service"; import { OrganizationService } from "@services/user-management/organization.service"; import { PermissionService } from "@services/user-management/permission.service"; -import { DeleteResult, In, Repository } from "typeorm"; +import { Brackets, DeleteResult, In, Repository } from "typeorm"; @Injectable() export class ApplicationService { @@ -52,6 +53,58 @@ export class ApplicationService { private chirpstackApplicationService: ApplicationChirpstackService ) {} + async countApplicationsWithError( + organizationId: number, + whitelist?: number[] + ): Promise { + const queryBuilder = this.applicationRepository + .createQueryBuilder("app") + .leftJoin("app.iotDevices", "device") + .leftJoin("app.belongsTo", "organization") + .leftJoin("device.latestReceivedMessage", "latestMessage") + .leftJoin("app.dataTargets", "dataTargets") + .andWhere("app.belongsToId = :organizationId", { organizationId: organizationId }); + + if (whitelist && whitelist.length > 0) { + queryBuilder.where("app.id IN (:...whitelist)", { whitelist }); + } + + queryBuilder.andWhere( + new Brackets(qb => { + qb.where("dataTargets.id IS NULL").orWhere("latestMessage.sentTime < NOW() - INTERVAL '24 HOURS'"); + }) + ); + + try { + const [result, total] = await queryBuilder.getManyAndCount(); + + return { + withError: result.length, + total: total, + }; + } catch (error) { + throw new Error("Database query failed"); + } + } + async countAllDevices(organizationId: number, whitelist?: number[]): Promise { + const queryBuilder = this.applicationRepository + .createQueryBuilder("app") + .leftJoinAndSelect("app.iotDevices", "device") + .leftJoin("app.dataTargets", "dataTargets") + .where("app.belongsToId = :organizationId", { organizationId }); + + if (whitelist && whitelist.length > 0) { + queryBuilder.andWhere("app.id IN (:...whitelist)", { whitelist }); + } + + const count = await queryBuilder.select("COUNT(device.id)", "count").getRawOne(); + + try { + return count.count ? parseInt(count.count, 10) : 0; + } catch (error) { + throw new Error("Database query failed"); + } + } async findAndCountInList( query?: ListAllApplicationsDto, whitelist?: number[] @@ -61,9 +114,9 @@ export class ApplicationService { const queryBuilder = this.applicationRepository .createQueryBuilder("app") .leftJoinAndSelect("app.iotDevices", "device") - .leftJoinAndSelect("app.dataTargets", "dataTarget") .leftJoinAndSelect("app.belongsTo", "organization") - .leftJoinAndSelect("device.latestReceivedMessage", "latestMessage"); + .leftJoinAndSelect("device.latestReceivedMessage", "latestMessage") + .leftJoinAndSelect("app.dataTargets", "dataTargets"); if (whitelist && whitelist.length > 0) { queryBuilder.where("app.id IN (:...whitelist)", { whitelist }); @@ -74,7 +127,6 @@ export class ApplicationService { } if (query.status) { - console.log("status : " + query.status); queryBuilder.andWhere("app.status = :status", { status: query.status }); } @@ -82,35 +134,19 @@ export class ApplicationService { queryBuilder.andWhere("app.owner = :owner", { owner: query.owner }); } - if (query.statusCheck) { + if (query.statusCheck === "alert") { queryBuilder.andWhere( - ` - app.id IN ( - SELECT - app.id, - CASE - WHEN COUNT(DISTINCT dataTarget.id) = 0 THEN 'alert' - WHEN latestMessage.sentTime < NOW() - INTERVAL '24 HOURS' THEN 'alert' - ELSE 'stable' - END AS statusCheck - FROM application app - LEFT JOIN data_target dataTarget ON dataTarget.applicationId = app.id - LEFT JOIN device ON device.applicationId = app.id - LEFT JOIN latestMessage ON latestMessage.deviceId = device.id - WHERE app.belongsToId = :organizationId - GROUP BY app.id - HAVING - ( - COUNT(DISTINCT dataTarget.id) = 0 - OR latestMessage.sentTime < NOW() - INTERVAL '24 HOURS' - ) - AND ( - :statusCheck = 'alert' - OR COUNT(DISTINCT dataTarget.id) > 0 - ) - ) - `, - { statusCheck: query.statusCheck, organizationId: query.organizationId } + new Brackets(qb => { + qb.where("dataTargets.id IS NULL").orWhere("latestMessage.sentTime < NOW() - INTERVAL '24 HOURS'"); + }) + ); + } + + if (query.statusCheck === "stable") { + queryBuilder.andWhere( + new Brackets(qb => { + qb.where("dataTargets.id IS NOT NULL").orWhere("latestMessage.sentTime > NOW() - INTERVAL '24 HOURS'"); + }) ); } @@ -145,7 +181,6 @@ export class ApplicationService { count: total, }; } catch (error) { - console.error("Error executing query:", error); throw new Error("Database query failed"); } } @@ -252,8 +287,6 @@ export class ApplicationService { } async findFilterInformation(applicationIds: number[] | "admin", organizationId: number) { - console.log(applicationIds); - const query = this.applicationRepository .createQueryBuilder("application") .leftJoinAndSelect("application.belongsTo", "organization")