From 783ec99319b44f3a2b8ce2d962d99ffae0e045c6 Mon Sep 17 00:00:00 2001 From: fcv-iteratorIt <123963294+fcv-iteratorIt@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:28:35 +0100 Subject: [PATCH] IOT-1276: Implemented sorting for gateway table (#234) --- .../application.controller.ts | 54 ++++++++-------- .../chirpstack-gateway.controller.ts | 8 +-- .../dto/chirpstack/chirpstack-get-all.dto.ts | 6 -- .../list-all-gateways-response.dto.ts | 11 ++++ .../dto/chirpstack/list-all-gateways.dto.ts | 31 +++++++--- src/entities/dto/list-all-entities.dto.ts | 3 +- .../chirpstack/chirpstack-gateway.service.ts | 62 +++++++++++++++++-- .../chirpstack/gateway-boostrapper.service.ts | 2 +- .../lorawan-device-database-enrich-job.ts | 1 + 9 files changed, 125 insertions(+), 53 deletions(-) delete mode 100644 src/entities/dto/chirpstack/chirpstack-get-all.dto.ts create mode 100644 src/entities/dto/chirpstack/list-all-gateways-response.dto.ts diff --git a/src/controllers/admin-controller/application.controller.ts b/src/controllers/admin-controller/application.controller.ts index f526c175..efa2ec97 100644 --- a/src/controllers/admin-controller/application.controller.ts +++ b/src/controllers/admin-controller/application.controller.ts @@ -85,33 +85,6 @@ export class ApplicationController { return await this.getApplicationsForNonGlobalAdmin(req, query); } - private async getApplicationsForNonGlobalAdmin(req: AuthenticatedRequest, query: ListAllApplicationsDto) { - if (query?.organizationId) { - checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); - return await this.getApplicationsInOrganization(req, query); - } - - const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); - const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - const applications = await this.applicationService.findAndCountApplicationInWhitelistOrOrganization( - query, - allowedApplications, - query.organizationId ? [query.organizationId] : allFromOrg - ); - return applications; - } - - private async getApplicationsInOrganization(req: AuthenticatedRequest, query: ListAllApplicationsDto) { - // User admins have access to all applications in the organization - const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); - if (allFromOrg.some(x => x === query?.organizationId)) { - return await this.applicationService.findAndCountWithPagination(query, [query.organizationId]); - } - - const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); - return await this.applicationService.findAndCountInList(query, allowedApplications, [query.organizationId]); - } - @Read() @Get(":id") @ApiOperation({ summary: "Find one Application by id" }) @@ -221,4 +194,31 @@ export class ApplicationController { throw new NotFoundException(err); } } + + private async getApplicationsForNonGlobalAdmin(req: AuthenticatedRequest, query: ListAllApplicationsDto) { + if (query?.organizationId) { + checkIfUserHasAccessToOrganization(req, query.organizationId, OrganizationAccessScope.ApplicationRead); + return await this.getApplicationsInOrganization(req, query); + } + + const allFromOrg = req.user.permissions.getAllOrganizationsWithApplicationAdmin(); + const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + const applications = await this.applicationService.findAndCountApplicationInWhitelistOrOrganization( + query, + allowedApplications, + query.organizationId ? [query.organizationId] : allFromOrg + ); + return applications; + } + + private async getApplicationsInOrganization(req: AuthenticatedRequest, query: ListAllApplicationsDto) { + // User admins have access to all applications in the organization + const allFromOrg = req.user.permissions.getAllOrganizationsWithUserAdmin(); + if (allFromOrg.some(x => x === query?.organizationId)) { + return await this.applicationService.findAndCountWithPagination(query, [query.organizationId]); + } + + const allowedApplications = req.user.permissions.getAllApplicationsWithAtLeastRead(); + return await this.applicationService.findAndCountInList(query, allowedApplications, [query.organizationId]); + } } diff --git a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts index d3c00236..f99e51a4 100644 --- a/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts +++ b/src/controllers/admin-controller/chirpstack/chirpstack-gateway.controller.ts @@ -18,7 +18,6 @@ import { GatewayAdmin, Read } from "@auth/roles.decorator"; import { RolesGuard } from "@auth/roles.guard"; import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; -import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; import { SingleGatewayResponseDto } from "@dto/chirpstack/single-gateway-response.dto"; import { UpdateGatewayDto } from "@dto/chirpstack/update-gateway.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; @@ -27,9 +26,10 @@ import { checkIfUserHasAccessToOrganization, OrganizationAccessScope } from "@he import { AuthenticatedRequest } from "@dto/internal/authenticated-request"; import { AuditLog } from "@services/audit-log.service"; import { ActionType } from "@entities/audit-log-entry"; -import { ChirpstackGetAll } from "@dto/chirpstack/chirpstack-get-all.dto"; import { ComposeAuthGuard } from "@auth/compose-auth.guard"; import { ApiAuth } from "@auth/swagger-auth-decorator"; +import { ListAllGatewaysDto } from "@dto/chirpstack/list-all-gateways.dto"; +import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways-response.dto"; @ApiTags("Chirpstack") @Controller("chirpstack/gateway") @@ -75,8 +75,8 @@ export class ChirpstackGatewayController { @ApiProduces("application/json") @ApiOperation({ summary: "List all Chirpstack gateways" }) @Read() - async getAll(@Query() query?: ChirpstackGetAll): Promise { - return await this.chirpstackGatewayService.getAll(query.organizationId); + async getAll(@Query() query?: ListAllGatewaysDto): Promise { + return await this.chirpstackGatewayService.getWithPaginationAndSorting(query, query.organizationId); } @Get(":gatewayId") diff --git a/src/entities/dto/chirpstack/chirpstack-get-all.dto.ts b/src/entities/dto/chirpstack/chirpstack-get-all.dto.ts deleted file mode 100644 index 92cc2786..00000000 --- a/src/entities/dto/chirpstack/chirpstack-get-all.dto.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { PickType } from "@nestjs/swagger"; -import { ChirpstackPaginatedListDto } from "./chirpstack-paginated-list.dto"; - -export class ChirpstackGetAll extends PickType(ChirpstackPaginatedListDto, [ - "organizationId", -]) {} diff --git a/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts b/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts new file mode 100644 index 00000000..3b0e4ab9 --- /dev/null +++ b/src/entities/dto/chirpstack/list-all-gateways-response.dto.ts @@ -0,0 +1,11 @@ +import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "./gateway-response.dto"; + +export class ListAllGatewaysResponseDto { + totalCount: number; + resultList: GatewayResponseDto[]; +} + +export class ListAllChirpstackGatewaysResponseDto { + totalCount: number; + resultList: ChirpstackGatewayResponseDto[]; +} diff --git a/src/entities/dto/chirpstack/list-all-gateways.dto.ts b/src/entities/dto/chirpstack/list-all-gateways.dto.ts index 3b0e4ab9..82e2d30a 100644 --- a/src/entities/dto/chirpstack/list-all-gateways.dto.ts +++ b/src/entities/dto/chirpstack/list-all-gateways.dto.ts @@ -1,11 +1,26 @@ -import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "./gateway-response.dto"; +import { ApiProperty, OmitType, PickType } from "@nestjs/swagger"; +import { ChirpstackPaginatedListDto } from "./chirpstack-paginated-list.dto"; +import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { IsSwaggerOptional } from "@helpers/optional-validator"; +import { NullableStringToNumber, StringToNumber } from "@helpers/string-to-number-validator"; +import { IsNumber, IsOptional } from "class-validator"; +import { Transform } from "class-transformer"; +import { DefaultLimit, DefaultOffset } from "@config/constants/pagination-constants"; -export class ListAllGatewaysResponseDto { - totalCount: number; - resultList: GatewayResponseDto[]; -} +export class ListAllGatewaysDto extends OmitType(ListAllEntitiesDto, ["limit", "offset"]) { + @IsSwaggerOptional({ description: "Filter to one organization" }) + @StringToNumber() + organizationId?: number; + + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + limit? = DefaultLimit; -export class ListAllChirpstackGatewaysResponseDto { - totalCount: number; - resultList: ChirpstackGatewayResponseDto[]; + @ApiProperty({ type: Number, required: false }) + @IsOptional() + @IsNumber() + @Transform(({ value }) => NullableStringToNumber(value)) + offset? = DefaultOffset; } diff --git a/src/entities/dto/list-all-entities.dto.ts b/src/entities/dto/list-all-entities.dto.ts index 3a50d968..1aef0ff9 100644 --- a/src/entities/dto/list-all-entities.dto.ts +++ b/src/entities/dto/list-all-entities.dto.ts @@ -40,5 +40,6 @@ export class ListAllEntitiesDto { | "openDataDkEnabled" | "deviceModel" | "devices" - | "dataTargets"; + | "dataTargets" + | "organizationName"; } diff --git a/src/services/chirpstack/chirpstack-gateway.service.ts b/src/services/chirpstack/chirpstack-gateway.service.ts index 042fd6b7..4a8abd78 100644 --- a/src/services/chirpstack/chirpstack-gateway.service.ts +++ b/src/services/chirpstack/chirpstack-gateway.service.ts @@ -9,10 +9,6 @@ import { ChirpstackErrorResponseDto } from "@dto/chirpstack/chirpstack-error-res import { ChirpstackResponseStatus } from "@dto/chirpstack/chirpstack-response.dto"; import { CreateGatewayDto } from "@dto/chirpstack/create-gateway.dto"; import { GatewayStatsElementDto } from "@dto/chirpstack/gateway-stats.response.dto"; -import { - ListAllChirpstackGatewaysResponseDto, - ListAllGatewaysResponseDto, -} from "@dto/chirpstack/list-all-gateways.dto"; import { SingleGatewayResponseDto } from "@dto/chirpstack/single-gateway-response.dto"; import { UpdateGatewayContentsDto, UpdateGatewayDto } from "@dto/chirpstack/update-gateway.dto"; import { ErrorCodes } from "@enum/error-codes.enum"; @@ -33,13 +29,19 @@ import { GetGatewayMetricsResponse, GetGatewayResponse, ListGatewaysRequest, - UpdateGatewayRequest, ListGatewaysResponse, + UpdateGatewayRequest, } from "@chirpstack/chirpstack-api/api/gateway_pb"; import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb"; import { Aggregation, Location } from "@chirpstack/chirpstack-api/common/common_pb"; import { dateToTimestamp, timestampToDate } from "@helpers/date.helper"; import { ChirpstackGatewayResponseDto, GatewayResponseDto } from "@dto/chirpstack/gateway-response.dto"; +import { ListAllEntitiesDto } from "@dto/list-all-entities.dto"; +import { + ListAllChirpstackGatewaysResponseDto, + ListAllGatewaysResponseDto, +} from "@dto/chirpstack/list-all-gateways-response.dto"; + @Injectable() export class ChirpstackGatewayService extends GenericChirpstackConfigurationService { constructor( @@ -148,6 +150,32 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ }; } + public async getWithPaginationAndSorting( + queryParams?: ListAllEntitiesDto, + organizationId?: number + ): Promise { + const orderByColumn = this.getSortingForGateways(queryParams); + const direction = queryParams?.sort?.toUpperCase() === "DESC" ? "DESC" : "ASC"; + + let query = this.gatewayRepository + .createQueryBuilder("gateway") + .innerJoinAndSelect("gateway.organization", "organization") + .skip(queryParams?.offset ? +queryParams.offset : 0) + .take(queryParams.limit ? +queryParams.limit : 100) + .orderBy(orderByColumn, direction); + + if (organizationId) { + query = query.where('"organizationId" = :organizationId', { organizationId }); + } + + const [gateways, count] = await query.getManyAndCount(); + + return { + resultList: gateways.map(this.mapGatewayToResponseDto), + totalCount: count, + }; + } + async getOne(gatewayId: string): Promise { if (gatewayId?.length != 16) { throw new BadRequestException("Invalid gateway id"); @@ -294,9 +322,13 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ gatewayId: string, rxPacketsReceived: number, txPacketsEmitted: number, + updatedAt: Date, lastSeenAt: Date | undefined ) { - await this.gatewayRepository.update({ gatewayId }, { rxPacketsReceived, txPacketsEmitted, lastSeenAt }); + await this.gatewayRepository.update( + { gatewayId }, + { rxPacketsReceived, txPacketsEmitted, lastSeenAt, updatedAt } + ); } async ensureOrganizationIdIsSet( @@ -454,4 +486,22 @@ export class ChirpstackGatewayService extends GenericChirpstackConfigurationServ }; return responseList; } + + private getSortingForGateways(query: ListAllEntitiesDto) { + let orderBy = "gateway.id"; + + if (query.orderOn == null) { + return orderBy; + } + + if (query.orderOn === "organizationName") { + orderBy = "organization.name"; + } else if (query.orderOn === "status") { + orderBy = "gateway.lastSeenAt"; + } else { + orderBy = `gateway.${query.orderOn}`; + } + + return orderBy; + } } diff --git a/src/services/chirpstack/gateway-boostrapper.service.ts b/src/services/chirpstack/gateway-boostrapper.service.ts index 80fd9705..6457365b 100644 --- a/src/services/chirpstack/gateway-boostrapper.service.ts +++ b/src/services/chirpstack/gateway-boostrapper.service.ts @@ -1,8 +1,8 @@ -import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways.dto"; import { GatewayStatusHistory } from "@entities/gateway-status-history.entity"; import { Inject, InternalServerErrorException, Logger, OnApplicationBootstrap } from "@nestjs/common"; import { ChirpstackGatewayService } from "./chirpstack-gateway.service"; import { GatewayStatusHistoryService } from "./gateway-status-history.service"; +import { ListAllGatewaysResponseDto } from "@dto/chirpstack/list-all-gateways-response.dto"; /** * Verify if any gateways exist on chirpstack and haven't been loaded into the database. diff --git a/src/services/device-management/lorawan-device-database-enrich-job.ts b/src/services/device-management/lorawan-device-database-enrich-job.ts index f6db7a63..6bfe881a 100644 --- a/src/services/device-management/lorawan-device-database-enrich-job.ts +++ b/src/services/device-management/lorawan-device-database-enrich-job.ts @@ -60,6 +60,7 @@ export class LorawanDeviceDatabaseEnrichJob { gateway.gatewayId, stats.rxPacketsReceived, stats.txPacketsEmitted, + gateway.updatedAt, chirpstackGateway.lastSeenAt ? timestampToDate(chirpstackGateway.lastSeenAt) : undefined ); } catch (err) {