diff --git a/apps/api/src/dtos/guild-leaderboard.dto.ts b/apps/api/src/dtos/guild-leaderboard.dto.ts index 483977c3a..6b98ee790 100644 --- a/apps/api/src/dtos/guild-leaderboard.dto.ts +++ b/apps/api/src/dtos/guild-leaderboard.dto.ts @@ -9,7 +9,6 @@ import { ApiProperty } from "@nestjs/swagger"; import { Guild, getLeaderboardFields } from "@statsify/schemas"; import { - IsEnum, IsInt, IsOptional, IsString, @@ -17,12 +16,13 @@ import { Min, MinLength, } from "class-validator"; +import { IsLeaderboardField } from "./leaderboard-field.decorator.js"; import { Transform } from "class-transformer"; const fields = getLeaderboardFields(Guild).map(([key]) => key); export class GuildLeaderboardDto { - @IsEnum(fields) + @IsLeaderboardField(fields) @ApiProperty({ enum: fields }) public field: string; diff --git a/apps/api/src/dtos/guild-rankings.dto.ts b/apps/api/src/dtos/guild-rankings.dto.ts index bb3b8f798..60fc81dc1 100644 --- a/apps/api/src/dtos/guild-rankings.dto.ts +++ b/apps/api/src/dtos/guild-rankings.dto.ts @@ -8,13 +8,15 @@ import { ApiProperty } from "@nestjs/swagger"; import { Guild, getLeaderboardFields } from "@statsify/schemas"; -import { IsEnum, IsString, MaxLength, MinLength } from "class-validator"; +import { IsArray, IsString, MaxLength, MinLength } from "class-validator"; +import { IsLeaderboardField } from "./leaderboard-field.decorator.js"; const fields = getLeaderboardFields(Guild).map(([key]) => key); export class GuildRankingDto { @ApiProperty({ enum: fields, type: [String] }) - @IsEnum(fields, { each: true }) + @IsArray() + @IsLeaderboardField(fields, { each: true }) public fields: string[]; @IsString() diff --git a/apps/api/src/dtos/leaderboard-field.decorator.ts b/apps/api/src/dtos/leaderboard-field.decorator.ts new file mode 100644 index 000000000..db6428d8a --- /dev/null +++ b/apps/api/src/dtos/leaderboard-field.decorator.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Statsify + * + * This source code is licensed under the GNU GPL v3 license found in the + * LICENSE file in the root directory of this source tree. + * https://github.com/Statsify/statsify/blob/main/LICENSE + */ + +import { ValidateBy, type ValidationOptions } from "class-validator"; + +export const IsLeaderboardField = (fields: string[], options?: ValidationOptions) => { + const fieldSet = new Set(fields); + + return ValidateBy( + { + name: "isLeaderboardField", + validator: { + validate: (value) => fieldSet.has(value), + defaultMessage: (args) => { + const value = args?.value; + if (value === undefined) return `${args?.property} is required`; + + const values = options?.each && Array.isArray(value) ? value : [value]; + const invalid = new Set( + values.filter((field) => !fieldSet.has(field)).map((field) => JSON.stringify(field)) + ); + + return `unknown leaderboard field${invalid.size === 1 ? "" : "s"}: ${[...invalid].join(", ")}`; + }, + }, + }, + options + ); +}; diff --git a/apps/api/src/dtos/player-leaderboard.dto.ts b/apps/api/src/dtos/player-leaderboard.dto.ts index 0e39b70ed..8ef1d3e35 100644 --- a/apps/api/src/dtos/player-leaderboard.dto.ts +++ b/apps/api/src/dtos/player-leaderboard.dto.ts @@ -7,7 +7,8 @@ */ import { ApiProperty, PartialType } from "@nestjs/swagger"; -import { IsEnum, IsInt, IsOptional, Min } from "class-validator"; +import { IsInt, IsOptional, Min } from "class-validator"; +import { IsLeaderboardField } from "./leaderboard-field.decorator.js"; import { Player, getLeaderboardFields } from "@statsify/schemas"; import { PlayerDto } from "./player.dto.js"; import { Transform } from "class-transformer"; @@ -15,7 +16,7 @@ import { Transform } from "class-transformer"; const fields = getLeaderboardFields(Player).map(([key]) => key); export class PlayerLeaderboardDto extends PartialType(PlayerDto) { - @IsEnum(fields) + @IsLeaderboardField(fields) @ApiProperty({ enum: fields }) public field: string; diff --git a/apps/api/src/dtos/player-rankings.dto.ts b/apps/api/src/dtos/player-rankings.dto.ts index 8f29c9010..f627bcc00 100644 --- a/apps/api/src/dtos/player-rankings.dto.ts +++ b/apps/api/src/dtos/player-rankings.dto.ts @@ -7,7 +7,8 @@ */ import { ApiProperty } from "@nestjs/swagger"; -import { IsEnum } from "class-validator"; +import { IsArray } from "class-validator"; +import { IsLeaderboardField } from "./leaderboard-field.decorator.js"; import { Player, getLeaderboardFields } from "@statsify/schemas"; import { UuidDto } from "./uuid.dto.js"; @@ -15,6 +16,7 @@ const fields = getLeaderboardFields(Player).map(([key]) => key); export class PlayerRankingsDto extends UuidDto { @ApiProperty({ enum: fields, type: [String] }) - @IsEnum(fields, { each: true }) + @IsArray() + @IsLeaderboardField(fields, { each: true }) public fields: string[]; }